diff --git a/.changeset/quick-kings-rescue.md b/.changeset/quick-kings-rescue.md new file mode 100644 index 00000000000..fb9c37d9c32 --- /dev/null +++ b/.changeset/quick-kings-rescue.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Add support for automatically sending the browser locale during the sign-in flow diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index e98538e1ba5..d2bb1dee2fb 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -69,6 +69,7 @@ import { generateSignatureWithMetamask, generateSignatureWithOKXWallet, getBaseIdentifier, + getBrowserLocale, getClerkQueryParam, getCoinbaseWalletIdentifier, getMetamaskIdentifier, @@ -164,9 +165,10 @@ export class SignIn extends BaseResource implements SignInResource { create = (params: SignInCreateParams): Promise => { debugLogger.debug('SignIn.create', { id: this.id, strategy: 'strategy' in params ? params.strategy : undefined }); + const locale = getBrowserLocale(); return this._basePost({ path: this.pathRoot, - body: params, + body: locale ? { locale, ...params } : params, }); }; @@ -708,9 +710,10 @@ class SignInFuture implements SignInFutureResource { } private async _create(params: SignInFutureCreateParams): Promise { + const locale = getBrowserLocale(); await this.resource.__internal_basePost({ path: this.resource.pathRoot, - body: params, + body: locale ? { locale, ...params } : params, }); } @@ -729,9 +732,14 @@ class SignInFuture implements SignInFutureResource { // TODO @userland-errors: const identifier = params.identifier || params.emailAddress || params.phoneNumber; const previousIdentifier = this.resource.identifier; + const locale = getBrowserLocale(); await this.resource.__internal_basePost({ path: this.resource.pathRoot, - body: { identifier: identifier || previousIdentifier, password: params.password }, + body: { + identifier: identifier || previousIdentifier, + password: params.password, + ...(locale ? { locale } : {}), + }, }); }); } diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 824e8845460..c18e20aeb56 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -1,39 +1,95 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { BaseResource } from '../internal'; import { SignIn } from '../SignIn'; describe('SignIn', () => { + describe('signIn.create', () => { + afterEach(() => { + vi.clearAllMocks(); + vi.unstubAllGlobals(); + }); + + it('includes locale in request body when navigator.language is available', async () => { + vi.stubGlobal('navigator', { language: 'fr-FR' }); + + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status: 'needs_first_factor' }, + }); + BaseResource._fetch = mockFetch; + + const signIn = new SignIn(); + await signIn.create({ identifier: 'user@example.com' }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + path: '/client/sign_ins', + body: { + identifier: 'user@example.com', + locale: 'fr-FR', + }, + }), + ); + }); + + it('excludes locale from request body when navigator.language is empty', async () => { + vi.stubGlobal('navigator', { language: '' }); + + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status: 'needs_first_factor' }, + }); + BaseResource._fetch = mockFetch; + + const signIn = new SignIn(); + await signIn.create({ identifier: 'user@example.com' }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + path: '/client/sign_ins', + body: { + identifier: 'user@example.com', + }, + }), + ); + }); + }); + describe('SignInFuture', () => { describe('selectFirstFactor', () => { - const signInCreatedJSON = { - id: 'test_id', - - supported_first_factors: [ - { strategy: 'email_code', emailAddressId: 'email_address_0', safe_identifier: 'test+abc@clerk.com' }, - { strategy: 'email_code', emailAddressId: 'email_address_1', safe_identifier: 'test@clerk.com' }, - { strategy: 'phone_code', phoneNumberId: 'phone_number_1', safe_identifier: '+301234567890' }, - ], - }; - - const firstFactorPreparedJSON = {}; - - BaseResource._fetch = vi.fn().mockImplementation(({ method, path, body }) => { - if (method === 'POST' && path === '/client/sign_ins') { - return Promise.resolve({ - client: null, - response: { ...signInCreatedJSON, identifier: body.identifier }, - }); - } - - if (method === 'POST' && path === '/client/sign_ins/test_id/prepare_first_factor') { - return Promise.resolve({ - client: null, - response: firstFactorPreparedJSON, - }); - } - - throw new Error('Unexpected call to BaseResource._fetch'); + beforeAll(() => { + const signInCreatedJSON = { + id: 'test_id', + + supported_first_factors: [ + { strategy: 'email_code', emailAddressId: 'email_address_0', safe_identifier: 'test+abc@clerk.com' }, + { strategy: 'email_code', emailAddressId: 'email_address_1', safe_identifier: 'test@clerk.com' }, + { strategy: 'phone_code', phoneNumberId: 'phone_number_1', safe_identifier: '+301234567890' }, + ], + }; + + const firstFactorPreparedJSON = {}; + + BaseResource._fetch = vi.fn().mockImplementation(({ method, path, body }) => { + if (method === 'POST' && path === '/client/sign_ins') { + return Promise.resolve({ + client: null, + response: { ...signInCreatedJSON, identifier: body.identifier }, + }); + } + + if (method === 'POST' && path === '/client/sign_ins/test_id/prepare_first_factor') { + return Promise.resolve({ + client: null, + response: firstFactorPreparedJSON, + }); + } + + throw new Error('Unexpected call to BaseResource._fetch'); + }); }); it('should select correct first factor by email address', async () => { diff --git a/packages/clerk-js/vitest.setup.mts b/packages/clerk-js/vitest.setup.mts index 2e81392e900..a9acc96325b 100644 --- a/packages/clerk-js/vitest.setup.mts +++ b/packages/clerk-js/vitest.setup.mts @@ -103,6 +103,12 @@ if (typeof window !== 'undefined') { writable: true, }); + Object.defineProperty(window.navigator, 'language', { + writable: true, + configurable: true, + value: '', + }); + // Mock IntersectionObserver //@ts-expect-error - Mocking class globalThis.IntersectionObserver = class IntersectionObserver {