diff --git a/.changeset/small-rugs-add.md b/.changeset/small-rugs-add.md new file mode 100644 index 00000000000..7436c2ee3f4 --- /dev/null +++ b/.changeset/small-rugs-add.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Upgrade `@stripe/stripe-js` from 5.6.0 to 9.0.0 and `@stripe/react-stripe-js` from 3.1.1 to 6.0.0 diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 5b44126e81d..537c95c710e 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -8,7 +8,7 @@ { "path": "./dist/vendors*.js", "maxSize": "7KB" }, { "path": "./dist/coinbase*.js", "maxSize": "36KB" }, { "path": "./dist/base-account-sdk*.js", "maxSize": "203KB" }, - { "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" }, + { "path": "./dist/stripe-vendors*.js", "maxSize": "2KB" }, { "path": "./dist/query-core-vendors*.js", "maxSize": "11KB" }, { "path": "./dist/zxcvbn-ts-core*.js", "maxSize": "12KB" }, { "path": "./dist/zxcvbn-common*.js", "maxSize": "226KB" } diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 4566c5fc3d4..26670dc986e 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -88,7 +88,7 @@ "@solana/wallet-adapter-base": "catalog:module-manager", "@solana/wallet-adapter-react": "catalog:module-manager", "@solana/wallet-standard": "catalog:module-manager", - "@stripe/stripe-js": "5.6.0", + "@stripe/stripe-js": "9.0.0", "@swc/helpers": "catalog:repo", "@tanstack/query-core": "5.90.16", "@wallet-standard/core": "catalog:module-manager", diff --git a/packages/shared/package.json b/packages/shared/package.json index 301dc8cf0dc..3057cbdc8b8 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -152,8 +152,8 @@ "@solana/wallet-adapter-base": "catalog:module-manager", "@solana/wallet-adapter-react": "catalog:module-manager", "@solana/wallet-standard": "catalog:module-manager", - "@stripe/react-stripe-js": "3.1.1", - "@stripe/stripe-js": "5.6.0", + "@stripe/react-stripe-js": "6.0.0", + "@stripe/stripe-js": "9.0.0", "@types/glob-to-regexp": "0.4.4", "@types/js-cookie": "3.0.6", "@wallet-standard/core": "catalog:module-manager", diff --git a/packages/shared/src/react/billing/__tests__/usePaymentElement.spec.tsx b/packages/shared/src/react/billing/__tests__/usePaymentElement.spec.tsx new file mode 100644 index 00000000000..aac9222a097 --- /dev/null +++ b/packages/shared/src/react/billing/__tests__/usePaymentElement.spec.tsx @@ -0,0 +1,227 @@ +import { act, renderHook } from '@testing-library/react'; +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// --- Mock state --- + +let mockStripe: any = null; +let mockElements: any = null; +const mockInitializePaymentMethod = vi.fn(); + +vi.mock('../../stripe-react', () => ({ + Elements: ({ children }: { children: React.ReactNode }) => <>{children}, + PaymentElement: () => null, + useElements: () => mockElements, + useStripe: () => mockStripe, +})); + +vi.mock('../../hooks/useClerk', () => ({ + useClerk: () => ({ + __internal_getOption: () => undefined, + __internal_environment: { + commerceSettings: { + billing: { + stripePublishableKey: 'pk_test_123', + }, + }, + displayConfig: { + userProfileUrl: 'https://example.com/profile', + organizationProfileUrl: 'https://example.com/org-profile', + }, + }, + }), +})); + +vi.mock('../useInitializePaymentMethod', () => ({ + __internal_useInitializePaymentMethod: () => ({ + initializedPaymentMethod: { + externalGatewayId: 'acct_123', + externalClientSecret: 'seti_123', + paymentMethodOrder: ['card'], + }, + initializePaymentMethod: mockInitializePaymentMethod, + }), +})); + +vi.mock('../useStripeClerkLibs', () => ({ + __internal_useStripeClerkLibs: () => ({ + loadStripe: vi.fn().mockResolvedValue({}), + }), +})); + +vi.mock('../useStripeLoader', () => ({ + __internal_useStripeLoader: () => ({}), +})); + +const { __experimental_PaymentElementProvider, __experimental_usePaymentElement } = await import('../payment-element'); + +function createWrapper() { + return function Wrapper({ children }: { children: React.ReactNode }) { + return <__experimental_PaymentElementProvider>{children}; + }; +} + +describe('usePaymentElement', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockStripe = null; + mockElements = null; + }); + + describe('when provider is not ready (no stripe/elements)', () => { + it('returns isProviderReady=false and isFormReady=false', () => { + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + expect(result.current.isProviderReady).toBe(false); + expect(result.current.isFormReady).toBe(false); + expect(result.current.provider).toBeUndefined(); + }); + + it('submit throws when stripe is not loaded', () => { + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + expect(() => result.current.submit()).toThrow('Clerk: Unable to submit, Stripe libraries are not yet loaded'); + }); + + it('reset throws when stripe is not loaded', () => { + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + expect(() => result.current.reset()).toThrow('Clerk: Unable to submit, Stripe libraries are not yet loaded'); + }); + }); + + describe('when provider is ready', () => { + beforeEach(() => { + mockStripe = { + confirmSetup: vi.fn(), + }; + mockElements = { + create: vi.fn(), + update: vi.fn(), + }; + }); + + it('returns isProviderReady=true with stripe provider info', () => { + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + expect(result.current.isProviderReady).toBe(true); + expect(result.current.provider).toEqual({ name: 'stripe' }); + }); + + it('submit returns paymentToken on successful confirmSetup', async () => { + mockStripe.confirmSetup.mockResolvedValue({ + setupIntent: { payment_method: 'pm_test_123' }, + error: null, + }); + + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + let submitResult: any; + await act(async () => { + submitResult = await result.current.submit(); + }); + + expect(mockStripe.confirmSetup).toHaveBeenCalledWith({ + elements: mockElements, + confirmParams: { + return_url: window.location.href, + }, + redirect: 'if_required', + }); + expect(submitResult).toEqual({ + data: { gateway: 'stripe', paymentToken: 'pm_test_123' }, + error: null, + }); + }); + + it('submit returns structured error when confirmSetup fails', async () => { + mockStripe.confirmSetup.mockResolvedValue({ + setupIntent: null, + error: { + type: 'card_error', + code: 'card_declined', + message: 'Your card was declined.', + }, + }); + + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + let submitResult: any; + await act(async () => { + submitResult = await result.current.submit(); + }); + + expect(submitResult).toEqual({ + data: null, + error: { + gateway: 'stripe', + error: { + type: 'card_error', + code: 'card_declined', + message: 'Your card was declined.', + }, + }, + }); + }); + + it('submit handles validation_error type from confirmSetup', async () => { + mockStripe.confirmSetup.mockResolvedValue({ + setupIntent: null, + error: { + type: 'validation_error', + message: 'Your card number is incomplete.', + }, + }); + + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + let submitResult: any; + await act(async () => { + submitResult = await result.current.submit(); + }); + + expect(submitResult).toEqual({ + data: null, + error: { + gateway: 'stripe', + error: { + type: 'validation_error', + code: undefined, + message: 'Your card number is incomplete.', + }, + }, + }); + }); + + it('reset calls initializePaymentMethod', async () => { + mockInitializePaymentMethod.mockResolvedValue({ + externalClientSecret: 'seti_new_456', + gateway: 'stripe', + }); + + const { result } = renderHook(() => __experimental_usePaymentElement(), { + wrapper: createWrapper(), + }); + + await act(async () => { + await result.current.reset(); + }); + + expect(mockInitializePaymentMethod).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a5b24875d4..caeab9d5477 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -472,8 +472,8 @@ importers: specifier: catalog:module-manager version: 1.1.4(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bs58@6.0.0)(react@18.3.1) '@stripe/stripe-js': - specifier: 5.6.0 - version: 5.6.0 + specifier: 9.0.0 + version: 9.0.0 '@swc/helpers': specifier: catalog:repo version: 0.5.17 @@ -888,11 +888,11 @@ importers: specifier: catalog:module-manager version: 1.1.4(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bs58@6.0.0)(react@18.3.1) '@stripe/react-stripe-js': - specifier: 3.1.1 - version: 3.1.1(@stripe/stripe-js@5.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 6.0.0 + version: 6.0.0(@stripe/stripe-js@9.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@stripe/stripe-js': - specifier: 5.6.0 - version: 5.6.0 + specifier: 9.0.0 + version: 9.0.0 '@types/glob-to-regexp': specifier: 0.4.4 version: 0.4.4 @@ -2607,7 +2607,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -4783,15 +4783,15 @@ packages: '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} - '@stripe/react-stripe-js@3.1.1': - resolution: {integrity: sha512-+JzYFgUivVD7koqYV7LmLlt9edDMAwKH7XhZAHFQMo7NeRC+6D2JmQGzp9tygWerzwttwFLlExGp4rAOvD6l9g==} + '@stripe/react-stripe-js@6.0.0': + resolution: {integrity: sha512-vJW37/uyRAJEkD3YKCh3aISmMIMrQGR3ViVBkUVQDB7CqbRCnOUyhwDe8zKg+d6K8V4d0MUcthcQdHL4meqG7A==} peerDependencies: - '@stripe/stripe-js': ^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@stripe/stripe-js': '>=9.0.0 <10.0.0' react: 18.3.1 react-dom: 18.3.1 - '@stripe/stripe-js@5.6.0': - resolution: {integrity: sha512-w8CEY73X/7tw2KKlL3iOk679V9bWseE4GzNz3zlaYxcTjmcmWOathRb0emgo/QQ3eoNzmq68+2Y2gxluAv3xGw==} + '@stripe/stripe-js@9.0.0': + resolution: {integrity: sha512-UtUK7LFglQSNVIOwcRBaxfzq4wkfU8CT2f2uQVEeu65jTP6wicxFshYdYCke9Tfd0PCxhOe/b8rwJjr5EiDCfQ==} engines: {node: '>=12.16'} '@svgr/babel-plugin-add-jsx-attribute@6.5.1': @@ -20047,14 +20047,14 @@ snapshots: '@stablelib/base64@1.0.1': {} - '@stripe/react-stripe-js@3.1.1(@stripe/stripe-js@5.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@stripe/react-stripe-js@6.0.0(@stripe/stripe-js@9.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@stripe/stripe-js': 5.6.0 + '@stripe/stripe-js': 9.0.0 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@stripe/stripe-js@5.6.0': {} + '@stripe/stripe-js@9.0.0': {} '@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.28.5)': dependencies: