Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .changeset/stale-gifts-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Handle unsafeMetadata in transfer flows
62 changes: 61 additions & 1 deletion packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/types/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/SignIn/SignInStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ function SignInStartInternal(): JSX.Element {
attribute,
identifierField.value,
),
unsafeMetadata: ctx.unsafeMetadata,
});
} else {
handleError(e, [identifierField, instantPasswordField], card.setError);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]',
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: '[email protected]',
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: '[email protected]',
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: '[email protected]',
unsafeMetadata: undefined,
});
});

it('should call completeSignUpFlow with phone number if phone number is optional field.', async () => {
const mockClerk = {
client: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type HandleCombinedFlowTransferProps = {
passwordEnabled: boolean;
alternativePhoneCodeChannel?: PhoneCodeChannel | null;
navigateOnSetActive: (opts: { session: SessionResource; redirectUrl: string }) => Promise<unknown>;
unsafeMetadata?: SignUpUnsafeMetadata;
};

/**
Expand All @@ -45,6 +46,7 @@ export function handleCombinedFlowTransfer({
passwordEnabled,
navigateOnSetActive,
alternativePhoneCodeChannel,
unsafeMetadata,
}: HandleCombinedFlowTransferProps): Promise<unknown> | void {
if (signUpMode === SIGN_UP_MODES.WAITLIST) {
const waitlistUrl = clerk.buildWaitlistUrl(
Expand Down Expand Up @@ -85,6 +87,7 @@ export function handleCombinedFlowTransfer({
.create({
[identifierAttribute]: identifierValue,
...alternativePhoneCodeChannelParams,
unsafeMetadata,
})
.then(async res => {
const completeSignUpFlow = await lazyCompleteSignUpFlow();
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/components/SignIn/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function SignInRoutes(): JSX.Element {
firstFactorUrl={'../factor-one'}
secondFactorUrl={'../factor-two'}
resetPasswordUrl={'../reset-password'}
unsafeMetadata={signInContext.unsafeMetadata}
/>
</Route>
<Route path='choose'>
Expand Down Expand Up @@ -117,6 +118,7 @@ function SignInRoutes(): JSX.Element {
continueSignUpUrl='../continue'
verifyEmailAddressUrl='../verify-email-address'
verifyPhoneNumberUrl='../verify-phone-number'
unsafeMetadata={signUpContext.unsafeMetadata}
/>
</Route>
<Route path='verify'>
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/SignUp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function SignUpRoutes(): JSX.Element {
continueSignUpUrl='../continue'
verifyEmailAddressUrl='../verify-email-address'
verifyPhoneNumberUrl='../verify-phone-number'
unsafeMetadata={signUpContext.unsafeMetadata}
/>
</Route>
<Route path='verify'>
Expand Down
Loading