From 427fcdeaaba4e77273be29b4d7cca43f9aa18693 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Wed, 3 Jul 2024 17:47:46 +0300 Subject: [PATCH] fix(clerk-react): Fix race condition on updating ClerkProvider props (#3655) Fixes issues with updating `ClerkProvider` props before ClerkJS has loaded. Sometimes, `clerk.__unstable__updateProps` was called before clerkjs had a value, so the props update did not happen. With this change the `__unstable__updateProps` waits for ClerkJS to load and then calls the `clerk.__unstable__updateProps` function to update the props. This change also implements `buildAfterMultiSessionSingleSignOutUrl` in `isomorphicClerk` to resolve typing issues. --- .changeset/lemon-bobcats-smile.md | 5 ++ .../src/__tests__/isomorphicClerk.test.ts | 52 +++++++++++++++++++ .../src/contexts/ClerkContextProvider.tsx | 4 +- packages/react/src/isomorphicClerk.ts | 24 ++++++--- 4 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 .changeset/lemon-bobcats-smile.md create mode 100644 packages/react/src/__tests__/isomorphicClerk.test.ts diff --git a/.changeset/lemon-bobcats-smile.md b/.changeset/lemon-bobcats-smile.md new file mode 100644 index 0000000000..3073d34375 --- /dev/null +++ b/.changeset/lemon-bobcats-smile.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-react": patch +--- + +Fix race condition on updating ClerkProvider props before ClerkJS has loaded diff --git a/packages/react/src/__tests__/isomorphicClerk.test.ts b/packages/react/src/__tests__/isomorphicClerk.test.ts new file mode 100644 index 0000000000..0b4618ebbb --- /dev/null +++ b/packages/react/src/__tests__/isomorphicClerk.test.ts @@ -0,0 +1,52 @@ +import { IsomorphicClerk } from '../isomorphicClerk'; + +describe('isomorphicClerk', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('instantiates a IsomorphicClerk instance', () => { + expect(() => { + new IsomorphicClerk({ publishableKey: 'pk_test_XXX' }); + }).not.toThrow(); + }); + + it('updates props asynchronously after clerkjs has loaded', async () => { + const propsHistory: any[] = []; + const dummyClerkJS = { + __unstable__updateProps: (props: any) => propsHistory.push(props), + }; + + const isomorphicClerk = new IsomorphicClerk({ publishableKey: 'pk_test_XXX' }); + (isomorphicClerk as any).clerkjs = dummyClerkJS as any; + + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'dark' } }); + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'light' } }); + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'purple' } }); + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'yellow' } }); + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'red' } }); + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'blue' } }); + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'green' } }); + expect(propsHistory).toEqual([]); + + jest.spyOn(isomorphicClerk, 'loaded', 'get').mockReturnValue(true); + isomorphicClerk.emitLoaded(); + void isomorphicClerk.__unstable__updateProps({ appearance: { baseTheme: 'white' } }); + await jest.runAllTimersAsync(); + + expect(propsHistory).toEqual([ + { appearance: { baseTheme: 'dark' } }, + { appearance: { baseTheme: 'light' } }, + { appearance: { baseTheme: 'purple' } }, + { appearance: { baseTheme: 'yellow' } }, + { appearance: { baseTheme: 'red' } }, + { appearance: { baseTheme: 'blue' } }, + { appearance: { baseTheme: 'green' } }, + { appearance: { baseTheme: 'white' } }, + ]); + }); +}); diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index e64645cbf8..5a5f46ad61 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -72,11 +72,11 @@ const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => { const isomorphicClerk = React.useMemo(() => IsomorphicClerk.getOrCreateInstance(options), []); React.useEffect(() => { - isomorphicClerk.__unstable__updateProps({ appearance: options.appearance }); + void isomorphicClerk.__unstable__updateProps({ appearance: options.appearance }); }, [options.appearance]); React.useEffect(() => { - isomorphicClerk.__unstable__updateProps({ options }); + void isomorphicClerk.__unstable__updateProps({ options }); }, [options.localization]); React.useEffect(() => { diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 82e66cc491..a8ab13a6c5 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -84,6 +84,7 @@ type IsomorphicLoadedClerk = Without< | 'buildAfterSignUpUrl' | 'buildAfterSignInUrl' | 'buildAfterSignOutUrl' + | 'buildAfterMultiSessionSingleSignOutUrl' | 'buildUrlWithAuth' | 'handleRedirectCallback' | 'handleGoogleOneTapCallback' @@ -132,6 +133,8 @@ type IsomorphicLoadedClerk = Without< buildAfterSignUpUrl: () => string | void; // TODO: Align return type buildAfterSignOutUrl: () => string | void; + // TODO: Align return type + buildAfterMultiSessionSingleSignOutUrl: () => string | void; // TODO: Align optional props mountUserButton: (node: HTMLDivElement, props: UserButtonProps) => void; mountOrganizationList: (node: HTMLDivElement, props: OrganizationListProps) => void; @@ -309,6 +312,15 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + buildAfterMultiSessionSingleSignOutUrl = (): string | void => { + const callback = () => this.clerkjs?.buildAfterMultiSessionSingleSignOutUrl() || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildAfterMultiSessionSingleSignOutUrl', callback); + } + }; + buildUserProfileUrl = (): string | void => { const callback = () => this.clerkjs?.buildUserProfileUrl() || ''; if (this.clerkjs && this.#loaded) { @@ -356,9 +368,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { #waitForClerkJS(): Promise { return new Promise(resolve => { - if (this.#loaded) { - resolve(this.clerkjs!); - } this.addOnLoaded(() => resolve(this.clerkjs!)); }); } @@ -579,12 +588,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } - __unstable__updateProps = (props: any): any => { + __unstable__updateProps = async (props: any): Promise => { + const clerkjs = await this.#waitForClerkJS(); // Handle case where accounts has clerk-react@4 installed, but clerk-js@3 is manually loaded - if (this.clerkjs && '__unstable__updateProps' in this.clerkjs) { - (this.clerkjs as any).__unstable__updateProps(props); - } else { - return undefined; + if (clerkjs && '__unstable__updateProps' in clerkjs) { + return (clerkjs as any).__unstable__updateProps(props); } };