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
5 changes: 5 additions & 0 deletions .changeset/quick-kings-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since there is no change (even backwards compatible) to the sdk api for users, I think we have a patch here -- open to make it a minor as well

---

Add support for automatically sending the browser locale during the sign-in flow
14 changes: 11 additions & 3 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
generateSignatureWithMetamask,
generateSignatureWithOKXWallet,
getBaseIdentifier,
getBrowserLocale,
getClerkQueryParam,
getCoinbaseWalletIdentifier,
getMetamaskIdentifier,
Expand Down Expand Up @@ -164,9 +165,10 @@ export class SignIn extends BaseResource implements SignInResource {

create = (params: SignInCreateParams): Promise<SignInResource> => {
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,
});
};

Expand Down Expand Up @@ -708,9 +710,10 @@ class SignInFuture implements SignInFutureResource {
}

private async _create(params: SignInFutureCreateParams): Promise<void> {
const locale = getBrowserLocale();
await this.resource.__internal_basePost({
path: this.resource.pathRoot,
body: params,
body: locale ? { locale, ...params } : params,
});
}

Expand All @@ -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 } : {}),
},
});
});
}
Expand Down
114 changes: 85 additions & 29 deletions packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with new tests and mocks, this was source of failing tests. So I just added moved the into the beforeAll function so we can have more control over order and scope

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 () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/clerk-js/vitest.setup.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading