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
8 changes: 8 additions & 0 deletions .changeset/signup-future-password-browser-locale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/clerk-js': patch
---

Fix the future SignUp API dropping documented params in some flows:

- `signUp.password()` and `signUp.sso()` now default the sign-up's `locale` to the browser locale when they create a new sign-up, matching the documented behavior and `signUp.create()`. An explicitly passed `locale` still takes precedence, and updates to an existing sign-up remain unaffected.
- `signUp.web3()` now forwards the `firstName`, `lastName`, and `locale` params to the created sign-up instead of silently ignoring them.
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "549KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "70KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "71KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "112KB" },
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "316KB" },
{ "path": "./dist/clerk.native.js", "maxSize": "70KB" },
Expand Down
12 changes: 10 additions & 2 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,9 @@ class SignUpFuture implements SignUpFutureResource {
if (this.#resource.id) {
await this.#resource.__internal_basePatch({ body });
} else {
// Inject browser locale only when creating the sign-up, so an existing
// sign-up's locale is not overwritten on update.
body.locale = params.locale ?? getBrowserLocale();
await this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body });
}
});
Expand Down Expand Up @@ -1001,6 +1004,7 @@ class SignUpFuture implements SignUpFutureResource {
enterpriseConnectionId,
emailAddress,
popup,
locale,
} = params;
return runAsyncResourceTask(this.#resource, async () => {
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken({ strategy });
Expand Down Expand Up @@ -1037,10 +1041,14 @@ class SignUpFuture implements SignUpFutureResource {
captchaToken,
captchaWidgetType,
captchaError,
locale,
};
if (this.#resource.id) {
return this.#resource.__internal_basePatch({ body });
}
// Inject browser locale only when creating the sign-up, so an existing
// sign-up's locale is not overwritten on update.
body.locale = locale ?? getBrowserLocale();
return this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body });
};

Expand Down Expand Up @@ -1068,7 +1076,7 @@ class SignUpFuture implements SignUpFutureResource {
}

async web3(params: SignUpFutureWeb3Params): Promise<{ error: ClerkError | null }> {
const { strategy, unsafeMetadata, legalAccepted } = params;
const { strategy, unsafeMetadata, legalAccepted, firstName, lastName, locale } = params;
const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider;

return runAsyncResourceTask(this.#resource, async () => {
Expand Down Expand Up @@ -1097,7 +1105,7 @@ class SignUpFuture implements SignUpFutureResource {

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const web3Wallet = identifier || this.#resource.web3wallet!;
await this._create({ web3Wallet, unsafeMetadata, legalAccepted });
await this._create({ web3Wallet, unsafeMetadata, legalAccepted, firstName, lastName, locale });
await this.#resource.__internal_basePost({
body: { strategy },
action: 'prepare_verification',
Expand Down
269 changes: 269 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,147 @@ describe('SignUp', () => {
);
});

it('includes browser locale when creating a new signup', async () => {
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
vi.stubGlobal('navigator', { language: 'fr-FR' });

const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
SignUp.clerk = {
buildUrlWithAuth: mockBuildUrlWithAuth,
__internal_environment: {
displayConfig: {
captchaOauthBypass: [],
},
},
} as any;

const mockFetch = vi.fn().mockResolvedValue({
client: null,
response: {
id: 'signup_123',
verifications: {
externalAccount: {
status: 'unverified',
externalVerificationRedirectURL: 'https://sso.example.com/auth',
},
},
},
});
BaseResource._fetch = mockFetch;

const signUp = new SignUp();
await signUp.__internal_future.sso({
strategy: 'oauth_google',
redirectUrl: '/complete',
redirectCallbackUrl: '/sso-callback',
});

expect(mockFetch).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
path: '/client/sign_ups',
body: expect.objectContaining({
strategy: 'oauth_google',
locale: 'fr-FR',
}),
}),
);
});

it('prefers an explicitly provided locale over the browser locale', async () => {
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
vi.stubGlobal('navigator', { language: 'fr-FR' });

const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
SignUp.clerk = {
buildUrlWithAuth: mockBuildUrlWithAuth,
__internal_environment: {
displayConfig: {
captchaOauthBypass: [],
},
},
} as any;

const mockFetch = vi.fn().mockResolvedValue({
client: null,
response: {
id: 'signup_123',
verifications: {
externalAccount: {
status: 'unverified',
externalVerificationRedirectURL: 'https://sso.example.com/auth',
},
},
},
});
BaseResource._fetch = mockFetch;

const signUp = new SignUp();
await signUp.__internal_future.sso({
strategy: 'oauth_google',
redirectUrl: '/complete',
redirectCallbackUrl: '/sso-callback',
locale: 'el-GR',
});

expect(mockFetch).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
path: '/client/sign_ups',
body: expect.objectContaining({
strategy: 'oauth_google',
locale: 'el-GR',
}),
}),
);
});

it('does not inject browser locale when continuing an existing signup', async () => {
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
vi.stubGlobal('navigator', { language: 'fr-FR' });

const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
SignUp.clerk = {
buildUrlWithAuth: mockBuildUrlWithAuth,
__internal_environment: {
displayConfig: {
captchaOauthBypass: [],
},
},
} as any;

const mockFetch = vi.fn().mockResolvedValue({
client: null,
response: {
id: 'signup_123',
verifications: {
externalAccount: {
status: 'unverified',
externalVerificationRedirectURL: 'https://sso.example.com/auth',
},
},
},
});
BaseResource._fetch = mockFetch;

const signUp = new SignUp({ id: 'signup_123' } as any);
await signUp.__internal_future.sso({
strategy: 'oauth_google',
redirectUrl: '/complete',
redirectCallbackUrl: '/sso-callback',
});

expect(mockFetch).toHaveBeenCalledWith(
expect.objectContaining({
method: 'PATCH',
path: '/client/sign_ups/signup_123',
body: expect.not.objectContaining({
locale: expect.anything(),
}),
}),
);
});

it('continues an existing sign up via the resource URL', async () => {
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });

Expand Down Expand Up @@ -1180,6 +1321,63 @@ describe('SignUp', () => {
// Verify error is returned without retry
expect(result.error).toBeTruthy();
});

it('passes locale and name params through to the created signup', async () => {
vi.stubGlobal('navigator', { language: 'fr-FR' });

const mockFetch = vi
.fn()
.mockResolvedValueOnce({
client: null,
response: {
id: 'signup_123',
verifications: {
web3_wallet: { status: 'unverified' },
},
},
})
.mockResolvedValueOnce({
client: null,
response: {
id: 'signup_123',
verifications: {
web3_wallet: { status: 'unverified', message: 'nonce_123' },
},
},
})
.mockResolvedValueOnce({
client: null,
response: { id: 'signup_123', status: 'complete' },
});
BaseResource._fetch = mockFetch;

const utilsModule = await import('../../../utils');
vi.spyOn(utilsModule, 'web3').mockReturnValue({
getMetamaskIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'),
generateSignatureWithMetamask: vi.fn().mockResolvedValue('signature_123'),
} as any);

const signUp = new SignUp();
await signUp.__internal_future.web3({
strategy: 'web3_metamask_signature',
firstName: 'Vitalik',
lastName: 'Nakamoto',
locale: 'el-GR',
});

expect(mockFetch).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
method: 'POST',
path: '/client/sign_ups',
body: expect.objectContaining({
firstName: 'Vitalik',
lastName: 'Nakamoto',
locale: 'el-GR',
}),
}),
);
});
});

describe('password', () => {
Expand Down Expand Up @@ -1255,6 +1453,77 @@ describe('SignUp', () => {

expect(result).toHaveProperty('error', null);
});

it('includes browser locale when creating a new signup', async () => {
vi.stubGlobal('navigator', { language: 'fr-FR' });

const mockFetch = vi.fn().mockResolvedValue({
client: null,
response: { id: 'signup_123', status: 'missing_requirements' },
});
BaseResource._fetch = mockFetch;

const signUp = new SignUp();
await signUp.__internal_future.password({ password: 'test-password-123' });

expect(mockFetch).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
path: '/client/sign_ups',
body: expect.objectContaining({
strategy: 'password',
locale: 'fr-FR',
}),
}),
);
});

it('prefers an explicitly provided locale over the browser locale', async () => {
vi.stubGlobal('navigator', { language: 'fr-FR' });

const mockFetch = vi.fn().mockResolvedValue({
client: null,
response: { id: 'signup_123', status: 'missing_requirements' },
});
BaseResource._fetch = mockFetch;

const signUp = new SignUp();
await signUp.__internal_future.password({ password: 'test-password-123', locale: 'el-GR' });

expect(mockFetch).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
path: '/client/sign_ups',
body: expect.objectContaining({
strategy: 'password',
locale: 'el-GR',
}),
}),
);
});

it('does not inject browser locale when updating an existing signup', async () => {
vi.stubGlobal('navigator', { language: 'fr-FR' });

const mockFetch = vi.fn().mockResolvedValue({
client: null,
response: { id: 'signup_123', status: 'missing_requirements' },
});
BaseResource._fetch = mockFetch;

const signUp = new SignUp({ id: 'signup_123' } as any);
await signUp.__internal_future.password({ password: 'test-password-123' });

expect(mockFetch).toHaveBeenCalledWith(
expect.objectContaining({
method: 'PATCH',
path: '/client/sign_ups/signup_123',
body: expect.not.objectContaining({
locale: expect.anything(),
}),
}),
);
});
});

describe('ticket', () => {
Expand Down
Loading