From 92e3c5e55063f4a7d756e109141ca6ddb94f7e19 Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Wed, 3 Dec 2025 13:36:47 -0600 Subject: [PATCH 1/8] pay later updates --- .../usePayLaterOneTimePaymentSession.test.ts | 68 +++++++++++++++++++ .../hooks/usePayLaterOneTimePaymentSession.ts | 13 ++-- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts index 761c016f..f818d6c5 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts @@ -4,6 +4,7 @@ import { expectCurrentErrorValue } from "./useErrorTestUtil"; import { usePayPal } from "./usePayPal"; import { usePayLaterOneTimePaymentSession } from "./usePayLaterOneTimePaymentSession"; import { useProxyProps } from "../utils"; +import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import type { OneTimePaymentSession } from "../types"; @@ -89,6 +90,73 @@ describe("usePayLaterOneTimePaymentSession", () => { expect(error).toEqual(new Error("no sdk instance available")); }); + test("should not error if there is no sdkInstance but loading is still pending", () => { + const mockOrderId = "123"; + + (usePayPal as jest.Mock).mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.PENDING, + }); + + const { + result: { + current: { error }, + }, + } = renderHook(() => + usePayLaterOneTimePaymentSession({ + presentationMode: "auto", + orderId: mockOrderId, + onApprove: jest.fn(), + }), + ); + + expect(error).toBeNull(); + }); + + test("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { + const mockOrderId = "123"; + const mockSession: OneTimePaymentSession = { + cancel: jest.fn(), + destroy: jest.fn(), + start: jest.fn(), + }; + const mockCreatePayLaterOneTimePaymentSession = jest + .fn() + .mockReturnValue(mockSession); + + // First render: no sdkInstance, should error + (usePayPal as jest.Mock).mockReturnValueOnce({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, + }); + + const { result, rerender } = renderHook(() => + usePayLaterOneTimePaymentSession({ + presentationMode: "auto", + orderId: mockOrderId, + onApprove: jest.fn(), + }), + ); + + expectCurrentErrorValue(result.current.error); + expect(result.current.error).toEqual( + new Error("no sdk instance available"), + ); + + // Second render: sdkInstance becomes available, error should clear + (usePayPal as jest.Mock).mockReturnValue({ + sdkInstance: { + createPayLaterOneTimePaymentSession: + mockCreatePayLaterOneTimePaymentSession, + }, + loadingStatus: INSTANCE_LOADING_STATE.RESOLVED, + }); + + rerender(); + + expect(result.current.error).toBeNull(); + }); + test("should provide a click handler that calls session start", async () => { const mockStart = jest.fn(); const mockSession: OneTimePaymentSession = { diff --git a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts index e01068c2..d400765d 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts @@ -4,6 +4,7 @@ import { usePayPal } from "./usePayPal"; import { useIsMountedRef } from "./useIsMounted"; import { useError } from "./useError"; import { useProxyProps } from "../utils"; +import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import type { BasePaymentSessionReturn, @@ -33,7 +34,7 @@ export function usePayLaterOneTimePaymentSession({ orderId, ...callbacks }: PayLaterOneTimePaymentSessionProps): BasePaymentSessionReturn { - const { sdkInstance } = usePayPal(); + const { sdkInstance, loadingStatus } = usePayPal(); const isMountedRef = useIsMountedRef(); const sessionRef = useRef(null); // handle cleanup const proxyCallbacks = useProxyProps(callbacks); @@ -46,10 +47,14 @@ export function usePayLaterOneTimePaymentSession({ // Separate error reporting effect to avoid infinite loops with proxyCallbacks useEffect(() => { - if (!sdkInstance) { - setError(new Error("no sdk instance available")); + if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { + if (!sdkInstance) { + setError(new Error("no sdk instance available")); + } else { + setError(null); + } } - }, [sdkInstance, setError]); + }, [sdkInstance, setError, loadingStatus]); useEffect(() => { if (!sdkInstance) { From 373dbd338c077150db0db4e8a2388489d7ec981c Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Wed, 3 Dec 2025 14:17:58 -0600 Subject: [PATCH 2/8] update other hooks --- .../hooks/usePayLaterOneTimePaymentSession.ts | 10 +-- .../usePayPalOneTimePaymentSession.test.ts | 66 ++++++++++++++++- .../hooks/usePayPalOneTimePaymentSession.ts | 9 ++- .../hooks/usePayPalSavePaymentSession.test.ts | 73 ++++++++++++++++++- .../v6/hooks/usePayPalSavePaymentSession.ts | 9 ++- .../useVenmoOneTimePaymentSession.test.ts | 64 ++++++++++++++++ .../v6/hooks/useVenmoOneTimePaymentSession.ts | 9 ++- 7 files changed, 223 insertions(+), 17 deletions(-) diff --git a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts index d400765d..469cd630 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.ts @@ -47,12 +47,10 @@ export function usePayLaterOneTimePaymentSession({ // Separate error reporting effect to avoid infinite loops with proxyCallbacks useEffect(() => { - if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { - if (!sdkInstance) { - setError(new Error("no sdk instance available")); - } else { - setError(null); - } + if (sdkInstance) { + setError(null); + } else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { + setError(new Error("no sdk instance available")); } }, [sdkInstance, setError, loadingStatus]); diff --git a/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts index 13bf7b35..c9a67300 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts @@ -56,7 +56,7 @@ describe("usePayPalOneTimePaymentSession", () => { test("should error if there is no sdkInstance when called", () => { mockUsePayPal.mockReturnValue({ sdkInstance: null, - loadingStatus: INSTANCE_LOADING_STATE.PENDING, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, eligiblePaymentMethods: null, error: null, }); @@ -80,6 +80,70 @@ describe("usePayPalOneTimePaymentSession", () => { expect(error).toEqual(new Error("no sdk instance available")); }); + 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 props: UsePayPalOneTimePaymentSessionProps = { + presentationMode: "popup", + orderId: "test-order-id", + onApprove: jest.fn(), + }; + + const { + result: { + current: { error }, + }, + } = renderHook(() => usePayPalOneTimePaymentSession(props)); + + expect(error).toBeNull(); + }); + + test("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { + const mockSession = createMockPayPalSession(); + const mockSdkInstanceNew = createMockSdkInstance(mockSession); + + // 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 props: UsePayPalOneTimePaymentSessionProps = { + presentationMode: "popup", + orderId: "test-order-id", + onApprove: jest.fn(), + }; + + const { result, rerender } = renderHook(() => + usePayPalOneTimePaymentSession(props), + ); + + expectCurrentErrorValue(result.current.error); + expect(result.current.error).toEqual( + new Error("no sdk instance available"), + ); + + // Second render: sdkInstance becomes available, error should clear + mockUsePayPal.mockReturnValue({ + // @ts-expect-error mocking sdk instance + sdkInstance: mockSdkInstanceNew, + loadingStatus: INSTANCE_LOADING_STATE.RESOLVED, + eligiblePaymentMethods: null, + error: null, + }); + + rerender(); + + expect(result.current.error).toBeNull(); + }); + test("should create a PayPal payment session when the hook is called with orderId", () => { const onApprove = jest.fn(); const onCancel = jest.fn(); diff --git a/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.ts b/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.ts index 7bdce33c..c8a43543 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.ts @@ -4,6 +4,7 @@ import { usePayPal } from "./usePayPal"; import { useIsMountedRef } from "./useIsMounted"; import { useError } from "./useError"; import { useProxyProps } from "../utils"; +import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import { OneTimePaymentSession, PayPalPresentationModeOptions, @@ -31,7 +32,7 @@ export function usePayPalOneTimePaymentSession({ orderId, ...callbacks }: UsePayPalOneTimePaymentSessionProps): BasePaymentSessionReturn { - const { sdkInstance } = usePayPal(); + const { sdkInstance, loadingStatus } = usePayPal(); const isMountedRef = useIsMountedRef(); const sessionRef = useRef(null); const proxyCallbacks = useProxyProps(callbacks); @@ -48,10 +49,12 @@ export function usePayPalOneTimePaymentSession({ // Separate error reporting effect to avoid infinite loops with proxyCallbacks useEffect(() => { - if (!sdkInstance) { + if (sdkInstance) { + setError(null); + } else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { setError(new Error("no sdk instance available")); } - }, [sdkInstance, setError]); + }, [sdkInstance, setError, loadingStatus]); useEffect(() => { if (!sdkInstance) { diff --git a/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts index 07b46e65..80ddfed1 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts @@ -4,6 +4,7 @@ import { expectCurrentErrorValue } from "./useErrorTestUtil"; import { usePayPal } from "./usePayPal"; import { usePayPalSavePaymentSession } from "./usePayPalSavePaymentSession"; import { useProxyProps } from "../utils"; +import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import type { SavePaymentSession } from "../types"; @@ -115,7 +116,10 @@ describe("usePayPalSavePaymentSession", () => { test("should error if there is no sdkInstance when called", () => { const mockVaultSetupToken = "vault-setup-token-123"; - (usePayPal as jest.Mock).mockReturnValue({ sdkInstance: null }); + (usePayPal as jest.Mock).mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, + }); const { result: { @@ -134,6 +138,73 @@ describe("usePayPalSavePaymentSession", () => { expect(error).toEqual(new Error("no sdk instance available")); }); + test("should not error if there is no sdkInstance but loading is still pending", () => { + const mockVaultSetupToken = "vault-setup-token-123"; + + (usePayPal as jest.Mock).mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.PENDING, + }); + + const { + result: { + current: { error }, + }, + } = renderHook(() => + usePayPalSavePaymentSession({ + presentationMode: "auto", + vaultSetupToken: mockVaultSetupToken, + onApprove: jest.fn(), + }), + ); + + expect(error).toBeNull(); + }); + + test("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { + const mockVaultSetupToken = "vault-setup-token-123"; + const mockSession: SavePaymentSession = { + cancel: jest.fn(), + destroy: jest.fn(), + start: jest.fn(), + }; + const mockCreatePayPalSavePaymentSession = jest + .fn() + .mockReturnValue(mockSession); + + // First render: no sdkInstance and not in PENDING state, should error + (usePayPal as jest.Mock).mockReturnValue({ + sdkInstance: null, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, + }); + + const { result, rerender } = renderHook(() => + usePayPalSavePaymentSession({ + presentationMode: "auto", + vaultSetupToken: mockVaultSetupToken, + onApprove: jest.fn(), + }), + ); + + expectCurrentErrorValue(result.current.error); + expect(result.current.error).toEqual( + new Error("no sdk instance available"), + ); + + // Second render: sdkInstance becomes available, error should clear + (usePayPal as jest.Mock).mockReturnValue({ + sdkInstance: { + createPayPalSavePaymentSession: + mockCreatePayPalSavePaymentSession, + }, + loadingStatus: INSTANCE_LOADING_STATE.RESOLVED, + }); + + rerender(); + + expect(result.current.error).toBeNull(); + }); + test("should provide a click handler that calls session start with vaultSetupToken", async () => { const mockStart = jest.fn(); const mockSession: SavePaymentSession = { diff --git a/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.ts b/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.ts index bfed2285..f53fc2e4 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.ts @@ -4,6 +4,7 @@ import { usePayPal } from "./usePayPal"; import { useIsMountedRef } from "./useIsMounted"; import { useError } from "./useError"; import { useProxyProps } from "../utils"; +import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import type { SavePaymentSession, @@ -32,7 +33,7 @@ export function usePayPalSavePaymentSession({ vaultSetupToken, ...callbacks }: PayPalSavePaymentSessionProps): BasePaymentSessionReturn { - const { sdkInstance } = usePayPal(); + const { sdkInstance, loadingStatus } = usePayPal(); const isMountedRef = useIsMountedRef(); const sessionRef = useRef(null); // handle cleanup const proxyCallbacks = useProxyProps(callbacks); @@ -45,10 +46,12 @@ export function usePayPalSavePaymentSession({ // Separate error reporting effect to avoid infinite loops with proxyCallbacks useEffect(() => { - if (!sdkInstance) { + if (sdkInstance) { + setError(null); + } else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { setError(new Error("no sdk instance available")); } - }, [sdkInstance, setError]); + }, [sdkInstance, setError, loadingStatus]); useEffect(() => { if (!sdkInstance) { diff --git a/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts index f9641aa6..48859bfa 100644 --- a/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts @@ -83,6 +83,70 @@ describe("useVenmoOneTimePaymentSession", () => { expect(error).toEqual(new Error("no sdk instance available")); }); + 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 props: UseVenmoOneTimePaymentSessionProps = { + presentationMode: "popup", + orderId: "test-order-id", + onApprove: jest.fn(), + }; + + const { + result: { + current: { error }, + }, + } = renderHook(() => useVenmoOneTimePaymentSession(props)); + + expect(error).toBeNull(); + }); + + test.only("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { + const mockSession = createMockVenmoSession(); + const mockSdkInstanceNew = createMockSdkInstance(mockSession); + + // 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 props: UseVenmoOneTimePaymentSessionProps = { + presentationMode: "popup", + orderId: "test-order-id", + onApprove: jest.fn(), + }; + + const { result, rerender } = renderHook(() => + useVenmoOneTimePaymentSession(props), + ); + + expectCurrentErrorValue(result.current.error); + expect(result.current.error).toEqual( + new Error("no sdk instance available"), + ); + + // Second render: sdkInstance becomes available, error should clear + mockUsePayPal.mockReturnValue({ + // @ts-expect-error mocking sdk instance + sdkInstance: mockSdkInstanceNew, + loadingStatus: INSTANCE_LOADING_STATE.RESOLVED, + eligiblePaymentMethods: null, + error: null, + }); + + rerender(); + + expect(result.current.error).toBeNull(); + }); + test("should create Venmo session with orderId when provided", () => { const onApprove = jest.fn(); const onCancel = jest.fn(); diff --git a/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.ts b/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.ts index 086f9c7b..fc175c37 100644 --- a/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.ts +++ b/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.ts @@ -4,6 +4,7 @@ import { usePayPal } from "./usePayPal"; import { useIsMountedRef } from "./useIsMounted"; import { useError } from "./useError"; import { useProxyProps } from "../utils"; +import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderEnums"; import type { VenmoOneTimePaymentSession, @@ -33,7 +34,7 @@ export function useVenmoOneTimePaymentSession({ orderId, ...callbacks }: UseVenmoOneTimePaymentSessionProps): BasePaymentSessionReturn { - const { sdkInstance } = usePayPal(); + const { sdkInstance, loadingStatus } = usePayPal(); const isMountedRef = useIsMountedRef(); const sessionRef = useRef(null); const proxyCallbacks = useProxyProps(callbacks); @@ -46,10 +47,12 @@ export function useVenmoOneTimePaymentSession({ // Separate error reporting effect to avoid infinite loops with proxyCallbacks useEffect(() => { - if (!sdkInstance) { + if (sdkInstance) { + setError(null); + } else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) { setError(new Error("no sdk instance available")); } - }, [sdkInstance, setError]); + }, [sdkInstance, setError, loadingStatus]); useEffect(() => { if (!sdkInstance) { From 05d4948e43fdf6409876022e059fdd89a9bdff11 Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Wed, 3 Dec 2025 15:29:58 -0600 Subject: [PATCH 3/8] remove document check --- packages/react-paypal-js/src/v6/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-paypal-js/src/v6/utils.ts b/packages/react-paypal-js/src/v6/utils.ts index 1f5b4d41..e24453fa 100644 --- a/packages/react-paypal-js/src/v6/utils.ts +++ b/packages/react-paypal-js/src/v6/utils.ts @@ -3,7 +3,7 @@ import { useRef } from "react"; import type { Components } from "./types"; export function isServer(): boolean { - return typeof window === "undefined" && typeof document === "undefined"; + return typeof window === "undefined"; } /** From 18b4a69ec41d3f12264b2887682de7594b6dad45 Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Wed, 3 Dec 2025 15:57:46 -0600 Subject: [PATCH 4/8] components default --- .../src/v6/components/PayPalProvider.test.tsx | 24 +++++++++++++++++++ .../src/v6/components/PayPalProvider.tsx | 8 ++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx b/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx index 1600ba84..a361fde3 100644 --- a/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx @@ -220,6 +220,30 @@ describe("PayPalProvider", () => { ); }); + test("should use default components array when not provided", async () => { + const mockCreateInstance = jest + .fn() + .mockResolvedValue(createMockSdkInstance()); + + (loadCoreSdkScript as jest.Mock).mockResolvedValue({ + createInstance: mockCreateInstance, + }); + + // @ts-expect-error components is required, testing defaulting behavior + const { state } = renderProvider({ + clientToken: TEST_CLIENT_TOKEN, + }); + + await waitFor(() => expectResolvedState(state)); + + expect(mockCreateInstance).toHaveBeenCalledWith( + expect.objectContaining({ + components: ["paypal-payments"], + clientToken: TEST_CLIENT_TOKEN, + }), + ); + }); + test("should handle createInstance failure", async () => { const instanceError = new Error("Instance creation failed"); const mockCreateInstance = jest diff --git a/packages/react-paypal-js/src/v6/components/PayPalProvider.tsx b/packages/react-paypal-js/src/v6/components/PayPalProvider.tsx index 51a12bc0..989ab1c2 100644 --- a/packages/react-paypal-js/src/v6/components/PayPalProvider.tsx +++ b/packages/react-paypal-js/src/v6/components/PayPalProvider.tsx @@ -27,10 +27,12 @@ import type { import type { PayPalState } from "../context/PayPalProviderContext"; import type { usePayPal } from "../hooks/usePayPal"; -type PayPalProviderProps = CreateInstanceOptions< - readonly [Components, ...Components[]] +type PayPalProviderProps = Omit< + CreateInstanceOptions, + "components" > & LoadCoreSdkScriptOptions & { + components?: Components[]; eligibleMethodsResponse?: FindEligiblePaymentMethodsResponse; eligibleMethodsPayload?: FindEligiblePaymentMethodsRequestPayload; children: React.ReactNode; @@ -43,7 +45,7 @@ type PayPalProviderProps = CreateInstanceOptions< export const PayPalProvider: React.FC = ({ clientMetadataId, clientToken, - components, + components = ["paypal-payments"], locale, pageType, partnerAttributionId, From 26027a60e30698aa1608a42a86a15935fb8f0d13 Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Wed, 3 Dec 2025 16:19:35 -0600 Subject: [PATCH 5/8] remove context export --- packages/react-paypal-js/src/v6/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-paypal-js/src/v6/index.ts b/packages/react-paypal-js/src/v6/index.ts index 66243977..5d7cd622 100644 --- a/packages/react-paypal-js/src/v6/index.ts +++ b/packages/react-paypal-js/src/v6/index.ts @@ -1,6 +1,5 @@ export * from "./types"; export { PayPalProvider } from "./components/PayPalProvider"; -export { PayPalContext } from "./context/PayPalProviderContext"; export { usePayPal } from "./hooks/usePayPal"; export { usePayLaterOneTimePaymentSession } from "./hooks/usePayLaterOneTimePaymentSession"; export { usePayPalOneTimePaymentSession } from "./hooks/usePayPalOneTimePaymentSession"; From a73fc6d99fbf0ae4f29f21682443f0abd7ce6f56 Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Wed, 3 Dec 2025 16:34:47 -0600 Subject: [PATCH 6/8] add changeset --- .changeset/rude-melons-wear.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/rude-melons-wear.md diff --git a/.changeset/rude-melons-wear.md b/.changeset/rude-melons-wear.md new file mode 100644 index 00000000..0f41af11 --- /dev/null +++ b/.changeset/rude-melons-wear.md @@ -0,0 +1,8 @@ +--- +"@paypal/react-paypal-js": patch +--- + +- Default `PayPalProvider` `components` to `["paypal-payments"]`. +- Update session hooks to check `loadingStatus` before returning an error for no `sdkInstance`. +- `PayPalContext` export was removed since merchants won't need to use that directly. +- Check only `window` for `isServer` SSR function. From 33dbc12915776989b7b31efd1e46e65e582a9021 Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Thu, 4 Dec 2025 09:49:05 -0600 Subject: [PATCH 7/8] fixed provider type --- .../src/v6/components/PayPalProvider.test.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx b/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx index a361fde3..054c7b89 100644 --- a/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx +++ b/packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx @@ -57,19 +57,26 @@ const createInstanceOptions: CreateInstanceOptions<["paypal-payments"]> = { }; function renderProvider( - instanceOptions = createInstanceOptions, - environment: "sandbox" | "production" = "sandbox", - debug = false, - children?: React.ReactNode, + props: Partial> = {}, ) { const { state, TestComponent } = setupTestComponent(); + const { + children, + clientToken = createInstanceOptions.clientToken, + components = createInstanceOptions.components, + debug = false, + environment = "sandbox", + ...restProps + } = props; + const result = render( {children} , @@ -184,7 +191,7 @@ describe("PayPalProvider", () => { return Promise.resolve(createMockPayPalNamespace()); }); - renderProvider(createInstanceOptions, environment); + renderProvider({ environment, ...createInstanceOptions }); expect(loadCoreSdkScript).toHaveBeenCalledWith({ environment, @@ -229,7 +236,6 @@ describe("PayPalProvider", () => { createInstance: mockCreateInstance, }); - // @ts-expect-error components is required, testing defaulting behavior const { state } = renderProvider({ clientToken: TEST_CLIENT_TOKEN, }); @@ -279,7 +285,6 @@ describe("PayPalProvider", () => { (loadCoreSdkScript as jest.Mock).mockResolvedValue({ createInstance: mockCreateInstance, }); - // @ts-expect-error renderProvider is typed for single component only const { state } = renderProvider(multiComponentOptions); await waitFor(() => expectResolvedState(state)); From 18a23110a1e81e98b1c6e1f08c5cae0531a4e208 Mon Sep 17 00:00:00 2001 From: akos_paypal Date: Thu, 4 Dec 2025 10:09:39 -0600 Subject: [PATCH 8/8] update tests --- .../v6/hooks/usePayLaterOneTimePaymentSession.test.ts | 11 ++++++++++- .../v6/hooks/usePayPalOneTimePaymentSession.test.ts | 5 ++++- .../src/v6/hooks/usePayPalSavePaymentSession.test.ts | 11 ++++++++++- .../v6/hooks/useVenmoOneTimePaymentSession.test.ts | 7 +++++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts index f818d6c5..b8cbcb79 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayLaterOneTimePaymentSession.test.ts @@ -68,8 +68,16 @@ describe("usePayLaterOneTimePaymentSession", () => { }); }); - test("should error if there is no sdkInstance when called", () => { + test("should error if there is no sdkInstance when called and not create a session", () => { const mockOrderId = "123"; + const mockSession: OneTimePaymentSession = { + cancel: jest.fn(), + destroy: jest.fn(), + start: jest.fn(), + }; + const mockCreatePayLaterOneTimePaymentSession = jest + .fn() + .mockReturnValue(mockSession); (usePayPal as jest.Mock).mockReturnValue({ sdkInstance: null }); @@ -88,6 +96,7 @@ describe("usePayLaterOneTimePaymentSession", () => { expectCurrentErrorValue(error); expect(error).toEqual(new Error("no sdk instance available")); + expect(mockCreatePayLaterOneTimePaymentSession).not.toHaveBeenCalled(); }); test("should not error if there is no sdkInstance but loading is still pending", () => { diff --git a/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts index c9a67300..951e8ea2 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayPalOneTimePaymentSession.test.ts @@ -53,7 +53,7 @@ describe("usePayPalOneTimePaymentSession", () => { }); describe("initialization", () => { - test("should error if there is no sdkInstance when called", () => { + test("should not create session when no SDK instance is available", () => { mockUsePayPal.mockReturnValue({ sdkInstance: null, loadingStatus: INSTANCE_LOADING_STATE.REJECTED, @@ -78,6 +78,9 @@ describe("usePayPalOneTimePaymentSession", () => { expectCurrentErrorValue(error); expect(error).toEqual(new Error("no sdk instance available")); + expect( + mockSdkInstance.createPayPalOneTimePaymentSession, + ).not.toHaveBeenCalled(); }); test("should not error if there is no sdkInstance but loading is still pending", () => { diff --git a/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts index 80ddfed1..882be4f2 100644 --- a/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/usePayPalSavePaymentSession.test.ts @@ -113,8 +113,16 @@ describe("usePayPalSavePaymentSession", () => { }); }); - test("should error if there is no sdkInstance when called", () => { + test("should error if there is no sdkInstance when called and not create a session", () => { const mockVaultSetupToken = "vault-setup-token-123"; + const mockSession: SavePaymentSession = { + cancel: jest.fn(), + destroy: jest.fn(), + start: jest.fn(), + }; + const mockCreatePayPalSavePaymentSession = jest + .fn() + .mockReturnValue(mockSession); (usePayPal as jest.Mock).mockReturnValue({ sdkInstance: null, @@ -136,6 +144,7 @@ describe("usePayPalSavePaymentSession", () => { expectCurrentErrorValue(error); expect(error).toEqual(new Error("no sdk instance available")); + expect(mockCreatePayPalSavePaymentSession).not.toHaveBeenCalled(); }); test("should not error if there is no sdkInstance but loading is still pending", () => { diff --git a/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts b/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts index 48859bfa..75c500e0 100644 --- a/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts +++ b/packages/react-paypal-js/src/v6/hooks/useVenmoOneTimePaymentSession.test.ts @@ -59,7 +59,7 @@ describe("useVenmoOneTimePaymentSession", () => { test("should not create session when no SDK instance is available", () => { mockUsePayPal.mockReturnValue({ sdkInstance: null, - loadingStatus: INSTANCE_LOADING_STATE.PENDING, + loadingStatus: INSTANCE_LOADING_STATE.REJECTED, eligiblePaymentMethods: null, error: null, }); @@ -81,6 +81,9 @@ describe("useVenmoOneTimePaymentSession", () => { expectCurrentErrorValue(error); expect(error).toEqual(new Error("no sdk instance available")); + expect( + mockSdkInstance.createVenmoOneTimePaymentSession, + ).not.toHaveBeenCalled(); }); test("should not error if there is no sdkInstance but loading is still pending", () => { @@ -106,7 +109,7 @@ describe("useVenmoOneTimePaymentSession", () => { expect(error).toBeNull(); }); - test.only("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { + test("should clear any sdkInstance related errors if the sdkInstance becomes available", () => { const mockSession = createMockVenmoSession(); const mockSdkInstanceNew = createMockSdkInstance(mockSession);