diff --git a/packages/sdks/angular-sdk/README.md b/packages/sdks/angular-sdk/README.md index ff3d1ddf7..dbd76065e 100644 --- a/packages/sdks/angular-sdk/README.md +++ b/packages/sdks/angular-sdk/README.md @@ -203,6 +203,37 @@ export class AppComponent { > ``` +### `onScreenUpdate` + +A function that is called whenever there is a new screen state and after every next call. It receives the following parameters: + +- `screenName`: The name of the screen that is about to be rendered +- `state`: An object containing the upcoming screen state +- `next`: A function that, when called, continues the flow execution +- `ref`: A reference to the descope-wc node + +The function can be sync or async, and should return a boolean indicating whether a custom screen should be rendered: + +- `true`: Render a custom screen +- `false`: Render the default flow screen + +This function allows rendering custom screens instead of the default flow screens. +It can be useful for highly customized UIs or specific logic not covered by the default screens + +To render a custom screen, its elements should be appended as children of the `descope` component + +Usage example: + +```javascript +function onScreenUpdate(screenName, state, next, ref) { + if (screenName === 'My Custom Screen') { + return true; + } + + return false; +} +``` + #### Standalone Mode All components in the sdk are standalone, so you can use them by directly importing them to your components. diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.spec.ts b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.spec.ts index 8ad8743e5..14f5d9556 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.spec.ts +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.spec.ts @@ -62,6 +62,7 @@ describe('DescopeComponent', () => { debug: jest.fn() }; component.errorTransformer = jest.fn(); + component.onScreenUpdate = jest.fn(); component.client = {}; component.form = {}; component.storeLastAuthenticatedUser = true; diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.ts b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.ts index 7baeffa23..2ff19624c 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.ts +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/descope/descope.component.ts @@ -38,6 +38,15 @@ export class DescopeComponent implements OnInit, OnChanges { @Input() debug: boolean; @Input() errorTransformer: (error: { text: string; type: string }) => string; + @Input() onScreenUpdate: ( + screenName: string, + state: Record, + next: ( + interactionId: string, + form: Record + ) => Promise, + ref: HTMLElement + ) => boolean | Promise; @Input() client: Record; @Input() form: Record; @Input() logger: ILogger; @@ -155,6 +164,10 @@ export class DescopeComponent implements OnInit, OnChanges { this.webComponent.errorTransformer = this.errorTransformer; } + if (this.onScreenUpdate) { + this.webComponent.onScreenUpdate = this.onScreenUpdate; + } + if (this.client) { this.webComponent.setAttribute('client', JSON.stringify(this.client)); } diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.html b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.html index d2483e87f..d9de91196 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.html +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.html @@ -12,6 +12,7 @@ [restartOnError]="restartOnError" [debug]="debug" [errorTransformer]="errorTransformer" + [onScreenUpdate]="onScreenUpdate" [client]="client" [form]="form" [logger]="logger" diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.ts b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.ts index ff5eb3f81..d2bd500a1 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.ts +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.ts @@ -20,9 +20,18 @@ export class SignInFlowComponent { @Input() autoFocus: true | false | 'skipFirstScreen'; @Input() validateOnBlur: boolean; @Input() restartOnError: boolean; - + @Input() debug: boolean; @Input() errorTransformer: (error: { text: string; type: string }) => string; + @Input() onScreenUpdate: ( + screenName: string, + state: Record, + next: ( + interactionId: string, + form: Record + ) => Promise, + ref: HTMLElement + ) => boolean | Promise; @Input() client: Record; @Input() form: Record; @Input() logger: ILogger; diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.html b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.html index 31eaeb775..031235ee1 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.html +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.html @@ -12,6 +12,7 @@ [restartOnError]="restartOnError" [debug]="debug" [errorTransformer]="errorTransformer" + [onScreenUpdate]="onScreenUpdate" [client]="client" [form]="form" [logger]="logger" diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.ts b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.ts index 0dff55933..8a1c4925f 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.ts +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.ts @@ -20,9 +20,18 @@ export class SignUpFlowComponent { @Input() autoFocus: true | false | 'skipFirstScreen'; @Input() validateOnBlur: boolean; @Input() restartOnError: boolean; - + @Input() debug: boolean; @Input() errorTransformer: (error: { text: string; type: string }) => string; + @Input() onScreenUpdate: ( + screenName: string, + state: Record, + next: ( + interactionId: string, + form: Record + ) => Promise, + ref: HTMLElement + ) => boolean | Promise; @Input() client: Record; @Input() form: Record; @Input() logger: ILogger; diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.html b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.html index 14dc176e1..f34f713a5 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.html +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.html @@ -12,6 +12,7 @@ [restartOnError]="restartOnError" [debug]="debug" [errorTransformer]="errorTransformer" + [onScreenUpdate]="onScreenUpdate" [client]="client" [form]="form" [logger]="logger" diff --git a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.ts b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.ts index a15624a48..ec5b49c75 100644 --- a/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.ts +++ b/packages/sdks/angular-sdk/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.ts @@ -20,9 +20,18 @@ export class SignUpOrInFlowComponent { @Input() autoFocus: true | false | 'skipFirstScreen'; @Input() validateOnBlur: boolean; @Input() restartOnError: boolean; - + @Input() debug: boolean; @Input() errorTransformer: (error: { text: string; type: string }) => string; + @Input() onScreenUpdate: ( + screenName: string, + state: Record, + next: ( + interactionId: string, + form: Record + ) => Promise, + ref: HTMLElement + ) => boolean | Promise; @Input() client: Record; @Input() form: Record; @Input() logger: ILogger; diff --git a/packages/sdks/react-sdk/README.md b/packages/sdks/react-sdk/README.md index dc4365e9d..80af2d358 100644 --- a/packages/sdks/react-sdk/README.md +++ b/packages/sdks/react-sdk/README.md @@ -166,6 +166,75 @@ const App = () => { } ``` +### `onScreenUpdate` + +A function that is called whenever there is a new screen state and after every next call. It receives the following parameters: + +- `screenName`: The name of the screen that is about to be rendered +- `state`: An object containing the upcoming screen state +- `next`: A function that, when called, continues the flow execution +- `ref`: A reference to the descope-wc node + +The function can be sync or async, and should return a boolean indicating whether a custom screen should be rendered: + +- `true`: Render a custom screen +- `false`: Render the default flow screen + +This function allows rendering custom screens instead of the default flow screens. +It can be useful for highly customized UIs or specific logic not covered by the default screens + +To render a custom screen, its elements should be appended as children of the `Descope` component + +Usage example: + +```javascript +const CustomScreen = ({onClick, setForm}) => { + const onChange = (e) => setForm({ email: e.target.value }) + + return ( + <> + + + +)} + +const Login = () => { + const [state, setState] = useState(); + const [form, setForm] = useState(); + + const onScreenUpdate = (screenName, state, next) => { + setState({screenName, state, next}) + + if (screenName === 'My Custom Screen') { + return true; + } + + return false; + }; + + return {state.screenName === 'My Custom Screen' && { + // replace with the button interaction id + next('interactionId', form) + }} + setForm={setForm}/>} + +} + +``` + ### Use the `useDescope`, `useSession` and `useUser` hooks in your components in order to get authentication state, user details and utilities This can be helpful to implement application-specific logic. Examples: diff --git a/packages/sdks/react-sdk/src/types.ts b/packages/sdks/react-sdk/src/types.ts index 3bea03b10..23ade2de2 100644 --- a/packages/sdks/react-sdk/src/types.ts +++ b/packages/sdks/react-sdk/src/types.ts @@ -136,7 +136,7 @@ export type DescopeProps = { form: Record, ) => Promise, ref: HTMLElement, - ) => boolean; + ) => boolean | Promise; children?: React.ReactNode; }; diff --git a/packages/sdks/vue-sdk/README.md b/packages/sdks/vue-sdk/README.md index 4c6fc2740..cf56ba9c7 100644 --- a/packages/sdks/vue-sdk/README.md +++ b/packages/sdks/vue-sdk/README.md @@ -87,6 +87,39 @@ const handleReady = () => { ``` + +### `onScreenUpdate` + +A function that is called whenever there is a new screen state and after every next call. It receives the following parameters: + +- `screenName`: The name of the screen that is about to be rendered +- `state`: An object containing the upcoming screen state +- `next`: A function that, when called, continues the flow execution +- `ref`: A reference to the descope-wc node + +The function can be sync or async, and should return a boolean indicating whether a custom screen should be rendered: + +- `true`: Render a custom screen +- `false`: Render the default flow screen + +This function allows rendering custom screens instead of the default flow screens. +It can be useful for highly customized UIs or specific logic not covered by the default screens + +To render a custom screen, its elements should be appended as children of the `Descope` component + +Usage example: + +```javascript +function onScreenUpdate(screenName, state, next) { + if (screenName === 'My Custom Screen') { + return true; + } + + return false; +} +``` + + ### Use the `useDescope`, `useSession` and `useUser` functions in your components in order to get authentication state, user details and utilities This can be helpful to implement application-specific logic. Examples: diff --git a/packages/sdks/vue-sdk/example/components/Login.vue b/packages/sdks/vue-sdk/example/components/Login.vue index a9023a144..4c58131e8 100644 --- a/packages/sdks/vue-sdk/example/components/Login.vue +++ b/packages/sdks/vue-sdk/example/components/Login.vue @@ -11,9 +11,11 @@ @error="handleError" @ready="handleReady" :errorTransformer="errorTransformer" + :onScreenUpdate="onScreenUpdate" :form="form" :client="client" - /> + > + @@ -44,6 +46,12 @@ const errorTransformer = (error) => { return translationMap[error.type] || error.text; }; +const onScreenUpdate = (screenName, state, next) => { + console.log('Screen update', screenName, state, next); + + return false; +}; + const { isLoading, isAuthenticated } = useSession(); const flowId = process.env.VUE_APP_DESCOPE_FLOW_ID || 'sign-up-or-in'; const form = {}; // { email: 'myemail@domain.com' }; // found in context key: form.email diff --git a/packages/sdks/vue-sdk/src/Descope.vue b/packages/sdks/vue-sdk/src/Descope.vue index 5d8ea77d9..1304c9942 100644 --- a/packages/sdks/vue-sdk/src/Descope.vue +++ b/packages/sdks/vue-sdk/src/Descope.vue @@ -19,12 +19,15 @@ :restart-on-error="restartOnError" :store-last-authenticated-user="storeLastAuthenticatedUser" :errorTransformer.prop="errorTransformer" + :onScreenUpdate.prop="onScreenUpdate" :form.attr="formStr" :client.attr="clientStr" @success="onSuccess" @error="onError" @ready="onReady" - /> + > + + @@ -95,6 +98,9 @@ const props = defineProps({ errorTransformer: { type: Function, }, + onScreenUpdate: { + type: Function, + }, form: { type: Object, }, diff --git a/packages/sdks/vue-sdk/tests/Descope.test.ts b/packages/sdks/vue-sdk/tests/Descope.test.ts index 153824011..86822fb57 100644 --- a/packages/sdks/vue-sdk/tests/Descope.test.ts +++ b/packages/sdks/vue-sdk/tests/Descope.test.ts @@ -23,6 +23,7 @@ describe('Descope.vue', () => { const errorTransformer = (error: { text: string; type: string }) => { return error.text || error.type; }; + const onScreenUpdate = () => false; const wrapper = mount(Descope, { props: { flowId: 'test-flow-id', @@ -34,6 +35,7 @@ describe('Descope.vue', () => { redirectUrl: 'test-redirect-url', autoFocus: true, errorTransformer, + onScreenUpdate, form: { test: 'a' }, client: { test: 'b' }, styleId: 'test-style-id', @@ -53,6 +55,7 @@ describe('Descope.vue', () => { expect(descopeWc.attributes('redirect-url')).toBe('test-redirect-url'); expect(descopeWc.attributes('auto-focus')).toBe('true'); expect(wrapper.vm.errorTransformer).toBe(errorTransformer); + expect(wrapper.vm.onScreenUpdate).toBe(onScreenUpdate); expect(descopeWc.attributes('form')).toBe('{"test":"a"}'); expect(wrapper.vm.client).toStrictEqual({ test: 'b' }); expect(descopeWc.attributes('style-id')).toBe('test-style-id'); @@ -61,14 +64,15 @@ describe('Descope.vue', () => { it('renders a DescopeWc component with empty props', () => { const wrapper = mount(Descope, { props: { - form: null, - client: null, + flowId: 'test-flow-id', + form: {}, + client: {}, }, }); const descopeWc = wrapper.find('descope-wc'); - expect(descopeWc.attributes('form')).toBe(''); - expect(wrapper.vm.client).toBeNull(); + expect(descopeWc.attributes('form')).toEqual('{}'); + expect(wrapper.vm.client).toEqual({}); }); it('init sdk config', async () => { diff --git a/packages/sdks/web-component/README.md b/packages/sdks/web-component/README.md index cceef762b..ac73709ef 100644 --- a/packages/sdks/web-component/README.md +++ b/packages/sdks/web-component/README.md @@ -133,14 +133,14 @@ descopeWcEle.logger = logger; ### `onScreenUpdate` -A function that is called whenever there is a new screen state or after every next call. It receives the following parameters: +A function that is called whenever there is a new screen state and after every next call. It receives the following parameters: - `screenName`: The name of the screen that is about to be rendered - `state`: An object containing the upcoming screen state - `next`: A function that, when called, continues the flow execution - `ref`: A reference to the descope-wc node -The function should return a boolean indicating whether a custom screen should be rendered: +The function can be sync or async, and should return a boolean indicating whether a custom screen should be rendered: - `true`: Render a custom screen - `false`: Render the default flow screen @@ -148,6 +148,8 @@ The function should return a boolean indicating whether a custom screen should b This function allows rendering custom screens instead of the default flow screens. It can be useful for highly customized UIs or specific logic not covered by the default screens +To render a custom screen, its elements should be appended as children of the `descope-wc` component + Usage example: ```javascript diff --git a/packages/sdks/web-component/src/lib/descope-wc/DescopeWc.ts b/packages/sdks/web-component/src/lib/descope-wc/DescopeWc.ts index 4dd2f5389..3a3660b32 100644 --- a/packages/sdks/web-component/src/lib/descope-wc/DescopeWc.ts +++ b/packages/sdks/web-component/src/lib/descope-wc/DescopeWc.ts @@ -86,7 +86,7 @@ class DescopeWc extends BaseDescopeWc { screenState: CustomScreenState, next: StepState['next'], ref: typeof this, - ) => Promise; + ) => boolean | Promise; constructor() { const flowState = new State({