diff --git a/.changeset/stale-gifts-jog.md b/.changeset/stale-gifts-jog.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/stale-gifts-jog.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index de0562da1bf..c30515ee889 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -992,7 +992,67 @@ describe('Clerk singleton', () => { await sut.handleRedirectCallback(); await waitFor(() => { - expect(mockSignUpCreate).toHaveBeenCalledWith({ transfer: true }); + expect(mockSignUpCreate).toHaveBeenCalledWith({ transfer: true, unsafeMetadata: undefined }); + expect(mockSetActive).toHaveBeenCalled(); + }); + }); + + it('passes unsafeMetadata to signUp.create during OAuth transfer flow', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + onWindowLocationHost: () => false, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [], + signIn: new SignIn({ + status: 'needs_identifier', + first_factor_verification: { + status: 'transferable', + strategy: 'oauth_google', + external_verification_redirect_url: '', + error: { + code: 'external_account_not_found', + long_message: 'The External Account was not found.', + message: 'Invalid external account', + }, + }, + second_factor_verification: null, + identifier: '', + user_data: null, + created_session_id: null, + created_user_id: null, + } as any as SignInJSON), + signUp: new SignUp(null), + }), + ); + + const mockSetActive = vi.fn(); + const mockSignUpCreate = vi + .fn() + .mockReturnValue(Promise.resolve({ status: 'complete', createdSessionId: '123' })); + + const sut = new Clerk(productionPublishableKey); + await sut.load(mockedLoadOptions); + if (!sut.client) { + fail('we should always have a client'); + } + sut.client.signUp.create = mockSignUpCreate; + sut.setActive = mockSetActive; + + const unsafeMetadata = { foo: 'bar', nested: { value: 123 } }; + await sut.handleRedirectCallback({ unsafeMetadata }); + + await waitFor(() => { + expect(mockSignUpCreate).toHaveBeenCalledWith({ transfer: true, unsafeMetadata }); expect(mockSetActive).toHaveBeenCalled(); }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 38a8e411478..51aeaa00928 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2244,7 +2244,7 @@ export class Clerk implements ClerkInterface { return navigateToSignIn(); } - const res = await signUp.create({ transfer: true }); + const res = await signUp.create({ transfer: true, unsafeMetadata: params.unsafeMetadata }); switch (res.status) { case 'complete': return this.setActive({ diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 314d186793b..a3dabddb937 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -1031,6 +1031,10 @@ export type HandleOAuthCallbackParams = TransferableOption & * The underlying resource to optionally reload before processing an OAuth callback. */ reloadResource?: 'signIn' | 'signUp'; + /** + * Additional arbitrary metadata to be stored alongside the User object when a sign-up transfer occurs. + */ + unsafeMetadata?: SignUpUnsafeMetadata; }; export type HandleSamlCallbackParams = HandleOAuthCallbackParams; diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index fd27013ed4e..92208909267 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -505,6 +505,7 @@ function SignInStartInternal(): JSX.Element { attribute, identifierField.value, ), + unsafeMetadata: ctx.unsafeMetadata, }); } else { handleError(e, [identifierField, instantPasswordField], card.setError); diff --git a/packages/ui/src/components/SignIn/__tests__/handleCombinedFlowTransfer.test.ts b/packages/ui/src/components/SignIn/__tests__/handleCombinedFlowTransfer.test.ts index 26e8cc5e819..c14adc916f7 100644 --- a/packages/ui/src/components/SignIn/__tests__/handleCombinedFlowTransfer.test.ts +++ b/packages/ui/src/components/SignIn/__tests__/handleCombinedFlowTransfer.test.ts @@ -47,6 +47,67 @@ describe('handleCombinedFlowTransfer', () => { expect(mockCompleteSignUpFlow).toHaveBeenCalled(); }); + it('should pass unsafeMetadata to signUp.create', async () => { + const mockCreate = vi.fn().mockResolvedValue({}); + const mockClerk = { + client: { + signUp: { + create: mockCreate, + optionalFields: [], + }, + }, + }; + + const unsafeMetadata = { foo: 'bar', nested: { value: 123 } }; + + await handleCombinedFlowTransfer({ + identifierAttribute: 'emailAddress', + identifierValue: 'test@test.com', + signUpMode: 'public', + navigate: mockNavigate, + handleError: mockHandleError, + clerk: mockClerk as unknown as LoadedClerk, + afterSignUpUrl: 'https://test.com', + passwordEnabled: false, + navigateOnSetActive: vi.fn(), + unsafeMetadata, + }); + + expect(mockCreate).toHaveBeenCalledWith({ + emailAddress: 'test@test.com', + unsafeMetadata, + }); + }); + + it('should pass undefined unsafeMetadata when not provided', async () => { + const mockCreate = vi.fn().mockResolvedValue({}); + const mockClerk = { + client: { + signUp: { + create: mockCreate, + optionalFields: [], + }, + }, + }; + + await handleCombinedFlowTransfer({ + identifierAttribute: 'emailAddress', + identifierValue: 'test@test.com', + signUpMode: 'public', + navigate: mockNavigate, + handleError: mockHandleError, + clerk: mockClerk as unknown as LoadedClerk, + afterSignUpUrl: 'https://test.com', + passwordEnabled: false, + navigateOnSetActive: vi.fn(), + }); + + expect(mockCreate).toHaveBeenCalledWith({ + emailAddress: 'test@test.com', + unsafeMetadata: undefined, + }); + }); + it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => { const mockClerk = { client: { diff --git a/packages/ui/src/components/SignIn/handleCombinedFlowTransfer.ts b/packages/ui/src/components/SignIn/handleCombinedFlowTransfer.ts index 5ed1926acb8..7ae2713cc55 100644 --- a/packages/ui/src/components/SignIn/handleCombinedFlowTransfer.ts +++ b/packages/ui/src/components/SignIn/handleCombinedFlowTransfer.ts @@ -25,6 +25,7 @@ type HandleCombinedFlowTransferProps = { passwordEnabled: boolean; alternativePhoneCodeChannel?: PhoneCodeChannel | null; navigateOnSetActive: (opts: { session: SessionResource; redirectUrl: string }) => Promise; + unsafeMetadata?: SignUpUnsafeMetadata; }; /** @@ -45,6 +46,7 @@ export function handleCombinedFlowTransfer({ passwordEnabled, navigateOnSetActive, alternativePhoneCodeChannel, + unsafeMetadata, }: HandleCombinedFlowTransferProps): Promise | void { if (signUpMode === SIGN_UP_MODES.WAITLIST) { const waitlistUrl = clerk.buildWaitlistUrl( @@ -85,6 +87,7 @@ export function handleCombinedFlowTransfer({ .create({ [identifierAttribute]: identifierValue, ...alternativePhoneCodeChannelParams, + unsafeMetadata, }) .then(async res => { const completeSignUpFlow = await lazyCompleteSignUpFlow(); diff --git a/packages/ui/src/components/SignIn/index.tsx b/packages/ui/src/components/SignIn/index.tsx index 8e8a6df14bb..f977eb229f9 100644 --- a/packages/ui/src/components/SignIn/index.tsx +++ b/packages/ui/src/components/SignIn/index.tsx @@ -78,6 +78,7 @@ function SignInRoutes(): JSX.Element { firstFactorUrl={'../factor-one'} secondFactorUrl={'../factor-two'} resetPasswordUrl={'../reset-password'} + unsafeMetadata={signInContext.unsafeMetadata} /> @@ -117,6 +118,7 @@ function SignInRoutes(): JSX.Element { continueSignUpUrl='../continue' verifyEmailAddressUrl='../verify-email-address' verifyPhoneNumberUrl='../verify-phone-number' + unsafeMetadata={signUpContext.unsafeMetadata} /> diff --git a/packages/ui/src/components/SignUp/index.tsx b/packages/ui/src/components/SignUp/index.tsx index 841159658ff..b4028235421 100644 --- a/packages/ui/src/components/SignUp/index.tsx +++ b/packages/ui/src/components/SignUp/index.tsx @@ -56,6 +56,7 @@ function SignUpRoutes(): JSX.Element { continueSignUpUrl='../continue' verifyEmailAddressUrl='../verify-email-address' verifyPhoneNumberUrl='../verify-phone-number' + unsafeMetadata={signUpContext.unsafeMetadata} />