From e97fec2f741dd39330b48648083f0422c643b9d0 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Thu, 4 Dec 2025 17:12:52 -0800 Subject: [PATCH 01/18] CardFieldsProvider and context created with tests --- .../CardFieldsProvider.ssr.test.tsx | 198 ++++++++++++ .../v6/components/CardFieldsProvider.test.tsx | 301 ++++++++++++++++++ .../src/v6/components/CardFieldsProvider.tsx | 89 ++++++ .../src/v6/context/CardFieldsContext.tsx | 9 + .../src/v6/hooks/useCardFields.test.ts | 13 + .../src/v6/hooks/useCardFields.ts | 23 ++ 6 files changed, 633 insertions(+) create mode 100644 packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx create mode 100644 packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx create mode 100644 packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx create mode 100644 packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx create mode 100644 packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts create mode 100644 packages/react-paypal-js/src/v6/hooks/useCardFields.ts diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx new file mode 100644 index 00000000..7e56ff59 --- /dev/null +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx @@ -0,0 +1,198 @@ +/** + * @jest-environment node + */ + +import React from "react"; + +import { usePayPal } from "../hooks/usePayPal"; +import { CardFieldsState } from "../context/CardFieldsContext"; +import { useCardFields } from "../hooks/useCardFields"; +import { INSTANCE_LOADING_STATE } from "../types"; +import { isServer } from "../utils"; +import { CardFieldsProvider, sessionType } from "./CardFieldsProvider"; +import { renderToString } from "react-dom/server"; + +jest.mock("../hooks/usePayPal"); + +jest.mock("../utils", () => ({ + ...jest.requireActual("../utils"), + isServer: () => true, +})); + +const mockUsePayPal = usePayPal as jest.MockedFunction; + +// Test utilites +function expectInitialState(state: Partial): void { + expect(state.cardFieldsSession).toBe(null); + expect(state.cardFieldsError).toBe(null); +} + +// Render helpers +function renderSSRCardFieldsProvider({ + sessionType, + children, +}: { + sessionType: sessionType; + children?: React.ReactNode; +}) { + const { cardFieldsState, TestComponent } = setupSSRTestComponent(); + + const html = renderToString( + + {children} + , + ); + + return { html, cardFieldsState }; +} + +function setupSSRTestComponent() { + const cardFieldsState: CardFieldsState = { + cardFieldsSession: null, + cardFieldsError: null, + }; + + function TestComponent({ + children = null, + }: { + children?: React.ReactNode; + }) { + try { + const newCardFieldsState = useCardFields(); + Object.assign(cardFieldsState, newCardFieldsState); + } catch (error) { + cardFieldsState.cardFieldsError = error as Error; + } + + return <>{children}; + } + + return { cardFieldsState, TestComponent }; +} + +describe("CardFieldsProvider SSR", () => { + beforeEach(() => { + mockUsePayPal.mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.PENDING, + eligiblePaymentMethods: null, + error: null, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("Server-Side Rendering", () => { + test("should verify isServer mock is working", () => { + expect(isServer()).toBe(true); + }); + + test("should not attempt DOM access during server rendering", () => { + // In Node environment, document should be undefined + expect(typeof document).toBe("undefined"); + + // Will throw error if any DOM access is attempted during render + expect(() => + renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + }), + ).not.toThrow(); + }); + + test("should work correctly in SSR context", () => { + const { cardFieldsState } = renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + }); + + expectInitialState(cardFieldsState); + }); + + test("should render without warnings or errors", () => { + // This sdkInstance state would fail client side + mockUsePayPal.mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, + eligiblePaymentMethods: null, + error: null, + }); + + const consoleSpy = jest + .spyOn(console, "error") + .mockImplementation(); + + const { html } = renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + children:
SSR Content
, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + expect(html).toBeTruthy(); + expect(typeof html).toBe("string"); + expect(html).toContain("SSR Content"); + + consoleSpy.mockRestore(); + }); + }); + + describe("Hydration Preparation", () => { + test("should prepare serializable state for client hydration", () => { + const { cardFieldsState } = renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + }); + + // Server state should be safe for serialization + const serializedState = JSON.stringify(cardFieldsState); + + expect(() => JSON.parse(serializedState)).not.toThrow(); + + const parsedState = JSON.parse(serializedState); + expectInitialState(parsedState); + }); + + test("should maintain consistent state with different options", () => { + const { cardFieldsState: cardFieldsState1 } = + renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + }); + const { cardFieldsState: cardFieldsState2 } = + renderSSRCardFieldsProvider({ sessionType: "save-payment" }); + + // Both should have consistent initial state regardless of options + expectInitialState(cardFieldsState1); + expectInitialState(cardFieldsState2); + }); + }); + + describe("Multiple renders", () => { + test("should maintain state consistency across multiple server renders", () => { + const { html: html1, cardFieldsState: cardFieldsState1 } = + renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + }); + const { html: html2, cardFieldsState: cardFieldsState2 } = + renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + }); + + expectInitialState(cardFieldsState1); + expectInitialState(cardFieldsState2); + expect(html1).toBe(html2); + }); + + test("should generate consistent HTML across renders", () => { + const { html: html1 } = renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + children:
Test Content
, + }); + const { html: html2 } = renderSSRCardFieldsProvider({ + sessionType: "one-time-payment", + children:
Test Content
, + }); + + expect(html1).toBe(html2); + expect(html1).toContain('data-testid="ssr-content"'); + }); + }); +}); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx new file mode 100644 index 00000000..6dbaf977 --- /dev/null +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx @@ -0,0 +1,301 @@ +import { usePayPal } from "../hooks/usePayPal"; +import React from "react"; +import { INSTANCE_LOADING_STATE } from "../types"; + +import { expectCurrentErrorValue } from "../hooks/useErrorTestUtil"; +import type { + CardFieldsOneTimePaymentSession, + CardFieldsSavePaymentSession, +} from "../types"; +import { CardFieldsProvider, sessionType } from "./CardFieldsProvider"; +import { renderHook } from "@testing-library/react-hooks"; +import { useCardFields } from "../hooks/useCardFields"; +import { toError } from "../utils"; + +jest.mock("../hooks/usePayPal"); + +jest.mock("../utils", () => ({ + ...jest.requireActual("../utils"), + isServer: () => false, +})); + +const mockUsePayPal = usePayPal as jest.MockedFunction; + +const oneTimePaymentSessionType: sessionType = "one-time-payment"; +const savePaymentSessionType: sessionType = "save-payment"; + +// Mock Factories +const createMockOneTimePaymentSession = + (): CardFieldsOneTimePaymentSession => ({ + createCardFieldsComponent: jest.fn(), + on: jest.fn(), + submit: jest.fn(), + update: jest.fn(), + }); + +const createMockSavePaymentSession = (): CardFieldsSavePaymentSession => ({ + createCardFieldsComponent: jest.fn(), + on: jest.fn(), + submit: jest.fn(), + update: jest.fn(), +}); + +const createMockSdkInstance = ({ + cardFieldsOneTimePaymentSession = createMockOneTimePaymentSession(), + cardFieldsSavePaymentSession = createMockSavePaymentSession(), +}: { + cardFieldsOneTimePaymentSession?: CardFieldsOneTimePaymentSession; + cardFieldsSavePaymentSession?: CardFieldsSavePaymentSession; +} = {}) => ({ + createCardFieldsOneTimePaymentSession: jest + .fn() + .mockReturnValue(cardFieldsOneTimePaymentSession), + createCardFieldsSavePaymentSession: jest + .fn() + .mockReturnValue(cardFieldsSavePaymentSession), +}); + +// Render helper +function renderCardFieldsProvider({ + sessionType, +}: { + sessionType: sessionType; +}) { + return renderHook(() => useCardFields(), { + initialProps: { sessionType }, + wrapper: ({ children, sessionType }) => ( + + {children} + + ), + }); +} + +describe("CardFieldsProvider", () => { + let mockCardFieldsOneTimePaymentSession: CardFieldsOneTimePaymentSession; + let mockCardFieldsSavePaymentSession: CardFieldsSavePaymentSession; + let mockSdkInstance: ReturnType; + + beforeEach(() => { + mockCardFieldsOneTimePaymentSession = createMockOneTimePaymentSession(); + mockCardFieldsSavePaymentSession = createMockSavePaymentSession(); + mockSdkInstance = createMockSdkInstance({ + cardFieldsOneTimePaymentSession: + mockCardFieldsOneTimePaymentSession, + cardFieldsSavePaymentSession: mockCardFieldsSavePaymentSession, + }); + + mockUsePayPal.mockReturnValue({ + // @ts-expect-error mocking sdk instance + sdkInstance: mockSdkInstance, + loadingStatus: INSTANCE_LOADING_STATE.RESOLVED, + eligiblePaymentMethods: null, + error: null, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("initialization", () => { + test("should not error if there is no sdkInstance but loading is still pending", () => { + mockUsePayPal.mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.PENDING, + eligiblePaymentMethods: null, + error: null, + }); + + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBeNull(); + expect(result.current.cardFieldsError).toBeNull(); + expectCurrentErrorValue(null); + }); + + test("should error if there is no sdkInstance and loading is rejected", () => { + mockUsePayPal.mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, + eligiblePaymentMethods: null, + error: null, + }); + + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBeNull(); + expect(result.current.cardFieldsError).toEqual( + toError("no sdk instance available"), + ); + expectCurrentErrorValue(result.current.cardFieldsError); + }); + + test("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { + // First render: no sdkInstance and not in PENDING state, should error + mockUsePayPal.mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, + eligiblePaymentMethods: null, + error: null, + }); + + const { result, rerender } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBeNull(); + expect(result.current.cardFieldsError).toEqual( + toError("no sdk instance available"), + ); + expectCurrentErrorValue(result.current.cardFieldsError); + + // Second render: sdkInstance becomes available, error should clear + mockUsePayPal.mockReturnValue({ + // @ts-expect-error mocking sdk instance + sdkInstance: mockSdkInstance, + loadingStatus: INSTANCE_LOADING_STATE.RESOLVED, + eligiblePaymentMethods: null, + error: null, + }); + + rerender(); + expect(result.current.cardFieldsSession).toBe( + mockCardFieldsOneTimePaymentSession, + ); + expect(result.current.cardFieldsError).toBeNull(); + expectCurrentErrorValue(null); + }); + }); + + describe("sessionType", () => { + test.each([ + { + sessionType: oneTimePaymentSessionType, + expectedMethod: + "createCardFieldsOneTimePaymentSession" as const, + unexpectedMethod: "createCardFieldsSavePaymentSession" as const, + }, + { + sessionType: savePaymentSessionType, + expectedMethod: "createCardFieldsSavePaymentSession" as const, + unexpectedMethod: + "createCardFieldsOneTimePaymentSession" as const, + }, + ])( + "should call $expectedMethod method when sessionType is $sessionType", + ({ sessionType, expectedMethod, unexpectedMethod }) => { + renderCardFieldsProvider({ + sessionType, + }); + + // Check that the correct create method was called + expect(mockSdkInstance[expectedMethod]).toHaveBeenCalledTimes( + 1, + ); + expect( + mockSdkInstance[unexpectedMethod], + ).not.toHaveBeenCalled(); + }, + ); + + test("should be correct session instance when sessionType is 'one-time-payment'", () => { + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBe( + mockCardFieldsOneTimePaymentSession, + ); + expect(result.current.cardFieldsSession).not.toBe( + mockCardFieldsSavePaymentSession, + ); + }); + + test("should be correct session instance when sessionType is 'save-payment'", () => { + const { result } = renderCardFieldsProvider({ + sessionType: savePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBe( + mockCardFieldsSavePaymentSession, + ); + expect(result.current.cardFieldsSession).not.toBe( + mockCardFieldsOneTimePaymentSession, + ); + }); + }); + + describe("session lifecycle", () => { + test("should update the session when the provider re-runs with a new sdkInstance", () => { + // Initial render with first mockSdkInstance + const { result, rerender } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBe( + mockCardFieldsOneTimePaymentSession, + ); + + // Create a new mockSdkInstance with different session instances + jest.clearAllMocks(); + const newMockCardFieldsOneTimePaymentSession = + createMockOneTimePaymentSession(); + const newMockCardFieldsSavePaymentSession = + createMockSavePaymentSession(); + const newMockSdkInstance = createMockSdkInstance({ + cardFieldsOneTimePaymentSession: + newMockCardFieldsOneTimePaymentSession, + cardFieldsSavePaymentSession: + newMockCardFieldsSavePaymentSession, + }); + + mockUsePayPal.mockReturnValue({ + // @ts-expect-error mocking sdk instance + sdkInstance: newMockSdkInstance, + loadingStatus: INSTANCE_LOADING_STATE.RESOLVED, + eligiblePaymentMethods: null, + error: null, + }); + + rerender(); + expect( + newMockSdkInstance.createCardFieldsOneTimePaymentSession, + ).toHaveBeenCalledTimes(1); + expect( + newMockSdkInstance.createCardFieldsSavePaymentSession, + ).not.toHaveBeenCalled(); + expect(result.current.cardFieldsSession).toBe( + newMockCardFieldsOneTimePaymentSession, + ); + }); + + test("should update the session when the provider re-runs with a new sessionType", () => { + // Initial render with one-time-payment + const { result, rerender } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBe( + mockCardFieldsOneTimePaymentSession, + ); + expect(result.current.cardFieldsSession).not.toBe( + mockCardFieldsSavePaymentSession, + ); + + // Rerender with save-payment + rerender({ sessionType: savePaymentSessionType }); + + expect(result.current.cardFieldsSession).toBe( + mockCardFieldsSavePaymentSession, + ); + expect(result.current.cardFieldsSession).not.toBe( + mockCardFieldsOneTimePaymentSession, + ); + }); + }); +}); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx new file mode 100644 index 00000000..fa9a147f --- /dev/null +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -0,0 +1,89 @@ +import React, { JSX, ReactNode, useEffect, useMemo, useState } from "react"; +import { usePayPal } from "../hooks/usePayPal"; +import { + CardFieldsOneTimePaymentSession, + CardFieldsSavePaymentSession, +} from "../types"; +import { + CardFieldsContext, + CardFieldsState, +} from "../context/CardFieldsContext"; +import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; +import { useError } from "../hooks/useError"; +import { toError } from "../utils"; + +export type sessionType = "one-time-payment" | "save-payment"; + +export type CardFieldsSession = + | CardFieldsOneTimePaymentSession + | CardFieldsSavePaymentSession; + +export type CardFieldsProviderProps = { + children: ReactNode; + sessionType: sessionType; +}; + +/** + * {@link CardFieldsProvider} creates the appropriate Card Fields session based on the {@link sessionType} prop value, and then provides it to child components that require it + * + * @example + * + * + * + */ +export const CardFieldsProvider = ({ + children, + sessionType, +}: CardFieldsProviderProps): JSX.Element => { + const { sdkInstance, loadingStatus } = usePayPal(); + const [cardFieldsSession, setCardFieldsSession] = + useState(null); + const [cardFieldsError, setCardFieldsError] = useState(null); + // Using the error hook here so it can participate in side-effects provided by the hook. + // The actual error instance is stored in the provider's state. + const [, setError] = useError(); + + useEffect(() => { + // Early return: Still loading, wait for sdkInstance + if (loadingStatus === INSTANCE_LOADING_STATE.PENDING) { + return; + } + + // Error case: Loading finished but no sdkInstance + if (!sdkInstance) { + const errorMsg = toError("no sdk instance available"); + setError(errorMsg); + setCardFieldsError(errorMsg); + return; + } + + // Clear previous sdkInstance loading errors + setError(null); + setCardFieldsError(null); + + // Create Card Fields session based on sessionType + setCardFieldsSession( + sessionType === "one-time-payment" + ? sdkInstance.createCardFieldsOneTimePaymentSession() + : sdkInstance.createCardFieldsSavePaymentSession(), + ); + + return () => { + setCardFieldsSession(null); + }; + }, [sdkInstance, loadingStatus, sessionType]); + + const contextValue: CardFieldsState = useMemo( + () => ({ + cardFieldsSession, + cardFieldsError, + }), + [cardFieldsSession, cardFieldsError], + ); + + return ( + + {children} + + ); +}; diff --git a/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx b/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx new file mode 100644 index 00000000..865e2fa7 --- /dev/null +++ b/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx @@ -0,0 +1,9 @@ +import { createContext } from "react"; +import { CardFieldsSession } from "../components/CardFieldsProvider"; + +export interface CardFieldsState { + cardFieldsSession: CardFieldsSession | null; + cardFieldsError: Error | null; +} + +export const CardFieldsContext = createContext(null); diff --git a/packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts b/packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts new file mode 100644 index 00000000..330fc4f3 --- /dev/null +++ b/packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts @@ -0,0 +1,13 @@ +import { renderHook } from "@testing-library/react-hooks"; + +import { useCardFields } from "./useCardFields"; + +describe("useCardFields", () => { + test("should throw an error when used without CardFieldsProvider", () => { + const { result } = renderHook(() => useCardFields()); + + expect(result.error).toEqual( + new Error("useCardFields must be used within a CardFieldsProvider"), + ); + }); +}); diff --git a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts new file mode 100644 index 00000000..d578d4a6 --- /dev/null +++ b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts @@ -0,0 +1,23 @@ +import { CardFieldsContext } from "../context/CardFieldsContext"; + +import { useContext } from "react"; + +import type { CardFieldsProvider } from "../components/CardFieldsProvider"; +import type { CardFieldsState } from "../context/CardFieldsContext"; + +/** + * Returns {@link CardFieldsContext} provided by a parent {@link CardFieldsProvider} + * + * @returns {CardFieldsContext} + */ +export function useCardFields(): CardFieldsState { + const context = useContext(CardFieldsContext); + + if (context === null) { + throw new Error( + "useCardFields must be used within a CardFieldsProvider", + ); + } + + return context; +} From 6a81a7483bc566d30e462778df20ca9ff742f846 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Thu, 4 Dec 2025 17:31:50 -0800 Subject: [PATCH 02/18] Fixed lint and format issues --- .../v6/components/CardFieldsProvider.ssr.test.tsx | 2 +- .../src/v6/components/CardFieldsProvider.test.tsx | 13 +++++++------ .../src/v6/components/CardFieldsProvider.tsx | 3 ++- .../src/v6/context/CardFieldsContext.tsx | 1 + .../react-paypal-js/src/v6/hooks/useCardFields.ts | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx index 7e56ff59..f6379c0b 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx @@ -3,6 +3,7 @@ */ import React from "react"; +import { renderToString } from "react-dom/server"; import { usePayPal } from "../hooks/usePayPal"; import { CardFieldsState } from "../context/CardFieldsContext"; @@ -10,7 +11,6 @@ import { useCardFields } from "../hooks/useCardFields"; import { INSTANCE_LOADING_STATE } from "../types"; import { isServer } from "../utils"; import { CardFieldsProvider, sessionType } from "./CardFieldsProvider"; -import { renderToString } from "react-dom/server"; jest.mock("../hooks/usePayPal"); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx index 6dbaf977..655ec666 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx @@ -1,16 +1,17 @@ -import { usePayPal } from "../hooks/usePayPal"; import React from "react"; -import { INSTANCE_LOADING_STATE } from "../types"; +import { renderHook } from "@testing-library/react-hooks"; +import { usePayPal } from "../hooks/usePayPal"; +import { INSTANCE_LOADING_STATE } from "../types"; import { expectCurrentErrorValue } from "../hooks/useErrorTestUtil"; +import { CardFieldsProvider, sessionType } from "./CardFieldsProvider"; +import { useCardFields } from "../hooks/useCardFields"; +import { toError } from "../utils"; + import type { CardFieldsOneTimePaymentSession, CardFieldsSavePaymentSession, } from "../types"; -import { CardFieldsProvider, sessionType } from "./CardFieldsProvider"; -import { renderHook } from "@testing-library/react-hooks"; -import { useCardFields } from "../hooks/useCardFields"; -import { toError } from "../utils"; jest.mock("../hooks/usePayPal"); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index fa9a147f..da59cc26 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -1,4 +1,5 @@ import React, { JSX, ReactNode, useEffect, useMemo, useState } from "react"; + import { usePayPal } from "../hooks/usePayPal"; import { CardFieldsOneTimePaymentSession, @@ -71,7 +72,7 @@ export const CardFieldsProvider = ({ return () => { setCardFieldsSession(null); }; - }, [sdkInstance, loadingStatus, sessionType]); + }, [sdkInstance, loadingStatus, sessionType, setError]); const contextValue: CardFieldsState = useMemo( () => ({ diff --git a/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx b/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx index 865e2fa7..f8630b88 100644 --- a/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx +++ b/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx @@ -1,4 +1,5 @@ import { createContext } from "react"; + import { CardFieldsSession } from "../components/CardFieldsProvider"; export interface CardFieldsState { diff --git a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts index d578d4a6..d4f04075 100644 --- a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts +++ b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts @@ -1,7 +1,7 @@ -import { CardFieldsContext } from "../context/CardFieldsContext"; - import { useContext } from "react"; +import { CardFieldsContext } from "../context/CardFieldsContext"; + import type { CardFieldsProvider } from "../components/CardFieldsProvider"; import type { CardFieldsState } from "../context/CardFieldsContext"; From 0139c172cff6e1e220dee88bb232ee113f3f288b Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Thu, 4 Dec 2025 18:45:21 -0800 Subject: [PATCH 03/18] Exported CardFieldsProvider component only --- packages/react-paypal-js/src/v6/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-paypal-js/src/v6/index.ts b/packages/react-paypal-js/src/v6/index.ts index fc2ccdd9..5b7999cf 100644 --- a/packages/react-paypal-js/src/v6/index.ts +++ b/packages/react-paypal-js/src/v6/index.ts @@ -2,6 +2,7 @@ export * from "./types"; export { PayPalOneTimePaymentButton } from "./components/PayPalOneTimePaymentButton"; export { PayPalProvider } from "./components/PayPalProvider"; export { usePayPal } from "./hooks/usePayPal"; +export { CardFieldsProvider } from "./components/CardFieldsProvider"; export { usePayLaterOneTimePaymentSession } from "./hooks/usePayLaterOneTimePaymentSession"; export { usePayPalOneTimePaymentSession } from "./hooks/usePayPalOneTimePaymentSession"; export { usePayPalSavePaymentSession } from "./hooks/usePayPalSavePaymentSession"; From 51dd0c5f6e35c73de939d3f605df2e483b32d433 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Thu, 4 Dec 2025 18:52:01 -0800 Subject: [PATCH 04/18] Created changeset --- .changeset/shiny-paws-allow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-paws-allow.md diff --git a/.changeset/shiny-paws-allow.md b/.changeset/shiny-paws-allow.md new file mode 100644 index 00000000..ceacb6e1 --- /dev/null +++ b/.changeset/shiny-paws-allow.md @@ -0,0 +1,5 @@ +--- +"@paypal/react-paypal-js": patch +--- + +Created CardFieldsProvider and context for creating and providing Card Fields sessions From 2dd02be9e15c69113aa2e1f8ae111ea12865450a Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Fri, 5 Dec 2025 12:10:44 -0800 Subject: [PATCH 05/18] Added try catch and error handling for card fields session creation --- .../src/v6/components/CardFieldsProvider.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index da59cc26..43635a40 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -63,11 +63,17 @@ export const CardFieldsProvider = ({ setCardFieldsError(null); // Create Card Fields session based on sessionType - setCardFieldsSession( - sessionType === "one-time-payment" - ? sdkInstance.createCardFieldsOneTimePaymentSession() - : sdkInstance.createCardFieldsSavePaymentSession(), - ); + try { + setCardFieldsSession( + sessionType === "one-time-payment" + ? sdkInstance.createCardFieldsOneTimePaymentSession() + : sdkInstance.createCardFieldsSavePaymentSession(), + ); + } catch (error) { + const errorMsg = toError(error); + setError(errorMsg); + setCardFieldsError(errorMsg); + } return () => { setCardFieldsSession(null); From 5c99ce19525fdf98271f80317a48d513c3e7ab2a Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Fri, 5 Dec 2025 12:18:19 -0800 Subject: [PATCH 06/18] Made card fields session creation more readable --- .../src/v6/components/CardFieldsProvider.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index 43635a40..5b6f0939 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -64,11 +64,12 @@ export const CardFieldsProvider = ({ // Create Card Fields session based on sessionType try { - setCardFieldsSession( + const newCardFieldsSession = sessionType === "one-time-payment" ? sdkInstance.createCardFieldsOneTimePaymentSession() - : sdkInstance.createCardFieldsSavePaymentSession(), - ); + : sdkInstance.createCardFieldsSavePaymentSession(); + + setCardFieldsSession(newCardFieldsSession); } catch (error) { const errorMsg = toError(error); setError(errorMsg); From 623c0b8d489701e9213310ecbdad8c51891dbcde Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Fri, 5 Dec 2025 12:34:36 -0800 Subject: [PATCH 07/18] Added tests for testing card fields session creation error handling --- .../v6/components/CardFieldsProvider.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx index 655ec666..ef81826a 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx @@ -171,6 +171,26 @@ describe("CardFieldsProvider", () => { expect(result.current.cardFieldsError).toBeNull(); expectCurrentErrorValue(null); }); + + test("should handle errors when creating the session", () => { + const errorMessage = "Failed to create session"; + + mockSdkInstance.createCardFieldsOneTimePaymentSession.mockImplementationOnce( + () => { + throw new Error(errorMessage); + }, + ); + + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + expect(result.current.cardFieldsSession).toBeNull(); + expect(result.current.cardFieldsError).toEqual( + toError(errorMessage), + ); + expectCurrentErrorValue(result.current.cardFieldsError); + }); }); describe("sessionType", () => { From d4bd4fca660d0e94874133b4c55e8dfc53838b2d Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Fri, 5 Dec 2025 13:58:19 -0800 Subject: [PATCH 08/18] Added small expect to test --- .../src/v6/components/CardFieldsProvider.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx index ef81826a..35e70257 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx @@ -293,6 +293,9 @@ describe("CardFieldsProvider", () => { expect(result.current.cardFieldsSession).toBe( newMockCardFieldsOneTimePaymentSession, ); + expect(result.current.cardFieldsSession).not.toBe( + mockCardFieldsOneTimePaymentSession, + ); }); test("should update the session when the provider re-runs with a new sessionType", () => { From 41229fd4d7a9b2f9ec4d6228caf9bbdd50991eff Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Fri, 5 Dec 2025 15:17:35 -0800 Subject: [PATCH 09/18] Separated sdkInstance loadingStatus validation into its own hook to follow stablished pattern --- .../src/v6/components/CardFieldsProvider.tsx | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index 5b6f0939..6bc522fb 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -1,4 +1,11 @@ -import React, { JSX, ReactNode, useEffect, useMemo, useState } from "react"; +import React, { + JSX, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { usePayPal } from "../hooks/usePayPal"; import { @@ -44,25 +51,29 @@ export const CardFieldsProvider = ({ // The actual error instance is stored in the provider's state. const [, setError] = useError(); + const handleError = useCallback( + (error: Error | null) => { + setError(error); + setCardFieldsError(error); + }, + [setError], + ); + + // Effect for reporting sdkInstance availability errors useEffect(() => { - // Early return: Still loading, wait for sdkInstance - if (loadingStatus === INSTANCE_LOADING_STATE.PENDING) { - return; + if (sdkInstance) { + handleError(null); + } else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { + handleError(toError("no sdk instance available")); } + }, [sdkInstance, loadingStatus, handleError]); - // Error case: Loading finished but no sdkInstance + // Effect for creating Card Fields session based on sessionType + useEffect(() => { if (!sdkInstance) { - const errorMsg = toError("no sdk instance available"); - setError(errorMsg); - setCardFieldsError(errorMsg); return; } - // Clear previous sdkInstance loading errors - setError(null); - setCardFieldsError(null); - - // Create Card Fields session based on sessionType try { const newCardFieldsSession = sessionType === "one-time-payment" @@ -70,16 +81,15 @@ export const CardFieldsProvider = ({ : sdkInstance.createCardFieldsSavePaymentSession(); setCardFieldsSession(newCardFieldsSession); + handleError(null); } catch (error) { - const errorMsg = toError(error); - setError(errorMsg); - setCardFieldsError(errorMsg); + handleError(toError(error)); } return () => { setCardFieldsSession(null); }; - }, [sdkInstance, loadingStatus, sessionType, setError]); + }, [sdkInstance, sessionType, handleError]); const contextValue: CardFieldsState = useMemo( () => ({ From 7ec806524d1b706879c6d098860daf80ba4934bb Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Fri, 5 Dec 2025 16:23:00 -0800 Subject: [PATCH 10/18] Unified two use Effects into single one --- .../src/v6/components/CardFieldsProvider.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index 6bc522fb..b1a418e8 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -59,21 +59,22 @@ export const CardFieldsProvider = ({ [setError], ); - // Effect for reporting sdkInstance availability errors useEffect(() => { - if (sdkInstance) { - handleError(null); - } else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { - handleError(toError("no sdk instance available")); + // Early return: Still loading, wait for sdkInstance + if (loadingStatus === INSTANCE_LOADING_STATE.PENDING) { + return; } - }, [sdkInstance, loadingStatus, handleError]); - // Effect for creating Card Fields session based on sessionType - useEffect(() => { + // Error case: Loading finished but no sdkInstance if (!sdkInstance) { + handleError(toError("no sdk instance available")); return; } + // Clear previous sdkInstance loading errors + handleError(null); + + // Create Card Fields session based on sessionType try { const newCardFieldsSession = sessionType === "one-time-payment" @@ -81,7 +82,6 @@ export const CardFieldsProvider = ({ : sdkInstance.createCardFieldsSavePaymentSession(); setCardFieldsSession(newCardFieldsSession); - handleError(null); } catch (error) { handleError(toError(error)); } @@ -89,7 +89,7 @@ export const CardFieldsProvider = ({ return () => { setCardFieldsSession(null); }; - }, [sdkInstance, sessionType, handleError]); + }, [sdkInstance, loadingStatus, sessionType, handleError]); const contextValue: CardFieldsState = useMemo( () => ({ From c0c311137fcccafab78bd0e57b282a895789eb1b Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Mon, 8 Dec 2025 10:23:04 -0800 Subject: [PATCH 11/18] Rename CardFieldsContext file --- .../src/v6/components/CardFieldsProvider.ssr.test.tsx | 2 +- .../react-paypal-js/src/v6/components/CardFieldsProvider.tsx | 2 +- .../{CardFieldsContext.tsx => CardFieldsProviderContext.tsx} | 0 packages/react-paypal-js/src/v6/hooks/useCardFields.ts | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename packages/react-paypal-js/src/v6/context/{CardFieldsContext.tsx => CardFieldsProviderContext.tsx} (100%) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx index f6379c0b..c1332303 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx @@ -6,7 +6,7 @@ import React from "react"; import { renderToString } from "react-dom/server"; import { usePayPal } from "../hooks/usePayPal"; -import { CardFieldsState } from "../context/CardFieldsContext"; +import { CardFieldsState } from "../context/CardFieldsProviderContext"; import { useCardFields } from "../hooks/useCardFields"; import { INSTANCE_LOADING_STATE } from "../types"; import { isServer } from "../utils"; diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index b1a418e8..ba3aaee9 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -15,7 +15,7 @@ import { import { CardFieldsContext, CardFieldsState, -} from "../context/CardFieldsContext"; +} from "../context/CardFieldsProviderContext"; import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import { useError } from "../hooks/useError"; import { toError } from "../utils"; diff --git a/packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx b/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx similarity index 100% rename from packages/react-paypal-js/src/v6/context/CardFieldsContext.tsx rename to packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx diff --git a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts index d4f04075..abef802d 100644 --- a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts +++ b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts @@ -1,9 +1,9 @@ import { useContext } from "react"; -import { CardFieldsContext } from "../context/CardFieldsContext"; +import { CardFieldsContext } from "../context/CardFieldsProviderContext"; import type { CardFieldsProvider } from "../components/CardFieldsProvider"; -import type { CardFieldsState } from "../context/CardFieldsContext"; +import type { CardFieldsState } from "../context/CardFieldsProviderContext"; /** * Returns {@link CardFieldsContext} provided by a parent {@link CardFieldsProvider} From b7d812629c1848445d7be967f9def6cb3dbb674b Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Mon, 8 Dec 2025 10:50:30 -0800 Subject: [PATCH 12/18] Changed sessionType to CardFieldsSessionType and exported it --- .../src/v6/components/CardFieldsProvider.ssr.test.tsx | 6 ++++-- .../src/v6/components/CardFieldsProvider.test.tsx | 9 +++++---- .../src/v6/components/CardFieldsProvider.tsx | 6 +++--- packages/react-paypal-js/src/v6/types/index.ts | 2 ++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx index c1332303..a91b6f7f 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx @@ -10,7 +10,9 @@ import { CardFieldsState } from "../context/CardFieldsProviderContext"; import { useCardFields } from "../hooks/useCardFields"; import { INSTANCE_LOADING_STATE } from "../types"; import { isServer } from "../utils"; -import { CardFieldsProvider, sessionType } from "./CardFieldsProvider"; +import { CardFieldsProvider } from "./CardFieldsProvider"; + +import type { CardFieldsSessionType } from "../types"; jest.mock("../hooks/usePayPal"); @@ -32,7 +34,7 @@ function renderSSRCardFieldsProvider({ sessionType, children, }: { - sessionType: sessionType; + sessionType: CardFieldsSessionType; children?: React.ReactNode; }) { const { cardFieldsState, TestComponent } = setupSSRTestComponent(); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx index 35e70257..b7c127be 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx @@ -4,13 +4,14 @@ import { renderHook } from "@testing-library/react-hooks"; import { usePayPal } from "../hooks/usePayPal"; import { INSTANCE_LOADING_STATE } from "../types"; import { expectCurrentErrorValue } from "../hooks/useErrorTestUtil"; -import { CardFieldsProvider, sessionType } from "./CardFieldsProvider"; +import { CardFieldsProvider } from "./CardFieldsProvider"; import { useCardFields } from "../hooks/useCardFields"; import { toError } from "../utils"; import type { CardFieldsOneTimePaymentSession, CardFieldsSavePaymentSession, + CardFieldsSessionType, } from "../types"; jest.mock("../hooks/usePayPal"); @@ -22,8 +23,8 @@ jest.mock("../utils", () => ({ const mockUsePayPal = usePayPal as jest.MockedFunction; -const oneTimePaymentSessionType: sessionType = "one-time-payment"; -const savePaymentSessionType: sessionType = "save-payment"; +const oneTimePaymentSessionType: CardFieldsSessionType = "one-time-payment"; +const savePaymentSessionType: CardFieldsSessionType = "save-payment"; // Mock Factories const createMockOneTimePaymentSession = @@ -60,7 +61,7 @@ const createMockSdkInstance = ({ function renderCardFieldsProvider({ sessionType, }: { - sessionType: sessionType; + sessionType: CardFieldsSessionType; }) { return renderHook(() => useCardFields(), { initialProps: { sessionType }, diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index ba3aaee9..db9288c5 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -20,7 +20,7 @@ import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import { useError } from "../hooks/useError"; import { toError } from "../utils"; -export type sessionType = "one-time-payment" | "save-payment"; +import type { CardFieldsSessionType } from "../types"; export type CardFieldsSession = | CardFieldsOneTimePaymentSession @@ -28,11 +28,11 @@ export type CardFieldsSession = export type CardFieldsProviderProps = { children: ReactNode; - sessionType: sessionType; + sessionType: CardFieldsSessionType; }; /** - * {@link CardFieldsProvider} creates the appropriate Card Fields session based on the {@link sessionType} prop value, and then provides it to child components that require it + * {@link CardFieldsProvider} creates the appropriate Card Fields session based on the `sessionType` prop value, and then provides it to child components that require it * * @example * diff --git a/packages/react-paypal-js/src/v6/types/index.ts b/packages/react-paypal-js/src/v6/types/index.ts index 027fe8e2..2758f1fa 100644 --- a/packages/react-paypal-js/src/v6/types/index.ts +++ b/packages/react-paypal-js/src/v6/types/index.ts @@ -9,3 +9,5 @@ export interface BasePaymentSessionReturn { handleCancel: () => void; handleDestroy: () => void; } + +export type CardFieldsSessionType = "one-time-payment" | "save-payment"; From 37bd290e0fc3a8388b62bd1f9730fb4235793068 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Mon, 8 Dec 2025 11:20:21 -0800 Subject: [PATCH 13/18] Updated type imports to 'import type' --- .../src/v6/components/CardFieldsProvider.ssr.test.tsx | 2 +- .../src/v6/components/CardFieldsProvider.tsx | 8 +++----- .../src/v6/context/CardFieldsProviderContext.tsx | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx index a91b6f7f..dfcba668 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx @@ -6,12 +6,12 @@ import React from "react"; import { renderToString } from "react-dom/server"; import { usePayPal } from "../hooks/usePayPal"; -import { CardFieldsState } from "../context/CardFieldsProviderContext"; import { useCardFields } from "../hooks/useCardFields"; import { INSTANCE_LOADING_STATE } from "../types"; import { isServer } from "../utils"; import { CardFieldsProvider } from "./CardFieldsProvider"; +import type { CardFieldsState } from "../context/CardFieldsProviderContext"; import type { CardFieldsSessionType } from "../types"; jest.mock("../hooks/usePayPal"); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index db9288c5..fc6d2a8f 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -12,21 +12,19 @@ import { CardFieldsOneTimePaymentSession, CardFieldsSavePaymentSession, } from "../types"; -import { - CardFieldsContext, - CardFieldsState, -} from "../context/CardFieldsProviderContext"; +import { CardFieldsContext } from "../context/CardFieldsProviderContext"; import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import { useError } from "../hooks/useError"; import { toError } from "../utils"; +import type { CardFieldsState } from "../context/CardFieldsProviderContext"; import type { CardFieldsSessionType } from "../types"; export type CardFieldsSession = | CardFieldsOneTimePaymentSession | CardFieldsSavePaymentSession; -export type CardFieldsProviderProps = { +type CardFieldsProviderProps = { children: ReactNode; sessionType: CardFieldsSessionType; }; diff --git a/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx b/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx index f8630b88..c2f54fff 100644 --- a/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx +++ b/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx @@ -1,6 +1,6 @@ import { createContext } from "react"; -import { CardFieldsSession } from "../components/CardFieldsProvider"; +import type { CardFieldsSession } from "../components/CardFieldsProvider"; export interface CardFieldsState { cardFieldsSession: CardFieldsSession | null; From 58fd9d6d1e81e874e0a76424cbed1932d8a80e27 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Mon, 8 Dec 2025 11:31:29 -0800 Subject: [PATCH 14/18] Updated more import type --- .../src/v6/components/CardFieldsProvider.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index fc6d2a8f..235e7d59 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -8,17 +8,17 @@ import React, { } from "react"; import { usePayPal } from "../hooks/usePayPal"; -import { - CardFieldsOneTimePaymentSession, - CardFieldsSavePaymentSession, -} from "../types"; import { CardFieldsContext } from "../context/CardFieldsProviderContext"; import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import { useError } from "../hooks/useError"; import { toError } from "../utils"; +import type { + CardFieldsOneTimePaymentSession, + CardFieldsSavePaymentSession, + CardFieldsSessionType, +} from "../types"; import type { CardFieldsState } from "../context/CardFieldsProviderContext"; -import type { CardFieldsSessionType } from "../types"; export type CardFieldsSession = | CardFieldsOneTimePaymentSession From c2a5cfce396c0a71641deff243e50a0d099c78d6 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Mon, 8 Dec 2025 18:25:49 -0800 Subject: [PATCH 15/18] Split provider context into 2 --- .../CardFieldsProvider.ssr.test.tsx | 188 +++++++++++++----- .../v6/components/CardFieldsProvider.test.tsx | 127 +++++++++--- .../src/v6/components/CardFieldsProvider.tsx | 30 ++- .../v6/context/CardFieldsProviderContext.tsx | 11 +- .../src/v6/hooks/useCardFields.test.ts | 14 +- .../src/v6/hooks/useCardFields.ts | 35 +++- packages/react-paypal-js/src/v6/index.ts | 3 +- 7 files changed, 309 insertions(+), 99 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx index dfcba668..4b5c7e49 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx @@ -6,12 +6,15 @@ import React from "react"; import { renderToString } from "react-dom/server"; import { usePayPal } from "../hooks/usePayPal"; -import { useCardFields } from "../hooks/useCardFields"; +import { useCardFields, useCardFieldsSession } from "../hooks/useCardFields"; import { INSTANCE_LOADING_STATE } from "../types"; import { isServer } from "../utils"; import { CardFieldsProvider } from "./CardFieldsProvider"; -import type { CardFieldsState } from "../context/CardFieldsProviderContext"; +import type { + CardFieldsSessionState, + CardFieldsStatusState, +} from "../context/CardFieldsProviderContext"; import type { CardFieldsSessionType } from "../types"; jest.mock("../hooks/usePayPal"); @@ -23,12 +26,18 @@ jest.mock("../utils", () => ({ const mockUsePayPal = usePayPal as jest.MockedFunction; +const oneTimePaymentSessionType: CardFieldsSessionType = "one-time-payment"; +const savePaymentSessionType: CardFieldsSessionType = "save-payment"; + // Test utilites -function expectInitialState(state: Partial): void { - expect(state.cardFieldsSession).toBe(null); +function expectInitialStatusState(state: CardFieldsStatusState): void { expect(state.cardFieldsError).toBe(null); } +function expectInitialSessionState(state: CardFieldsSessionState): void { + expect(state.cardFieldsSession).toBe(null); +} + // Render helpers function renderSSRCardFieldsProvider({ sessionType, @@ -37,7 +46,8 @@ function renderSSRCardFieldsProvider({ sessionType: CardFieldsSessionType; children?: React.ReactNode; }) { - const { cardFieldsState, TestComponent } = setupSSRTestComponent(); + const { cardFieldsSessionState, cardFieldsStatusState, TestComponent } = + setupSSRTestComponent(); const html = renderToString( @@ -45,31 +55,33 @@ function renderSSRCardFieldsProvider({ , ); - return { html, cardFieldsState }; + return { html, cardFieldsSessionState, cardFieldsStatusState }; } function setupSSRTestComponent() { - const cardFieldsState: CardFieldsState = { - cardFieldsSession: null, + const cardFieldsStatusState: CardFieldsStatusState = { cardFieldsError: null, }; + const cardFieldsSessionState: CardFieldsSessionState = { + cardFieldsSession: null, + }; + function TestComponent({ children = null, }: { children?: React.ReactNode; }) { - try { - const newCardFieldsState = useCardFields(); - Object.assign(cardFieldsState, newCardFieldsState); - } catch (error) { - cardFieldsState.cardFieldsError = error as Error; - } + const newCardFieldsStatusState = useCardFields(); + const newCardFieldsSessionState = useCardFieldsSession(); + + Object.assign(cardFieldsStatusState, newCardFieldsStatusState); + Object.assign(cardFieldsSessionState, newCardFieldsSessionState); return <>{children}; } - return { cardFieldsState, TestComponent }; + return { cardFieldsStatusState, cardFieldsSessionState, TestComponent }; } describe("CardFieldsProvider SSR", () => { @@ -98,17 +110,19 @@ describe("CardFieldsProvider SSR", () => { // Will throw error if any DOM access is attempted during render expect(() => renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", + sessionType: oneTimePaymentSessionType, }), ).not.toThrow(); }); test("should work correctly in SSR context", () => { - const { cardFieldsState } = renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", - }); + const { cardFieldsSessionState, cardFieldsStatusState } = + renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); - expectInitialState(cardFieldsState); + expectInitialStatusState(cardFieldsStatusState); + expectInitialSessionState(cardFieldsSessionState); }); test("should render without warnings or errors", () => { @@ -125,7 +139,7 @@ describe("CardFieldsProvider SSR", () => { .mockImplementation(); const { html } = renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", + sessionType: oneTimePaymentSessionType, children:
SSR Content
, }); @@ -140,56 +154,81 @@ describe("CardFieldsProvider SSR", () => { describe("Hydration Preparation", () => { test("should prepare serializable state for client hydration", () => { - const { cardFieldsState } = renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", - }); + const { cardFieldsSessionState, cardFieldsStatusState } = + renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); // Server state should be safe for serialization - const serializedState = JSON.stringify(cardFieldsState); + const serializedSessionState = JSON.stringify( + cardFieldsSessionState, + ); + const serializedStatusState = JSON.stringify(cardFieldsStatusState); - expect(() => JSON.parse(serializedState)).not.toThrow(); + expect(() => JSON.parse(serializedSessionState)).not.toThrow(); + expect(() => JSON.parse(serializedStatusState)).not.toThrow(); - const parsedState = JSON.parse(serializedState); - expectInitialState(parsedState); + const parsedSessionState = JSON.parse(serializedSessionState); + const parsedStatusState = JSON.parse(serializedStatusState); + + expectInitialSessionState(parsedSessionState); + expectInitialStatusState(parsedStatusState); }); test("should maintain consistent state with different options", () => { - const { cardFieldsState: cardFieldsState1 } = - renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", - }); - const { cardFieldsState: cardFieldsState2 } = - renderSSRCardFieldsProvider({ sessionType: "save-payment" }); + const { + cardFieldsStatusState: cardFieldsStatusState1, + cardFieldsSessionState: cardFieldsSessionState1, + } = renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + const { + cardFieldsStatusState: cardFieldsStatusState2, + cardFieldsSessionState: cardFieldsSessionState2, + } = renderSSRCardFieldsProvider({ + sessionType: savePaymentSessionType, + }); // Both should have consistent initial state regardless of options - expectInitialState(cardFieldsState1); - expectInitialState(cardFieldsState2); + expectInitialStatusState(cardFieldsStatusState1); + expectInitialSessionState(cardFieldsSessionState1); + + expectInitialStatusState(cardFieldsStatusState2); + expectInitialSessionState(cardFieldsSessionState2); }); }); describe("Multiple renders", () => { test("should maintain state consistency across multiple server renders", () => { - const { html: html1, cardFieldsState: cardFieldsState1 } = - renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", - }); - const { html: html2, cardFieldsState: cardFieldsState2 } = - renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", - }); + const { + html: html1, + cardFieldsStatusState: cardFieldsStatusState1, + cardFieldsSessionState: cardFieldsSessionState1, + } = renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + const { + html: html2, + cardFieldsStatusState: cardFieldsStatusState2, + cardFieldsSessionState: cardFieldsSessionState2, + } = renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); - expectInitialState(cardFieldsState1); - expectInitialState(cardFieldsState2); + expectInitialStatusState(cardFieldsStatusState1); + expectInitialSessionState(cardFieldsSessionState1); + expectInitialStatusState(cardFieldsStatusState2); + expectInitialSessionState(cardFieldsSessionState2); expect(html1).toBe(html2); }); test("should generate consistent HTML across renders", () => { const { html: html1 } = renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", + sessionType: oneTimePaymentSessionType, children:
Test Content
, }); const { html: html2 } = renderSSRCardFieldsProvider({ - sessionType: "one-time-payment", + sessionType: oneTimePaymentSessionType, children:
Test Content
, }); @@ -197,4 +236,57 @@ describe("CardFieldsProvider SSR", () => { expect(html1).toContain('data-testid="ssr-content"'); }); }); + + describe("Context isolation", () => { + const expectedSessionContextKeys = ["cardFieldsSession"] as const; + const expectedStatusContextKeys = ["cardFieldsError"] as const; + + describe("useCardFields", () => { + test("should only return status context values", () => { + const { cardFieldsStatusState } = renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedStatusKeys = Object.keys(cardFieldsStatusState); + expect(receivedStatusKeys.sort()).toEqual( + [...expectedStatusContextKeys].sort(), + ); + }); + + test("should not return session context values", () => { + const { cardFieldsStatusState } = renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedStatusKeys = Object.keys(cardFieldsStatusState); + expectedSessionContextKeys.forEach((key) => { + expect(receivedStatusKeys).not.toContain(key); + }); + }); + }); + + describe("useCardFieldsSession", () => { + test("should only return session context values", () => { + const { cardFieldsSessionState } = renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedSessionKeys = Object.keys(cardFieldsSessionState); + expect(receivedSessionKeys.sort()).toEqual( + [...expectedSessionContextKeys].sort(), + ); + }); + + test("should not return status context values", () => { + const { cardFieldsSessionState } = renderSSRCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedSessionKeys = Object.keys(cardFieldsSessionState); + expectedStatusContextKeys.forEach((key) => { + expect(receivedSessionKeys).not.toContain(key); + }); + }); + }); + }); }); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx index b7c127be..f95bf473 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx @@ -5,7 +5,7 @@ import { usePayPal } from "../hooks/usePayPal"; import { INSTANCE_LOADING_STATE } from "../types"; import { expectCurrentErrorValue } from "../hooks/useErrorTestUtil"; import { CardFieldsProvider } from "./CardFieldsProvider"; -import { useCardFields } from "../hooks/useCardFields"; +import { useCardFields, useCardFieldsSession } from "../hooks/useCardFields"; import { toError } from "../utils"; import type { @@ -63,14 +63,20 @@ function renderCardFieldsProvider({ }: { sessionType: CardFieldsSessionType; }) { - return renderHook(() => useCardFields(), { - initialProps: { sessionType }, - wrapper: ({ children, sessionType }) => ( - - {children} - - ), - }); + return renderHook( + () => ({ + status: useCardFields(), + session: useCardFieldsSession(), + }), + { + initialProps: { sessionType }, + wrapper: ({ children, sessionType }) => ( + + {children} + + ), + }, + ); } describe("CardFieldsProvider", () => { @@ -113,8 +119,8 @@ describe("CardFieldsProvider", () => { sessionType: oneTimePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBeNull(); - expect(result.current.cardFieldsError).toBeNull(); + expect(result.current.session.cardFieldsSession).toBeNull(); + expect(result.current.status.cardFieldsError).toBeNull(); expectCurrentErrorValue(null); }); @@ -130,11 +136,11 @@ describe("CardFieldsProvider", () => { sessionType: oneTimePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBeNull(); - expect(result.current.cardFieldsError).toEqual( + expect(result.current.session.cardFieldsSession).toBeNull(); + expect(result.current.status.cardFieldsError).toEqual( toError("no sdk instance available"), ); - expectCurrentErrorValue(result.current.cardFieldsError); + expectCurrentErrorValue(result.current.status.cardFieldsError); }); test("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { @@ -150,11 +156,11 @@ describe("CardFieldsProvider", () => { sessionType: oneTimePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBeNull(); - expect(result.current.cardFieldsError).toEqual( + expect(result.current.session.cardFieldsSession).toBeNull(); + expect(result.current.status.cardFieldsError).toEqual( toError("no sdk instance available"), ); - expectCurrentErrorValue(result.current.cardFieldsError); + expectCurrentErrorValue(result.current.status.cardFieldsError); // Second render: sdkInstance becomes available, error should clear mockUsePayPal.mockReturnValue({ @@ -166,10 +172,10 @@ describe("CardFieldsProvider", () => { }); rerender(); - expect(result.current.cardFieldsSession).toBe( + expect(result.current.session.cardFieldsSession).toBe( mockCardFieldsOneTimePaymentSession, ); - expect(result.current.cardFieldsError).toBeNull(); + expect(result.current.status.cardFieldsError).toBeNull(); expectCurrentErrorValue(null); }); @@ -186,11 +192,11 @@ describe("CardFieldsProvider", () => { sessionType: oneTimePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBeNull(); - expect(result.current.cardFieldsError).toEqual( + expect(result.current.session.cardFieldsSession).toBeNull(); + expect(result.current.status.cardFieldsError).toEqual( toError(errorMessage), ); - expectCurrentErrorValue(result.current.cardFieldsError); + expectCurrentErrorValue(result.current.status.cardFieldsError); }); }); @@ -230,10 +236,10 @@ describe("CardFieldsProvider", () => { sessionType: oneTimePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBe( + expect(result.current.session.cardFieldsSession).toBe( mockCardFieldsOneTimePaymentSession, ); - expect(result.current.cardFieldsSession).not.toBe( + expect(result.current.session.cardFieldsSession).not.toBe( mockCardFieldsSavePaymentSession, ); }); @@ -243,10 +249,10 @@ describe("CardFieldsProvider", () => { sessionType: savePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBe( + expect(result.current.session.cardFieldsSession).toBe( mockCardFieldsSavePaymentSession, ); - expect(result.current.cardFieldsSession).not.toBe( + expect(result.current.session.cardFieldsSession).not.toBe( mockCardFieldsOneTimePaymentSession, ); }); @@ -259,7 +265,7 @@ describe("CardFieldsProvider", () => { sessionType: oneTimePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBe( + expect(result.current.session.cardFieldsSession).toBe( mockCardFieldsOneTimePaymentSession, ); @@ -291,10 +297,10 @@ describe("CardFieldsProvider", () => { expect( newMockSdkInstance.createCardFieldsSavePaymentSession, ).not.toHaveBeenCalled(); - expect(result.current.cardFieldsSession).toBe( + expect(result.current.session.cardFieldsSession).toBe( newMockCardFieldsOneTimePaymentSession, ); - expect(result.current.cardFieldsSession).not.toBe( + expect(result.current.session.cardFieldsSession).not.toBe( mockCardFieldsOneTimePaymentSession, ); }); @@ -305,22 +311,77 @@ describe("CardFieldsProvider", () => { sessionType: oneTimePaymentSessionType, }); - expect(result.current.cardFieldsSession).toBe( + expect(result.current.session.cardFieldsSession).toBe( mockCardFieldsOneTimePaymentSession, ); - expect(result.current.cardFieldsSession).not.toBe( + expect(result.current.session.cardFieldsSession).not.toBe( mockCardFieldsSavePaymentSession, ); // Rerender with save-payment rerender({ sessionType: savePaymentSessionType }); - expect(result.current.cardFieldsSession).toBe( + expect(result.current.session.cardFieldsSession).toBe( mockCardFieldsSavePaymentSession, ); - expect(result.current.cardFieldsSession).not.toBe( + expect(result.current.session.cardFieldsSession).not.toBe( mockCardFieldsOneTimePaymentSession, ); }); }); + + describe("Context isolation", () => { + const expectedSessionContextKeys = ["cardFieldsSession"] as const; + const expectedStatusContextKeys = ["cardFieldsError"] as const; + + describe("useCardFields", () => { + test("should only return status context values", () => { + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedStatusKeys = Object.keys(result.current.status); + expect(receivedStatusKeys.sort()).toEqual( + [...expectedStatusContextKeys].sort(), + ); + }); + + test("should not return session context values", () => { + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedStatusKeys = Object.keys(result.current.status); + expectedSessionContextKeys.forEach((key) => { + expect(receivedStatusKeys).not.toContain(key); + }); + }); + }); + + describe("useCardFieldsSession", () => { + test("should only return session context values", () => { + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedSessionKeys = Object.keys(result.current.session); + expect(receivedSessionKeys.sort()).toEqual( + [...expectedSessionContextKeys].sort(), + ); + }); + + test("should not return status context values", () => { + const { result } = renderCardFieldsProvider({ + sessionType: oneTimePaymentSessionType, + }); + + const receivedSesssionKeys = Object.keys( + result.current.session, + ); + expectedStatusContextKeys.forEach((key) => { + expect(receivedSesssionKeys).not.toContain(key); + }); + }); + }); + }); }); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index 235e7d59..c1909fff 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -8,7 +8,10 @@ import React, { } from "react"; import { usePayPal } from "../hooks/usePayPal"; -import { CardFieldsContext } from "../context/CardFieldsProviderContext"; +import { + CardFieldsSessionContext, + CardFieldsStatusContext, +} from "../context/CardFieldsProviderContext"; import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import { useError } from "../hooks/useError"; import { toError } from "../utils"; @@ -18,7 +21,10 @@ import type { CardFieldsSavePaymentSession, CardFieldsSessionType, } from "../types"; -import type { CardFieldsState } from "../context/CardFieldsProviderContext"; +import type { + CardFieldsSessionState, + CardFieldsStatusState, +} from "../context/CardFieldsProviderContext"; export type CardFieldsSession = | CardFieldsOneTimePaymentSession @@ -30,7 +36,7 @@ type CardFieldsProviderProps = { }; /** - * {@link CardFieldsProvider} creates the appropriate Card Fields session based on the `sessionType` prop value, and then provides it to child components that require it + * {@link CardFieldsProvider} creates the appropriate Card Fields session based on the `sessionType` prop value, and then provides it to child components that require it. * * @example * @@ -89,17 +95,25 @@ export const CardFieldsProvider = ({ }; }, [sdkInstance, loadingStatus, sessionType, handleError]); - const contextValue: CardFieldsState = useMemo( + const sessionContextValue: CardFieldsSessionState = useMemo( () => ({ cardFieldsSession, + }), + [cardFieldsSession], + ); + + const statusContextValue: CardFieldsStatusState = useMemo( + () => ({ cardFieldsError, }), - [cardFieldsSession, cardFieldsError], + [cardFieldsError], ); return ( - - {children} - + + + {children} + + ); }; diff --git a/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx b/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx index c2f54fff..998d41c6 100644 --- a/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx +++ b/packages/react-paypal-js/src/v6/context/CardFieldsProviderContext.tsx @@ -2,9 +2,16 @@ import { createContext } from "react"; import type { CardFieldsSession } from "../components/CardFieldsProvider"; -export interface CardFieldsState { +export interface CardFieldsSessionState { cardFieldsSession: CardFieldsSession | null; +} + +export const CardFieldsSessionContext = + createContext(null); + +export interface CardFieldsStatusState { cardFieldsError: Error | null; } -export const CardFieldsContext = createContext(null); +export const CardFieldsStatusContext = + createContext(null); diff --git a/packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts b/packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts index 330fc4f3..ee87259f 100644 --- a/packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/useCardFields.test.ts @@ -1,6 +1,6 @@ import { renderHook } from "@testing-library/react-hooks"; -import { useCardFields } from "./useCardFields"; +import { useCardFields, useCardFieldsSession } from "./useCardFields"; describe("useCardFields", () => { test("should throw an error when used without CardFieldsProvider", () => { @@ -11,3 +11,15 @@ describe("useCardFields", () => { ); }); }); + +describe("useCardFieldsSession", () => { + test("should throw an error when used without CardFieldsProvider", () => { + const { result } = renderHook(() => useCardFieldsSession()); + + expect(result.error).toEqual( + new Error( + "useCardFieldsSession must be used within a CardFieldsProvider", + ), + ); + }); +}); diff --git a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts index abef802d..b1ae60f9 100644 --- a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts +++ b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts @@ -1,17 +1,23 @@ import { useContext } from "react"; -import { CardFieldsContext } from "../context/CardFieldsProviderContext"; +import { + CardFieldsSessionContext, + CardFieldsStatusContext, +} from "../context/CardFieldsProviderContext"; import type { CardFieldsProvider } from "../components/CardFieldsProvider"; -import type { CardFieldsState } from "../context/CardFieldsProviderContext"; +import type { + CardFieldsSessionState, + CardFieldsStatusState, +} from "../context/CardFieldsProviderContext"; /** - * Returns {@link CardFieldsContext} provided by a parent {@link CardFieldsProvider} + * Returns {@link CardFieldsStatusContext} provided by a parent {@link CardFieldsProvider} * - * @returns {CardFieldsContext} + * @returns {CardFieldsStatusContext} */ -export function useCardFields(): CardFieldsState { - const context = useContext(CardFieldsContext); +export function useCardFields(): CardFieldsStatusState { + const context = useContext(CardFieldsStatusContext); if (context === null) { throw new Error( @@ -21,3 +27,20 @@ export function useCardFields(): CardFieldsState { return context; } + +/** + * Returns {@link CardFieldsSessionContext} provided by a parent {@link CardFieldsProvider} + * + * @returns {CardFieldsSessionContext} + */ +export function useCardFieldsSession(): CardFieldsSessionState { + const context = useContext(CardFieldsSessionContext); + + if (context === null) { + throw new Error( + "useCardFieldsSession must be used within a CardFieldsProvider", + ); + } + + return context; +} diff --git a/packages/react-paypal-js/src/v6/index.ts b/packages/react-paypal-js/src/v6/index.ts index 5b7999cf..2807a8c9 100644 --- a/packages/react-paypal-js/src/v6/index.ts +++ b/packages/react-paypal-js/src/v6/index.ts @@ -1,8 +1,9 @@ export * from "./types"; +export { CardFieldsProvider } from "./components/CardFieldsProvider"; export { PayPalOneTimePaymentButton } from "./components/PayPalOneTimePaymentButton"; export { PayPalProvider } from "./components/PayPalProvider"; +export { useCardFields } from "./hooks/useCardFields"; export { usePayPal } from "./hooks/usePayPal"; -export { CardFieldsProvider } from "./components/CardFieldsProvider"; export { usePayLaterOneTimePaymentSession } from "./hooks/usePayLaterOneTimePaymentSession"; export { usePayPalOneTimePaymentSession } from "./hooks/usePayPalOneTimePaymentSession"; export { usePayPalSavePaymentSession } from "./hooks/usePayPalSavePaymentSession"; From 3b2e9ba37be6483d9d36c0ce9a5ebc00ab932dfb Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Tue, 9 Dec 2025 08:29:21 -0800 Subject: [PATCH 16/18] Updated useCardFields comments --- packages/react-paypal-js/src/v6/hooks/useCardFields.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts index b1ae60f9..41edc2f7 100644 --- a/packages/react-paypal-js/src/v6/hooks/useCardFields.ts +++ b/packages/react-paypal-js/src/v6/hooks/useCardFields.ts @@ -12,9 +12,9 @@ import type { } from "../context/CardFieldsProviderContext"; /** - * Returns {@link CardFieldsStatusContext} provided by a parent {@link CardFieldsProvider} + * Returns {@link CardFieldsStatusState} provided by a parent {@link CardFieldsProvider} * - * @returns {CardFieldsStatusContext} + * @returns {CardFieldsStatusState} */ export function useCardFields(): CardFieldsStatusState { const context = useContext(CardFieldsStatusContext); @@ -29,9 +29,9 @@ export function useCardFields(): CardFieldsStatusState { } /** - * Returns {@link CardFieldsSessionContext} provided by a parent {@link CardFieldsProvider} + * Returns {@link CardFieldsSessionState} provided by a parent {@link CardFieldsProvider} * - * @returns {CardFieldsSessionContext} + * @returns {CardFieldsSessionState} */ export function useCardFieldsSession(): CardFieldsSessionState { const context = useContext(CardFieldsSessionContext); From b6c2f708d63db15497048e2ba3ccd04e2199e9c1 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Tue, 9 Dec 2025 12:51:55 -0800 Subject: [PATCH 17/18] Updated CardFieldsProvider types --- .../src/v6/components/CardFieldsProvider.ssr.test.tsx | 2 +- .../src/v6/components/CardFieldsProvider.test.tsx | 2 +- .../react-paypal-js/src/v6/components/CardFieldsProvider.tsx | 3 ++- packages/react-paypal-js/src/v6/index.ts | 5 ++++- packages/react-paypal-js/src/v6/types/index.ts | 2 -- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx index 4b5c7e49..89061277 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.ssr.test.tsx @@ -15,7 +15,7 @@ import type { CardFieldsSessionState, CardFieldsStatusState, } from "../context/CardFieldsProviderContext"; -import type { CardFieldsSessionType } from "../types"; +import type { CardFieldsSessionType } from "./CardFieldsProvider"; jest.mock("../hooks/usePayPal"); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx index f95bf473..e6bc2a0d 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.test.tsx @@ -11,8 +11,8 @@ import { toError } from "../utils"; import type { CardFieldsOneTimePaymentSession, CardFieldsSavePaymentSession, - CardFieldsSessionType, } from "../types"; +import type { CardFieldsSessionType } from "./CardFieldsProvider"; jest.mock("../hooks/usePayPal"); diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index c1909fff..9fff8545 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -19,7 +19,6 @@ import { toError } from "../utils"; import type { CardFieldsOneTimePaymentSession, CardFieldsSavePaymentSession, - CardFieldsSessionType, } from "../types"; import type { CardFieldsSessionState, @@ -30,6 +29,8 @@ export type CardFieldsSession = | CardFieldsOneTimePaymentSession | CardFieldsSavePaymentSession; +export type CardFieldsSessionType = "one-time-payment" | "save-payment"; + type CardFieldsProviderProps = { children: ReactNode; sessionType: CardFieldsSessionType; diff --git a/packages/react-paypal-js/src/v6/index.ts b/packages/react-paypal-js/src/v6/index.ts index 2807a8c9..a32927d2 100644 --- a/packages/react-paypal-js/src/v6/index.ts +++ b/packages/react-paypal-js/src/v6/index.ts @@ -1,5 +1,8 @@ export * from "./types"; -export { CardFieldsProvider } from "./components/CardFieldsProvider"; +export { + CardFieldsProvider, + type CardFieldsSessionType, +} from "./components/CardFieldsProvider"; export { PayPalOneTimePaymentButton } from "./components/PayPalOneTimePaymentButton"; export { PayPalProvider } from "./components/PayPalProvider"; export { useCardFields } from "./hooks/useCardFields"; diff --git a/packages/react-paypal-js/src/v6/types/index.ts b/packages/react-paypal-js/src/v6/types/index.ts index 2758f1fa..027fe8e2 100644 --- a/packages/react-paypal-js/src/v6/types/index.ts +++ b/packages/react-paypal-js/src/v6/types/index.ts @@ -9,5 +9,3 @@ export interface BasePaymentSessionReturn { handleCancel: () => void; handleDestroy: () => void; } - -export type CardFieldsSessionType = "one-time-payment" | "save-payment"; From bdc987d08bd74e566395d85a7bb0bbb6121e1e79 Mon Sep 17 00:00:00 2001 From: Alejandro Gastelum Flores Date: Tue, 9 Dec 2025 12:58:43 -0800 Subject: [PATCH 18/18] Updated CardFieldsProvider comment --- .../src/v6/components/CardFieldsProvider.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx index 9fff8545..20665313 100644 --- a/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/CardFieldsProvider.tsx @@ -40,9 +40,15 @@ type CardFieldsProviderProps = { * {@link CardFieldsProvider} creates the appropriate Card Fields session based on the `sessionType` prop value, and then provides it to child components that require it. * * @example - * - * - * + * + * + * + * + * */ export const CardFieldsProvider = ({ children,