-
Notifications
You must be signed in to change notification settings - Fork 444
fix(clerk-js,shared): upgrade Stripe SDK from v5 to v9 #8223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
61686eb
a6f6fe6
042fd15
e06bf3b
94c15c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Comment on lines
+7
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify no explicit `any` remains in this test file.
rg -nP '\bany\b' packages/shared/src/react/billing/__tests__/usePaymentElement.spec.tsxRepository: clerk/javascript Length of output: 220 Remove explicit The Stripe mock objects and submit results use Proposed fix-let mockStripe: any = null;
-let mockElements: any = null;
+type MockStripe = {
+ confirmSetup: ReturnType<typeof vi.fn>;
+};
+
+type MockElements = {
+ create: ReturnType<typeof vi.fn>;
+ update: ReturnType<typeof vi.fn>;
+};
+
+type SubmitResult = Awaited<
+ ReturnType<ReturnType<typeof __experimental_usePaymentElement>['submit']>
+>;
+
+let mockStripe: MockStripe | null = null;
+let mockElements: MockElements | null = null;
@@
- let submitResult: any;
+ let submitResult: SubmitResult;
@@
- let submitResult: any;
+ let submitResult: SubmitResult;
@@
- let submitResult: any;
+ let submitResult: SubmitResult;Per coding guidelines: "Avoid 🤖 Prompt for AI Agents |
||
| 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}</__experimental_PaymentElementProvider>; | ||
| }; | ||
| } | ||
|
|
||
| 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); | ||
| }); | ||
| }); | ||
| }); | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: clerk/javascript
Length of output: 128
🏁 Script executed:
Repository: clerk/javascript
Length of output: 670
🏁 Script executed:
Repository: clerk/javascript
Length of output: 401
🏁 Script executed:
Repository: clerk/javascript
Length of output: 180
🏁 Script executed:
Repository: clerk/javascript
Length of output: 4393
🏁 Script executed:
Repository: clerk/javascript
Length of output: 11043
🏁 Script executed:
# Check recent git changes to understand what was modified git status git log --oneline -10Repository: clerk/javascript
Length of output: 359
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 678
🏁 Script executed:
Repository: clerk/javascript
Length of output: 81
🏁 Script executed:
Repository: clerk/javascript
Length of output: 905
🏁 Script executed:
Repository: clerk/javascript
Length of output: 744
🏁 Script executed:
Repository: clerk/javascript
Length of output: 79
🏁 Script executed:
Repository: clerk/javascript
Length of output: 186
🏁 Script executed:
Repository: clerk/javascript
Length of output: 238
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
Major Stripe SDK upgrade requires verification of payment confirmation flow before merge.
Lines 155-156 upgrade Stripe SDKs across major versions (v5→v9 for
stripe-js, v3→v6 forreact-stripe-js). These are significant version jumps that may introduce breaking changes. TheconfirmSetup()call atpackages/shared/src/react/billing/payment-element.tsx:329is critical billing code that must be verified to work correctly after this upgrade. Add test coverage (unit or e2e) or manual verification evidence for the payment confirmation flow before merging to prevent runtime billing failures.🤖 Prompt for AI Agents