Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-cars-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': patch
---

Return a url from the externalIdp function for app developers to use to redirect their url
12 changes: 10 additions & 2 deletions e2e/davinci-app/components/social-login-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void | InternalErrorResponse>,
) {
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);
}
3 changes: 1 addition & 2 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/davinci-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
},
Expand Down
64 changes: 52 additions & 12 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -60,7 +65,10 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
}) {
const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom });
const store = createClientStore({ requestMiddleware, logger: log });

const serverInfo = createStorage<ContinueNode['server']>(
{ storeType: 'localStorage' },
'serverInfo',
);
if (!config.serverConfig.wellknown) {
const error = new Error(
'`wellknown` property is a required as part of the `config.serverConfig`',
Expand Down Expand Up @@ -108,12 +116,24 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
* @param collector IdpCollector
* @returns {function}
*/
externalIdp: (collector: IdpCollector) => {
externalIdp: (): (() => Promise<void | InternalErrorResponse>) => {
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;
};
},

/**
Expand Down Expand Up @@ -166,12 +186,32 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
* @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<InternalErrorResponse | NodeStates> => {
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',
};
}
},

/**
Expand Down
18 changes: 10 additions & 8 deletions packages/davinci-client/src/lib/davinci.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,10 @@ export const davinciApi = createApi({
handleResponse(cacheEntry, api.dispatch, response?.status || 0, logger);
},
}),
resume: builder.query<unknown, { continueToken: string }>({
async queryFn({ continueToken }, api, _c, baseQuery) {
const continueUrl = window.localStorage.getItem('continueUrl') || null;
resume: builder.query<unknown, { serverInfo: ContinueNode['server']; continueToken: string }>({
async queryFn({ serverInfo, continueToken }, api, _c, baseQuery) {
const { requestMiddleware, logger } = api.extra as Extras;
const links = serverInfo._links;

if (!continueToken) {
return {
Expand All @@ -362,7 +362,12 @@ export const davinciApi = createApi({
},
};
}
if (!continueUrl) {
if (
!links ||
!('continue' in links) ||
!('href' in links['continue']) ||
!links['continue'].href
) {
Comment on lines +365 to +370
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes! Completely non-blocking, but it would be nice to have some sort of selector or lens to prevent this type of object path checking.

return {
error: {
data: 'No continue url',
Expand All @@ -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',
Expand Down
43 changes: 1 addition & 42 deletions packages/davinci-client/src/lib/davinci.utils.ts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is the right location for this functionality. This utils file is supposed to be tied to the davinci.api.ts file, which this function doesn't directly relate to it. I think we need to evaluate where this logic lives.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -245,40 +241,3 @@ export function handleResponse(
}
}
}

export function authorize(
serverSlice: RootState['node']['server'],
collector: IdpCollector,
logger: ReturnType<typeof loggerFn>,
): 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',
};
}
}
5 changes: 4 additions & 1 deletion packages/davinci-client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"references": [
{
"path": "../sdk-effects/logger"
"path": "../sdk-effects/storage"
},
{
"path": "../sdk-types"
Expand All @@ -22,6 +22,9 @@
{
"path": "../sdk-effects/oidc"
},
{
"path": "../sdk-effects/logger"
},
{
"path": "./tsconfig.lib.json"
},
Expand Down
5 changes: 4 additions & 1 deletion packages/davinci-client/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -42,6 +42,9 @@
},
{
"path": "../sdk-effects/oidc/tsconfig.lib.json"
},
{
"path": "../sdk-effects/logger/tsconfig.lib.json"
}
]
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading