From a9a2589cec22fc912c1e3738d8d510a433068252 Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Mon, 29 Sep 2025 10:16:18 -0700 Subject: [PATCH 01/10] pass locale to stripe elements --- packages/clerk-js/sandbox/app.ts | 23 +- packages/clerk-js/src/core/clerk.ts | 8 +- .../src/react/__tests__/commerce.test.tsx | 321 ++++++++++++++++++ packages/shared/src/react/commerce.tsx | 17 + 4 files changed, 357 insertions(+), 12 deletions(-) create mode 100644 packages/shared/src/react/__tests__/commerce.test.tsx diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 15d45afc722..f9793b0918a 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -237,19 +237,20 @@ function otherOptions() { }); const updateOtherOptions = () => { - void Clerk.__unstable__updateProps({ - options: Object.fromEntries( - Object.entries(otherOptionsInputs).map(([key, input]) => { - sessionStorage.setItem(key, input.value); + const options = Object.fromEntries( + Object.entries(otherOptionsInputs).map(([key, input]) => { + sessionStorage.setItem(key, input.value); - if (key === 'localization') { - return [key, l[input.value as keyof typeof l]]; - } + if (key === 'localization') { + const localizationObj = l[input.value as keyof typeof l]; + return [key, localizationObj]; + } - return [key, input.value]; - }), - ), - }); + return [key, input.value]; + }), + ); + + void Clerk.__unstable__updateProps({ options }); }; Object.values(otherOptionsInputs).forEach(input => { diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 6e676e2d371..f9971e10926 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2375,10 +2375,16 @@ export class Clerk implements ClerkInterface { // 2. clerk-js initializes propA with a default value // 3. The customer update propB independently of propA and window.Clerk.updateProps is called // 4. If we don't merge the new props with the current options, propA will be reset to undefined + const mergedOptions = { ...this.#options, ..._props.options }; + + // Update the Clerk instance's internal options + this.#options = mergedOptions; + const props = { ..._props, - options: this.#initOptions({ ...this.#options, ..._props.options }), + options: this.#initOptions(mergedOptions), }; + return this.#componentControls?.ensureMounted().then(controls => controls.updateProps(props)); }; diff --git a/packages/shared/src/react/__tests__/commerce.test.tsx b/packages/shared/src/react/__tests__/commerce.test.tsx new file mode 100644 index 00000000000..bd18ae14850 --- /dev/null +++ b/packages/shared/src/react/__tests__/commerce.test.tsx @@ -0,0 +1,321 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; + +import { OptionsContext } from '../contexts'; +import { __experimental_PaymentElementProvider, __experimental_PaymentElement } from '../commerce'; + +// Mock the Stripe components +jest.mock('../stripe-react', () => ({ + Elements: ({ children, options }: { children: React.ReactNode; options: any }) => ( +
+ {children} +
+ ), + PaymentElement: ({ fallback }: { fallback?: React.ReactNode }) => ( +
{fallback}
+ ), + useElements: () => null, + useStripe: () => null, +})); + +// Mock the hooks +const mockGetOption = jest.fn(); +jest.mock('../hooks/useClerk', () => ({ + useClerk: () => ({ + __internal_loadStripeJs: jest.fn().mockResolvedValue(() => Promise.resolve({})), + __internal_getOption: mockGetOption, + __unstable__environment: { + commerceSettings: { + billing: { + stripePublishableKey: 'pk_test_123', + }, + }, + displayConfig: { + userProfileUrl: 'https://example.com/profile', + organizationProfileUrl: 'https://example.com/org-profile', + }, + }, + }), +})); + +jest.mock('../hooks/useUser', () => ({ + useUser: () => ({ + user: { + id: 'user_123', + initializePaymentSource: jest.fn().mockResolvedValue({ + externalGatewayId: 'acct_123', + externalClientSecret: 'seti_123', + paymentMethodOrder: ['card'], + }), + }, + }), +})); + +jest.mock('../hooks/useOrganization', () => ({ + useOrganization: () => ({ + organization: null, + }), +})); + +jest.mock('swr', () => ({ + __esModule: true, + default: () => ({ data: { loadStripe: jest.fn().mockResolvedValue({}) } }), +})); + +jest.mock('swr/mutation', () => ({ + __esModule: true, + default: () => ({ + data: { + externalGatewayId: 'acct_123', + externalClientSecret: 'seti_123', + paymentMethodOrder: ['card'], + }, + trigger: jest.fn().mockResolvedValue({ + externalGatewayId: 'acct_123', + externalClientSecret: 'seti_123', + paymentMethodOrder: ['card'], + }), + }), +})); + +describe('PaymentElement Localization', () => { + const mockCheckout = { + id: 'checkout_123', + plan: { + id: 'plan_123', + name: 'Test Plan', + description: 'Test plan description', + fee: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' }, + annualFee: { amount: 10000, amountFormatted: '$100.00', currency: 'usd', currencySymbol: '$' }, + annualMonthlyFee: { amount: 833, amountFormatted: '$8.33', currency: 'usd', currencySymbol: '$' }, + currency: 'usd', + interval: 'month' as const, + intervalCount: 1, + maxAllowedInstances: 1, + trialDays: 0, + isAddon: false, + isPopular: false, + isPerSeat: false, + isUsageBased: false, + isFree: false, + isLegacy: false, + isDefault: false, + isRecurring: true, + hasBaseFee: true, + forPayerType: 'user' as const, + publiclyVisible: true, + slug: 'test-plan', + avatarUrl: '', + freeTrialDays: 0, + freeTrialEnabled: false, + pathRoot: '/', + reload: jest.fn(), + features: [], + limits: {}, + metadata: {}, + }, + totals: { + subtotal: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' }, + grandTotal: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' }, + taxTotal: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' }, + totalDueNow: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' }, + credit: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' }, + pastDue: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' }, + }, + status: 'needs_confirmation' as const, + error: null, + fetchStatus: 'idle' as const, + confirm: jest.fn(), + start: jest.fn(), + clear: jest.fn(), + finalize: jest.fn(), + getState: jest.fn(), + isConfirming: false, + isStarting: false, + planPeriod: 'month' as const, + externalClientSecret: 'seti_123', + externalGatewayId: 'acct_123', + isImmediatePlanChange: false, + paymentMethodOrder: ['card'], + freeTrialEndsAt: null, + payer: { + id: 'payer_123', + createdAt: new Date('2023-01-01'), + updatedAt: new Date('2023-01-01'), + imageUrl: null, + userId: 'user_123', + email: 'test@example.com', + firstName: 'Test', + lastName: 'User', + organizationId: undefined, + organizationName: undefined, + pathRoot: '/', + reload: jest.fn(), + }, + }; + + const renderWithLocale = (locale: string) => { + // Mock the __internal_getOption to return the expected localization + mockGetOption.mockImplementation(key => { + if (key === 'localization') { + return { locale }; + } + return undefined; + }); + + const options = { + localization: { locale }, + }; + + return render( + + <__experimental_PaymentElementProvider checkout={mockCheckout}> + <__experimental_PaymentElement fallback={
Loading...
} /> + +
, + ); + }; + + it('should pass the correct locale to Stripe Elements', () => { + renderWithLocale('es'); + + const elements = screen.getByTestId('stripe-elements'); + expect(elements).toHaveAttribute('data-locale', 'es'); + }); + + it('should default to "en" when no locale is provided', () => { + // Mock the __internal_getOption to return undefined for localization + mockGetOption.mockImplementation(key => { + if (key === 'localization') { + return undefined; + } + return undefined; + }); + + const options = {}; + + render( + + <__experimental_PaymentElementProvider checkout={mockCheckout}> + <__experimental_PaymentElement fallback={
Loading...
} /> + +
, + ); + + const elements = screen.getByTestId('stripe-elements'); + expect(elements).toHaveAttribute('data-locale', 'en'); + }); + + it('should handle different locale values', () => { + const locales = ['en', 'es', 'fr', 'de', 'it']; + + locales.forEach(locale => { + const { unmount } = renderWithLocale(locale); + + const elements = screen.getByTestId('stripe-elements'); + expect(elements).toHaveAttribute('data-locale', locale); + + unmount(); + }); + }); + + it('should handle undefined localization object', () => { + // Mock the __internal_getOption to return undefined for localization + mockGetOption.mockImplementation(key => { + if (key === 'localization') { + return undefined; + } + return undefined; + }); + + const options = { + localization: undefined, + }; + + render( + + <__experimental_PaymentElementProvider checkout={mockCheckout}> + <__experimental_PaymentElement fallback={
Loading...
} /> + +
, + ); + + const elements = screen.getByTestId('stripe-elements'); + expect(elements).toHaveAttribute('data-locale', 'en'); + }); + + it('should work with full LocalizationResource structure like ClerkProvider', () => { + // Mock the __internal_getOption to return the expected localization + mockGetOption.mockImplementation(key => { + if (key === 'localization') { + return { locale: 'fr-FR' }; + } + return undefined; + }); + + // This test simulates the actual ClerkProvider usage pattern: + // import { frFR } from '@clerk/localizations'; + // + const options = { + localization: { + locale: 'fr-FR', + // This would normally contain all the translation strings from frFR + // but we only need the locale property for our implementation + }, + }; + + render( + + <__experimental_PaymentElementProvider checkout={mockCheckout}> + <__experimental_PaymentElement fallback={
Loading...
} /> + +
, + ); + + const elements = screen.getByTestId('stripe-elements'); + // Should normalize 'fr-FR' to 'fr' for Stripe compatibility + expect(elements).toHaveAttribute('data-locale', 'fr'); + }); + + it('should normalize full locale strings to 2-letter codes for Stripe', () => { + const testCases = [ + { input: 'en-US', expected: 'en' }, + { input: 'fr-FR', expected: 'fr' }, + { input: 'es-ES', expected: 'es' }, + { input: 'de-DE', expected: 'de' }, + { input: 'it-IT', expected: 'it' }, + { input: 'pt-BR', expected: 'pt' }, + ]; + + testCases.forEach(({ input, expected }) => { + // Mock the __internal_getOption to return the expected localization + mockGetOption.mockImplementation(key => { + if (key === 'localization') { + return { locale: input }; + } + return undefined; + }); + + const options = { + localization: { locale: input }, + }; + + const { unmount } = render( + + <__experimental_PaymentElementProvider checkout={mockCheckout}> + <__experimental_PaymentElement fallback={
Loading...
} /> + +
, + ); + + const elements = screen.getByTestId('stripe-elements'); + expect(elements).toHaveAttribute('data-locale', expected); + + unmount(); + }); + }); +}); diff --git a/packages/shared/src/react/commerce.tsx b/packages/shared/src/react/commerce.tsx index 72fd978f4b1..0e7251cd2ba 100644 --- a/packages/shared/src/react/commerce.tsx +++ b/packages/shared/src/react/commerce.tsx @@ -62,6 +62,21 @@ const useInternalEnvironment = () => { return clerk.__unstable__environment as unknown as EnvironmentResource | null | undefined; }; +const useLocalization = () => { + const clerk = useClerk(); + + let locale = 'en'; + try { + const localization = clerk.__internal_getOption('localization'); + locale = localization?.locale || 'en'; + } catch (_) {} + + // Normalize locale to 2-letter language code for Stripe compatibility + const normalizedLocale = locale.split('-')[0]; + + return normalizedLocale; +}; + const usePaymentSourceUtils = (forResource: ForPayerType = 'user') => { const { organization } = useOrganization(); const { user } = useUser(); @@ -206,6 +221,7 @@ const PaymentElementProvider = ({ children, ...props }: PropsWithChildren { const { stripe, externalClientSecret, stripeAppearance } = usePaymentElementContext(); + const locale = useLocalization(); if (stripe && externalClientSecret) { return ( @@ -219,6 +235,7 @@ const PaymentElementInternalRoot = (props: PropsWithChildren) => { appearance: { variables: stripeAppearance, }, + locale: locale as any, }} > {props.children} From cca53a0bba848557094d82d2e277bfc275835a5b Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Mon, 29 Sep 2025 10:42:02 -0700 Subject: [PATCH 02/10] changeset --- .changeset/cold-bottles-watch.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/cold-bottles-watch.md diff --git a/.changeset/cold-bottles-watch.md b/.changeset/cold-bottles-watch.md new file mode 100644 index 00000000000..e57ff6acfba --- /dev/null +++ b/.changeset/cold-bottles-watch.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Pass current localization value to Stripe Elements From 9f503546790e0de9de7c329af06f29a7d0b1f0ca Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Mon, 29 Sep 2025 10:53:09 -0700 Subject: [PATCH 03/10] feedback --- packages/clerk-js/src/core/clerk.ts | 9 ++++++--- packages/shared/src/react/commerce.tsx | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f9971e10926..c014cdb39b0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2377,12 +2377,15 @@ export class Clerk implements ClerkInterface { // 4. If we don't merge the new props with the current options, propA will be reset to undefined const mergedOptions = { ...this.#options, ..._props.options }; - // Update the Clerk instance's internal options - this.#options = mergedOptions; + // Process the merged options to ensure consistency between internal state and emitted props + const processedOptions = this.#initOptions(mergedOptions); + + // Update the Clerk instance's internal options with processed data + this.#options = processedOptions; const props = { ..._props, - options: this.#initOptions(mergedOptions), + options: processedOptions, }; return this.#componentControls?.ensureMounted().then(controls => controls.updateProps(props)); diff --git a/packages/shared/src/react/commerce.tsx b/packages/shared/src/react/commerce.tsx index 0e7251cd2ba..ca143a6aa74 100644 --- a/packages/shared/src/react/commerce.tsx +++ b/packages/shared/src/react/commerce.tsx @@ -69,7 +69,9 @@ const useLocalization = () => { try { const localization = clerk.__internal_getOption('localization'); locale = localization?.locale || 'en'; - } catch (_) {} + } catch { + // ignore errors + } // Normalize locale to 2-letter language code for Stripe compatibility const normalizedLocale = locale.split('-')[0]; From a7f50d0c9d5f423b526a9eb105d4dd6d6ac207a8 Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Mon, 29 Sep 2025 11:02:23 -0700 Subject: [PATCH 04/10] linter fix --- packages/shared/src/react/__tests__/commerce.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/react/__tests__/commerce.test.tsx b/packages/shared/src/react/__tests__/commerce.test.tsx index bd18ae14850..9d090d8dff3 100644 --- a/packages/shared/src/react/__tests__/commerce.test.tsx +++ b/packages/shared/src/react/__tests__/commerce.test.tsx @@ -1,9 +1,10 @@ -import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; + +import { render, screen } from '@testing-library/react'; import React from 'react'; +import { __experimental_PaymentElement, __experimental_PaymentElementProvider } from '../commerce'; import { OptionsContext } from '../contexts'; -import { __experimental_PaymentElementProvider, __experimental_PaymentElement } from '../commerce'; // Mock the Stripe components jest.mock('../stripe-react', () => ({ From 132286568fdcf45a62e9f9b1074c80a14f7a3bad Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 30 Oct 2025 16:14:14 +0200 Subject: [PATCH 05/10] remove changes in props options --- packages/clerk-js/src/core/clerk.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index dabd9df11ab..c4a78fa2619 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2416,17 +2416,9 @@ export class Clerk implements ClerkInterface { // 2. clerk-js initializes propA with a default value // 3. The customer update propB independently of propA and window.Clerk.updateProps is called // 4. If we don't merge the new props with the current options, propA will be reset to undefined - const mergedOptions = { ...this.#options, ..._props.options }; - - // Process the merged options to ensure consistency between internal state and emitted props - const processedOptions = this.#initOptions(mergedOptions); - - // Update the Clerk instance's internal options with processed data - this.#options = processedOptions; - const props = { ..._props, - options: processedOptions, + options: this.#initOptions({ ...this.#options, ..._props.options }), }; return this.#componentControls?.ensureMounted().then(controls => controls.updateProps(props)); From fa215eecfa8cae8c3cfc1309d49d35909bf64ac5 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 30 Oct 2025 16:20:34 +0200 Subject: [PATCH 06/10] clean up some of the tests --- .../src/react/__tests__/commerce.test.tsx | 76 +------------------ 1 file changed, 2 insertions(+), 74 deletions(-) diff --git a/packages/shared/src/react/__tests__/commerce.test.tsx b/packages/shared/src/react/__tests__/commerce.test.tsx index 9d090d8dff3..4a9fb3aac85 100644 --- a/packages/shared/src/react/__tests__/commerce.test.tsx +++ b/packages/shared/src/react/__tests__/commerce.test.tsx @@ -16,9 +16,7 @@ jest.mock('../stripe-react', () => ({ {children} ), - PaymentElement: ({ fallback }: { fallback?: React.ReactNode }) => ( -
{fallback}
- ), + PaymentElement: ({ fallback }: { fallback?: React.ReactNode }) =>
{fallback}
, useElements: () => null, useStripe: () => null, })); @@ -211,79 +209,9 @@ describe('PaymentElement Localization', () => { expect(elements).toHaveAttribute('data-locale', 'en'); }); - it('should handle different locale values', () => { - const locales = ['en', 'es', 'fr', 'de', 'it']; - - locales.forEach(locale => { - const { unmount } = renderWithLocale(locale); - - const elements = screen.getByTestId('stripe-elements'); - expect(elements).toHaveAttribute('data-locale', locale); - - unmount(); - }); - }); - - it('should handle undefined localization object', () => { - // Mock the __internal_getOption to return undefined for localization - mockGetOption.mockImplementation(key => { - if (key === 'localization') { - return undefined; - } - return undefined; - }); - - const options = { - localization: undefined, - }; - - render( - - <__experimental_PaymentElementProvider checkout={mockCheckout}> - <__experimental_PaymentElement fallback={
Loading...
} /> - -
, - ); - - const elements = screen.getByTestId('stripe-elements'); - expect(elements).toHaveAttribute('data-locale', 'en'); - }); - - it('should work with full LocalizationResource structure like ClerkProvider', () => { - // Mock the __internal_getOption to return the expected localization - mockGetOption.mockImplementation(key => { - if (key === 'localization') { - return { locale: 'fr-FR' }; - } - return undefined; - }); - - // This test simulates the actual ClerkProvider usage pattern: - // import { frFR } from '@clerk/localizations'; - // - const options = { - localization: { - locale: 'fr-FR', - // This would normally contain all the translation strings from frFR - // but we only need the locale property for our implementation - }, - }; - - render( - - <__experimental_PaymentElementProvider checkout={mockCheckout}> - <__experimental_PaymentElement fallback={
Loading...
} /> - -
, - ); - - const elements = screen.getByTestId('stripe-elements'); - // Should normalize 'fr-FR' to 'fr' for Stripe compatibility - expect(elements).toHaveAttribute('data-locale', 'fr'); - }); - it('should normalize full locale strings to 2-letter codes for Stripe', () => { const testCases = [ + { input: 'en', expected: 'en' }, { input: 'en-US', expected: 'en' }, { input: 'fr-FR', expected: 'fr' }, { input: 'es-ES', expected: 'es' }, From e33b61facee3a9c857062df4dd56ba7661f98ca0 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 30 Oct 2025 16:34:22 +0200 Subject: [PATCH 07/10] remove as any --- packages/shared/src/react/commerce.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/react/commerce.tsx b/packages/shared/src/react/commerce.tsx index 1f9d355a86b..845bc52fba5 100644 --- a/packages/shared/src/react/commerce.tsx +++ b/packages/shared/src/react/commerce.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/consistent-type-imports */ import type { BillingCheckoutResource, EnvironmentResource, ForPayerType } from '@clerk/types'; -import type { Stripe, StripeElements } from '@stripe/stripe-js'; +import type { Stripe, StripeElements, StripeElementsOptions } from '@stripe/stripe-js'; import React, { type PropsWithChildren, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import useSWR from 'swr'; import useSWRMutation from 'swr/mutation'; @@ -237,7 +237,7 @@ const PaymentElementInternalRoot = (props: PropsWithChildren) => { appearance: { variables: stripeAppearance, }, - locale: locale as any, + locale: locale as StripeElementsOptions['locale'], }} > {props.children} From 154ed017de7ce2b81f3d52f2b3cf0a62bb51796b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 30 Oct 2025 16:46:13 +0200 Subject: [PATCH 08/10] replace jest with vite --- .../src/react/__tests__/commerce.test.tsx | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/shared/src/react/__tests__/commerce.test.tsx b/packages/shared/src/react/__tests__/commerce.test.tsx index 4a9fb3aac85..29cc56c5ac4 100644 --- a/packages/shared/src/react/__tests__/commerce.test.tsx +++ b/packages/shared/src/react/__tests__/commerce.test.tsx @@ -1,13 +1,12 @@ -import '@testing-library/jest-dom'; - import { render, screen } from '@testing-library/react'; import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; import { __experimental_PaymentElement, __experimental_PaymentElementProvider } from '../commerce'; import { OptionsContext } from '../contexts'; // Mock the Stripe components -jest.mock('../stripe-react', () => ({ +vi.mock('../stripe-react', () => ({ Elements: ({ children, options }: { children: React.ReactNode; options: any }) => (
({ })); // Mock the hooks -const mockGetOption = jest.fn(); -jest.mock('../hooks/useClerk', () => ({ +const mockGetOption = vi.fn(); +vi.mock('../hooks/useClerk', () => ({ useClerk: () => ({ - __internal_loadStripeJs: jest.fn().mockResolvedValue(() => Promise.resolve({})), + __internal_loadStripeJs: vi.fn().mockResolvedValue(() => Promise.resolve({})), __internal_getOption: mockGetOption, __unstable__environment: { commerceSettings: { @@ -41,11 +40,11 @@ jest.mock('../hooks/useClerk', () => ({ }), })); -jest.mock('../hooks/useUser', () => ({ +vi.mock('../hooks/useUser', () => ({ useUser: () => ({ user: { id: 'user_123', - initializePaymentSource: jest.fn().mockResolvedValue({ + initializePaymentSource: vi.fn().mockResolvedValue({ externalGatewayId: 'acct_123', externalClientSecret: 'seti_123', paymentMethodOrder: ['card'], @@ -54,18 +53,18 @@ jest.mock('../hooks/useUser', () => ({ }), })); -jest.mock('../hooks/useOrganization', () => ({ +vi.mock('../hooks/useOrganization', () => ({ useOrganization: () => ({ organization: null, }), })); -jest.mock('swr', () => ({ +vi.mock('swr', () => ({ __esModule: true, - default: () => ({ data: { loadStripe: jest.fn().mockResolvedValue({}) } }), + default: () => ({ data: { loadStripe: vi.fn().mockResolvedValue({}) } }), })); -jest.mock('swr/mutation', () => ({ +vi.mock('swr/mutation', () => ({ __esModule: true, default: () => ({ data: { @@ -73,7 +72,7 @@ jest.mock('swr/mutation', () => ({ externalClientSecret: 'seti_123', paymentMethodOrder: ['card'], }, - trigger: jest.fn().mockResolvedValue({ + trigger: vi.fn().mockResolvedValue({ externalGatewayId: 'acct_123', externalClientSecret: 'seti_123', paymentMethodOrder: ['card'], @@ -84,6 +83,7 @@ jest.mock('swr/mutation', () => ({ describe('PaymentElement Localization', () => { const mockCheckout = { id: 'checkout_123', + needsPaymentMethod: true, plan: { id: 'plan_123', name: 'Test Plan', @@ -112,7 +112,7 @@ describe('PaymentElement Localization', () => { freeTrialDays: 0, freeTrialEnabled: false, pathRoot: '/', - reload: jest.fn(), + reload: vi.fn(), features: [], limits: {}, metadata: {}, @@ -128,11 +128,11 @@ describe('PaymentElement Localization', () => { status: 'needs_confirmation' as const, error: null, fetchStatus: 'idle' as const, - confirm: jest.fn(), - start: jest.fn(), - clear: jest.fn(), - finalize: jest.fn(), - getState: jest.fn(), + confirm: vi.fn(), + start: vi.fn(), + clear: vi.fn(), + finalize: vi.fn(), + getState: vi.fn(), isConfirming: false, isStarting: false, planPeriod: 'month' as const, @@ -153,7 +153,7 @@ describe('PaymentElement Localization', () => { organizationId: undefined, organizationName: undefined, pathRoot: '/', - reload: jest.fn(), + reload: vi.fn(), }, }; @@ -183,7 +183,7 @@ describe('PaymentElement Localization', () => { renderWithLocale('es'); const elements = screen.getByTestId('stripe-elements'); - expect(elements).toHaveAttribute('data-locale', 'es'); + expect(elements.getAttribute('data-locale')).toBe('es'); }); it('should default to "en" when no locale is provided', () => { @@ -206,7 +206,7 @@ describe('PaymentElement Localization', () => { ); const elements = screen.getByTestId('stripe-elements'); - expect(elements).toHaveAttribute('data-locale', 'en'); + expect(elements.getAttribute('data-locale')).toBe('en'); }); it('should normalize full locale strings to 2-letter codes for Stripe', () => { @@ -242,7 +242,7 @@ describe('PaymentElement Localization', () => { ); const elements = screen.getByTestId('stripe-elements'); - expect(elements).toHaveAttribute('data-locale', expected); + expect(elements.getAttribute('data-locale')).toBe(expected); unmount(); }); From b87f1629bb679eff595b8bcff746a3a28b32cfc9 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 30 Oct 2025 19:41:49 +0200 Subject: [PATCH 09/10] Apply suggestion from @panteliselef --- .changeset/cold-bottles-watch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cold-bottles-watch.md b/.changeset/cold-bottles-watch.md index e57ff6acfba..a525e856df1 100644 --- a/.changeset/cold-bottles-watch.md +++ b/.changeset/cold-bottles-watch.md @@ -3,4 +3,4 @@ '@clerk/shared': patch --- -Pass current localization value to Stripe Elements +Propagate locale from ClerkProvider to PaymentElement From 7917403d04b746b94a8b520083cecd0d26d0679b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 30 Oct 2025 19:43:11 +0200 Subject: [PATCH 10/10] revert change --- packages/clerk-js/sandbox/app.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 7bb78295ca5..6132a5f5025 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -239,20 +239,19 @@ function otherOptions() { }); const updateOtherOptions = () => { - const options = Object.fromEntries( - Object.entries(otherOptionsInputs).map(([key, input]) => { - sessionStorage.setItem(key, input.value); - - if (key === 'localization') { - const localizationObj = l[input.value as keyof typeof l]; - return [key, localizationObj]; - } + void Clerk.__unstable__updateProps({ + options: Object.fromEntries( + Object.entries(otherOptionsInputs).map(([key, input]) => { + sessionStorage.setItem(key, input.value); - return [key, input.value]; - }), - ); + if (key === 'localization') { + return [key, l[input.value as keyof typeof l]]; + } - void Clerk.__unstable__updateProps({ options }); + return [key, input.value]; + }), + ), + }); }; Object.values(otherOptionsInputs).forEach(input => {