Skip to content

Commit eb5c02d

Browse files
dmoernerclaude
andauthored
fix(clerk-js): Correct locale inconsistencies when creating SignUpFuture (#8762)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 83f50f6 commit eb5c02d

4 files changed

Lines changed: 288 additions & 3 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Fix the future SignUp API dropping documented params in some flows:
6+
7+
- `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.
8+
- `signUp.web3()` now forwards the `firstName`, `lastName`, and `locale` params to the created sign-up instead of silently ignoring them.

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"files": [
33
{ "path": "./dist/clerk.js", "maxSize": "549KB" },
4-
{ "path": "./dist/clerk.browser.js", "maxSize": "70KB" },
4+
{ "path": "./dist/clerk.browser.js", "maxSize": "71KB" },
55
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "112KB" },
66
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "316KB" },
77
{ "path": "./dist/clerk.native.js", "maxSize": "70KB" },

packages/clerk-js/src/core/resources/SignUp.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,9 @@ class SignUpFuture implements SignUpFutureResource {
906906
if (this.#resource.id) {
907907
await this.#resource.__internal_basePatch({ body });
908908
} else {
909+
// Inject browser locale only when creating the sign-up, so an existing
910+
// sign-up's locale is not overwritten on update.
911+
body.locale = params.locale ?? getBrowserLocale();
909912
await this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body });
910913
}
911914
});
@@ -1001,6 +1004,7 @@ class SignUpFuture implements SignUpFutureResource {
10011004
enterpriseConnectionId,
10021005
emailAddress,
10031006
popup,
1007+
locale,
10041008
} = params;
10051009
return runAsyncResourceTask(this.#resource, async () => {
10061010
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken({ strategy });
@@ -1037,10 +1041,14 @@ class SignUpFuture implements SignUpFutureResource {
10371041
captchaToken,
10381042
captchaWidgetType,
10391043
captchaError,
1044+
locale,
10401045
};
10411046
if (this.#resource.id) {
10421047
return this.#resource.__internal_basePatch({ body });
10431048
}
1049+
// Inject browser locale only when creating the sign-up, so an existing
1050+
// sign-up's locale is not overwritten on update.
1051+
body.locale = locale ?? getBrowserLocale();
10441052
return this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body });
10451053
};
10461054

@@ -1068,7 +1076,7 @@ class SignUpFuture implements SignUpFutureResource {
10681076
}
10691077

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

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

10981106
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
10991107
const web3Wallet = identifier || this.#resource.web3wallet!;
1100-
await this._create({ web3Wallet, unsafeMetadata, legalAccepted });
1108+
await this._create({ web3Wallet, unsafeMetadata, legalAccepted, firstName, lastName, locale });
11011109
await this.#resource.__internal_basePost({
11021110
body: { strategy },
11031111
action: 'prepare_verification',

packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,147 @@ describe('SignUp', () => {
698698
);
699699
});
700700

701+
it('includes browser locale when creating a new signup', async () => {
702+
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
703+
vi.stubGlobal('navigator', { language: 'fr-FR' });
704+
705+
const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
706+
SignUp.clerk = {
707+
buildUrlWithAuth: mockBuildUrlWithAuth,
708+
__internal_environment: {
709+
displayConfig: {
710+
captchaOauthBypass: [],
711+
},
712+
},
713+
} as any;
714+
715+
const mockFetch = vi.fn().mockResolvedValue({
716+
client: null,
717+
response: {
718+
id: 'signup_123',
719+
verifications: {
720+
externalAccount: {
721+
status: 'unverified',
722+
externalVerificationRedirectURL: 'https://sso.example.com/auth',
723+
},
724+
},
725+
},
726+
});
727+
BaseResource._fetch = mockFetch;
728+
729+
const signUp = new SignUp();
730+
await signUp.__internal_future.sso({
731+
strategy: 'oauth_google',
732+
redirectUrl: '/complete',
733+
redirectCallbackUrl: '/sso-callback',
734+
});
735+
736+
expect(mockFetch).toHaveBeenCalledWith(
737+
expect.objectContaining({
738+
method: 'POST',
739+
path: '/client/sign_ups',
740+
body: expect.objectContaining({
741+
strategy: 'oauth_google',
742+
locale: 'fr-FR',
743+
}),
744+
}),
745+
);
746+
});
747+
748+
it('prefers an explicitly provided locale over the browser locale', async () => {
749+
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
750+
vi.stubGlobal('navigator', { language: 'fr-FR' });
751+
752+
const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
753+
SignUp.clerk = {
754+
buildUrlWithAuth: mockBuildUrlWithAuth,
755+
__internal_environment: {
756+
displayConfig: {
757+
captchaOauthBypass: [],
758+
},
759+
},
760+
} as any;
761+
762+
const mockFetch = vi.fn().mockResolvedValue({
763+
client: null,
764+
response: {
765+
id: 'signup_123',
766+
verifications: {
767+
externalAccount: {
768+
status: 'unverified',
769+
externalVerificationRedirectURL: 'https://sso.example.com/auth',
770+
},
771+
},
772+
},
773+
});
774+
BaseResource._fetch = mockFetch;
775+
776+
const signUp = new SignUp();
777+
await signUp.__internal_future.sso({
778+
strategy: 'oauth_google',
779+
redirectUrl: '/complete',
780+
redirectCallbackUrl: '/sso-callback',
781+
locale: 'el-GR',
782+
});
783+
784+
expect(mockFetch).toHaveBeenCalledWith(
785+
expect.objectContaining({
786+
method: 'POST',
787+
path: '/client/sign_ups',
788+
body: expect.objectContaining({
789+
strategy: 'oauth_google',
790+
locale: 'el-GR',
791+
}),
792+
}),
793+
);
794+
});
795+
796+
it('does not inject browser locale when continuing an existing signup', async () => {
797+
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
798+
vi.stubGlobal('navigator', { language: 'fr-FR' });
799+
800+
const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
801+
SignUp.clerk = {
802+
buildUrlWithAuth: mockBuildUrlWithAuth,
803+
__internal_environment: {
804+
displayConfig: {
805+
captchaOauthBypass: [],
806+
},
807+
},
808+
} as any;
809+
810+
const mockFetch = vi.fn().mockResolvedValue({
811+
client: null,
812+
response: {
813+
id: 'signup_123',
814+
verifications: {
815+
externalAccount: {
816+
status: 'unverified',
817+
externalVerificationRedirectURL: 'https://sso.example.com/auth',
818+
},
819+
},
820+
},
821+
});
822+
BaseResource._fetch = mockFetch;
823+
824+
const signUp = new SignUp({ id: 'signup_123' } as any);
825+
await signUp.__internal_future.sso({
826+
strategy: 'oauth_google',
827+
redirectUrl: '/complete',
828+
redirectCallbackUrl: '/sso-callback',
829+
});
830+
831+
expect(mockFetch).toHaveBeenCalledWith(
832+
expect.objectContaining({
833+
method: 'PATCH',
834+
path: '/client/sign_ups/signup_123',
835+
body: expect.not.objectContaining({
836+
locale: expect.anything(),
837+
}),
838+
}),
839+
);
840+
});
841+
701842
it('continues an existing sign up via the resource URL', async () => {
702843
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
703844

@@ -1180,6 +1321,63 @@ describe('SignUp', () => {
11801321
// Verify error is returned without retry
11811322
expect(result.error).toBeTruthy();
11821323
});
1324+
1325+
it('passes locale and name params through to the created signup', async () => {
1326+
vi.stubGlobal('navigator', { language: 'fr-FR' });
1327+
1328+
const mockFetch = vi
1329+
.fn()
1330+
.mockResolvedValueOnce({
1331+
client: null,
1332+
response: {
1333+
id: 'signup_123',
1334+
verifications: {
1335+
web3_wallet: { status: 'unverified' },
1336+
},
1337+
},
1338+
})
1339+
.mockResolvedValueOnce({
1340+
client: null,
1341+
response: {
1342+
id: 'signup_123',
1343+
verifications: {
1344+
web3_wallet: { status: 'unverified', message: 'nonce_123' },
1345+
},
1346+
},
1347+
})
1348+
.mockResolvedValueOnce({
1349+
client: null,
1350+
response: { id: 'signup_123', status: 'complete' },
1351+
});
1352+
BaseResource._fetch = mockFetch;
1353+
1354+
const utilsModule = await import('../../../utils');
1355+
vi.spyOn(utilsModule, 'web3').mockReturnValue({
1356+
getMetamaskIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'),
1357+
generateSignatureWithMetamask: vi.fn().mockResolvedValue('signature_123'),
1358+
} as any);
1359+
1360+
const signUp = new SignUp();
1361+
await signUp.__internal_future.web3({
1362+
strategy: 'web3_metamask_signature',
1363+
firstName: 'Vitalik',
1364+
lastName: 'Nakamoto',
1365+
locale: 'el-GR',
1366+
});
1367+
1368+
expect(mockFetch).toHaveBeenNthCalledWith(
1369+
1,
1370+
expect.objectContaining({
1371+
method: 'POST',
1372+
path: '/client/sign_ups',
1373+
body: expect.objectContaining({
1374+
firstName: 'Vitalik',
1375+
lastName: 'Nakamoto',
1376+
locale: 'el-GR',
1377+
}),
1378+
}),
1379+
);
1380+
});
11831381
});
11841382

11851383
describe('password', () => {
@@ -1255,6 +1453,77 @@ describe('SignUp', () => {
12551453

12561454
expect(result).toHaveProperty('error', null);
12571455
});
1456+
1457+
it('includes browser locale when creating a new signup', async () => {
1458+
vi.stubGlobal('navigator', { language: 'fr-FR' });
1459+
1460+
const mockFetch = vi.fn().mockResolvedValue({
1461+
client: null,
1462+
response: { id: 'signup_123', status: 'missing_requirements' },
1463+
});
1464+
BaseResource._fetch = mockFetch;
1465+
1466+
const signUp = new SignUp();
1467+
await signUp.__internal_future.password({ password: 'test-password-123' });
1468+
1469+
expect(mockFetch).toHaveBeenCalledWith(
1470+
expect.objectContaining({
1471+
method: 'POST',
1472+
path: '/client/sign_ups',
1473+
body: expect.objectContaining({
1474+
strategy: 'password',
1475+
locale: 'fr-FR',
1476+
}),
1477+
}),
1478+
);
1479+
});
1480+
1481+
it('prefers an explicitly provided locale over the browser locale', async () => {
1482+
vi.stubGlobal('navigator', { language: 'fr-FR' });
1483+
1484+
const mockFetch = vi.fn().mockResolvedValue({
1485+
client: null,
1486+
response: { id: 'signup_123', status: 'missing_requirements' },
1487+
});
1488+
BaseResource._fetch = mockFetch;
1489+
1490+
const signUp = new SignUp();
1491+
await signUp.__internal_future.password({ password: 'test-password-123', locale: 'el-GR' });
1492+
1493+
expect(mockFetch).toHaveBeenCalledWith(
1494+
expect.objectContaining({
1495+
method: 'POST',
1496+
path: '/client/sign_ups',
1497+
body: expect.objectContaining({
1498+
strategy: 'password',
1499+
locale: 'el-GR',
1500+
}),
1501+
}),
1502+
);
1503+
});
1504+
1505+
it('does not inject browser locale when updating an existing signup', async () => {
1506+
vi.stubGlobal('navigator', { language: 'fr-FR' });
1507+
1508+
const mockFetch = vi.fn().mockResolvedValue({
1509+
client: null,
1510+
response: { id: 'signup_123', status: 'missing_requirements' },
1511+
});
1512+
BaseResource._fetch = mockFetch;
1513+
1514+
const signUp = new SignUp({ id: 'signup_123' } as any);
1515+
await signUp.__internal_future.password({ password: 'test-password-123' });
1516+
1517+
expect(mockFetch).toHaveBeenCalledWith(
1518+
expect.objectContaining({
1519+
method: 'PATCH',
1520+
path: '/client/sign_ups/signup_123',
1521+
body: expect.not.objectContaining({
1522+
locale: expect.anything(),
1523+
}),
1524+
}),
1525+
);
1526+
});
12581527
});
12591528

12601529
describe('ticket', () => {

0 commit comments

Comments
 (0)