diff --git a/.changeset/clear-cars-tan.md b/.changeset/clear-cars-tan.md new file mode 100644 index 0000000000..1051ef7f2f --- /dev/null +++ b/.changeset/clear-cars-tan.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': patch +--- + +Return a url from the externalIdp function for app developers to use to redirect their url diff --git a/e2e/davinci-app/components/social-login-button.ts b/e2e/davinci-app/components/social-login-button.ts index 64a2416970..a7a461efc5 100644 --- a/e2e/davinci-app/components/social-login-button.ts +++ b/e2e/davinci-app/components/social-login-button.ts @@ -5,17 +5,25 @@ * of the MIT license. See the LICENSE file for details. */ import type { IdpCollector } from '@forgerock/davinci-client/types'; +import { InternalErrorResponse } from 'packages/davinci-client/src/lib/client.types.js'; export default function submitButtonComponent( formEl: HTMLFormElement, collector: IdpCollector, - updater: () => void, + updater: () => Promise, ) { const button = document.createElement('button'); console.log('collector', collector); button.value = collector.output.label; button.innerHTML = collector.output.label; - button.onclick = () => updater(); + button.onclick = async () => { + await updater(); + if ('url' in collector.output && typeof collector.output.url === 'string') { + window.location.assign(collector.output.url); + } else { + console.error('no url to continue from'); + } + }; formEl?.appendChild(button); } diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index 7f3a57d5ec..4c05d65233 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -239,8 +239,7 @@ const urlParams = new URLSearchParams(window.location.search); ); } else if (collector.type === 'IdpCollector') { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - collector; - socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp(collector)); + socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp()); } else if (collector.type === 'FlowCollector') { flowLinkComponent( formEl, // You can ignore this; it's just for rendering diff --git a/packages/davinci-client/package.json b/packages/davinci-client/package.json index 3ae592c595..fc1ef65d6d 100644 --- a/packages/davinci-client/package.json +++ b/packages/davinci-client/package.json @@ -27,6 +27,7 @@ "@forgerock/sdk-oidc": "workspace:*", "@forgerock/sdk-request-middleware": "workspace:*", "@forgerock/sdk-types": "workspace:*", + "@forgerock/storage": "workspace:*", "@reduxjs/toolkit": "catalog:", "immer": "catalog:" }, diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index a38e320aa7..39b3f03192 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -8,6 +8,7 @@ * Import RTK slices and api */ import { CustomLogger, logger as loggerFn, LogLevel } from '@forgerock/sdk-logger'; +import { createStorage } from '@forgerock/storage'; import { createClientStore, handleUpdateValidateError, RootState } from './client.store.utils.js'; import { nodeSlice } from './node.slice.js'; @@ -28,15 +29,19 @@ import type { } from './davinci.types.js'; import type { SingleValueCollectors, - IdpCollector, MultiSelectCollector, ObjectValueCollectors, PhoneNumberInputValue, } from './collector.types.js'; -import type { InitFlow, NodeStates, Updater, Validator } from './client.types.js'; +import type { + InitFlow, + InternalErrorResponse, + NodeStates, + Updater, + Validator, +} from './client.types.js'; import { returnValidator } from './collector.utils.js'; -import { authorize } from './davinci.utils.js'; -import { StartNode } from './node.types.js'; +import { ContinueNode, StartNode } from './node.types.js'; /** * Create a client function that returns a set of methods @@ -60,7 +65,10 @@ export async function davinci({ }) { const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); const store = createClientStore({ requestMiddleware, logger: log }); - + const serverInfo = createStorage( + { storeType: 'localStorage' }, + 'serverInfo', + ); if (!config.serverConfig.wellknown) { const error = new Error( '`wellknown` property is a required as part of the `config.serverConfig`', @@ -108,12 +116,24 @@ export async function davinci({ * @param collector IdpCollector * @returns {function} */ - externalIdp: (collector: IdpCollector) => { + externalIdp: (): (() => Promise) => { const rootState: RootState = store.getState(); - const serverSlice = nodeSlice.selectors.selectServer(rootState); - return () => authorize(serverSlice, collector, log); + if (serverSlice && serverSlice.status === 'continue') { + return async () => { + await serverInfo.set(serverSlice); + }; + } + return async () => { + return { + error: { + message: + 'Not in a continue node state, must be in a continue node to use external idp method', + type: 'state_error', + }, + } as InternalErrorResponse; + }; }, /** @@ -166,12 +186,32 @@ export async function davinci({ * @method: resume - Resume a social login flow when returned to application * @returns unknown */ - resume: async ({ continueToken }: { continueToken: string }) => { - await store.dispatch(davinciApi.endpoints.resume.initiate({ continueToken })); + resume: async ({ + continueToken, + }: { + continueToken: string; + }): Promise => { + try { + const storedServerInfo = (await serverInfo.get()) as ContinueNode['server']; + await store.dispatch( + davinciApi.endpoints.resume.initiate({ continueToken, serverInfo: storedServerInfo }), + ); + await serverInfo.remove(); - const node = nodeSlice.selectSlice(store.getState()); + const node = nodeSlice.selectSlice(store.getState()); - return node; + return node; + } catch { + // logger.error('No url found in collector, social login needs a url in the collector'); + return { + error: { + message: + 'No url found in storage, social login needs a continue url which is saved in local storage. You may have cleared your browser data', + type: 'internal_error', + }, + type: 'internal_error', + }; + } }, /** diff --git a/packages/davinci-client/src/lib/davinci.api.ts b/packages/davinci-client/src/lib/davinci.api.ts index bcce8dc2ec..f259a6bfed 100644 --- a/packages/davinci-client/src/lib/davinci.api.ts +++ b/packages/davinci-client/src/lib/davinci.api.ts @@ -347,10 +347,10 @@ export const davinciApi = createApi({ handleResponse(cacheEntry, api.dispatch, response?.status || 0, logger); }, }), - resume: builder.query({ - async queryFn({ continueToken }, api, _c, baseQuery) { - const continueUrl = window.localStorage.getItem('continueUrl') || null; + resume: builder.query({ + async queryFn({ serverInfo, continueToken }, api, _c, baseQuery) { const { requestMiddleware, logger } = api.extra as Extras; + const links = serverInfo._links; if (!continueToken) { return { @@ -362,7 +362,12 @@ export const davinciApi = createApi({ }, }; } - if (!continueUrl) { + if ( + !links || + !('continue' in links) || + !('href' in links['continue']) || + !links['continue'].href + ) { return { error: { data: 'No continue url', @@ -373,10 +378,7 @@ export const davinciApi = createApi({ }; } - if (continueUrl) { - window.localStorage.removeItem('continueUrl'); - } - + const continueUrl = links['continue'].href; const request: FetchArgs = { url: continueUrl, credentials: 'include', diff --git a/packages/davinci-client/src/lib/davinci.utils.ts b/packages/davinci-client/src/lib/davinci.utils.ts index 9695e8e624..27f4891578 100644 --- a/packages/davinci-client/src/lib/davinci.utils.ts +++ b/packages/davinci-client/src/lib/davinci.utils.ts @@ -11,8 +11,6 @@ import type { Dispatch } from '@reduxjs/toolkit'; import { logger as loggerFn } from '@forgerock/sdk-logger'; -import type { RootState } from './client.store.utils.js'; - import { nodeSlice } from './node.slice.js'; import type { @@ -24,9 +22,7 @@ import type { DaVinciSuccessResponse, } from './davinci.types.js'; import type { ContinueNode } from './node.types.js'; -import { DeviceValue, IdpCollector, PhoneNumberInputValue } from './collector.types.js'; -import { InternalErrorResponse } from './client.types.js'; - +import { DeviceValue, PhoneNumberInputValue } from './collector.types.js'; /** * @function transformSubmitRequest - Transforms a NextNode into a DaVinciRequest for form submissions * @param {ContinueNode} node - The node to transform into a DaVinciRequest @@ -245,40 +241,3 @@ export function handleResponse( } } } - -export function authorize( - serverSlice: RootState['node']['server'], - collector: IdpCollector, - logger: ReturnType, -): InternalErrorResponse | void { - if (serverSlice && '_links' in serverSlice) { - const continueUrl = serverSlice._links?.['continue']?.href ?? null; - if (continueUrl) { - window.localStorage.setItem('continueUrl', continueUrl); - if (collector.output.url) { - window.location.assign(collector.output.url); - } - } else { - logger.error('No url found in collector, social login needs a url in the collector'); - return { - error: { - message: - 'No url found in collector, social login needs a url in the collector to navigate to', - type: 'network_error', - }, - type: 'internal_error', - }; - } - logger.error( - 'No Continue Url found, social login needs a continue url to be saved in localStorage', - ); - return { - error: { - message: - 'No Continue Url found, social login needs a continue url to be saved in localStorage', - type: 'network_error', - }, - type: 'internal_error', - }; - } -} diff --git a/packages/davinci-client/tsconfig.json b/packages/davinci-client/tsconfig.json index 8bf1ebed66..906cfbf878 100644 --- a/packages/davinci-client/tsconfig.json +++ b/packages/davinci-client/tsconfig.json @@ -11,7 +11,7 @@ }, "references": [ { - "path": "../sdk-effects/logger" + "path": "../sdk-effects/storage" }, { "path": "../sdk-types" @@ -22,6 +22,9 @@ { "path": "../sdk-effects/oidc" }, + { + "path": "../sdk-effects/logger" + }, { "path": "./tsconfig.lib.json" }, diff --git a/packages/davinci-client/tsconfig.lib.json b/packages/davinci-client/tsconfig.lib.json index 38959c91ee..ba359f9877 100644 --- a/packages/davinci-client/tsconfig.lib.json +++ b/packages/davinci-client/tsconfig.lib.json @@ -32,7 +32,7 @@ ], "references": [ { - "path": "../sdk-effects/logger/tsconfig.lib.json" + "path": "../sdk-effects/storage/tsconfig.lib.json" }, { "path": "../sdk-types/tsconfig.lib.json" @@ -42,6 +42,9 @@ }, { "path": "../sdk-effects/oidc/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/logger/tsconfig.lib.json" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c2226da88..08ac9357a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,9 @@ importers: '@forgerock/sdk-types': specifier: workspace:* version: link:../sdk-types + '@forgerock/storage': + specifier: workspace:* + version: link:../sdk-effects/storage '@reduxjs/toolkit': specifier: 'catalog:' version: 2.8.2