diff --git a/.changeset/signup-future-password-browser-locale.md b/.changeset/signup-future-password-browser-locale.md new file mode 100644 index 00000000000..1f459ad93bc --- /dev/null +++ b/.changeset/signup-future-password-browser-locale.md @@ -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. diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index f5d5842b777..3ddac1911c4 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -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" }, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 4527d168d14..bccdfa48919 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -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 }); } }); @@ -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 }); @@ -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 }); }; @@ -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 () => { @@ -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', diff --git a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts index c190e2b68e6..4e3f7911fe8 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts @@ -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' } }); @@ -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', () => { @@ -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', () => {