Skip to content

Commit 13f06a8

Browse files
Feature: Update v6 PayPalProvider Eligibility (#811)
* remove default eligibility request * add setEligibility to context * update provider to set dispatch wrapper in context * initial updates to useFetchEligibleMethods * update tests * update to use internal dispatch context * update tests again * add hook doc comments, tests and update eligibility hook to surface context errors * remove comment * chore: add changeset * move useFetchEligibleMethods to its own file * remove client token in favor of headers object in use fetch eligible methods and update tests * update comment to remove lint warning * remove todos * add server-only package and apply to useFetchEligibleMethods hook * fix useFetchEligibleMethods jest configuration due to server-only import * refactor useEligibleMethods to fetch methods when payload has changed * update paypal dispatch context commnet * add useFetchEligibleMethods to index for export
1 parent e079afb commit 13f06a8

File tree

13 files changed

+893
-343
lines changed

13 files changed

+893
-343
lines changed

.changeset/dirty-rules-wear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@paypal/react-paypal-js": minor
3+
---
4+
5+
Removes v6 PayPalProvider default eligibility request and adds useEligibleMethods hook.

package-lock.json

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
import "@testing-library/jest-dom";
2+
3+
// Export empty object to satisfy server-only module resolution via moduleNameMapper
4+
// The server-only package throws when imported on the client, but tests need to import it
5+
export {};

packages/react-paypal-js/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"homepage": "https://paypal.github.io/react-paypal-js/",
6060
"dependencies": {
6161
"@paypal/paypal-js": "^9.2.0",
62-
"@paypal/sdk-constants": "^1.0.122"
62+
"@paypal/sdk-constants": "^1.0.122",
63+
"server-only": "^0.0.1"
6364
},
6465
"devDependencies": {
6566
"@babel/core": "^7.17.5",
@@ -128,7 +129,8 @@
128129
"./jest.setup.ts"
129130
],
130131
"moduleNameMapper": {
131-
"@paypal/paypal-js/sdk-v6": "<rootDir>/../../node_modules/@paypal/paypal-js/dist/v6/esm/paypal-js.js"
132+
"@paypal/paypal-js/sdk-v6": "<rootDir>/../../node_modules/@paypal/paypal-js/dist/v6/esm/paypal-js.js",
133+
"^server-only$": "<rootDir>/jest.setup.ts"
132134
}
133135
},
134136
"bugs": {

packages/react-paypal-js/src/v6/components/PayPalProvider.test.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ describe("PayPalProvider", () => {
438438
});
439439

440440
describe("Eligibility Loading", () => {
441-
test("should call findEligibleMethods when no eligibleMethodsResponse is provided", async () => {
441+
test("should not call findEligibleMethods when no eligibleMethodsResponse is provided", async () => {
442442
const mockFindEligibleMethods = jest
443443
.fn()
444444
.mockResolvedValue(TEST_SDK_ELIGIBILITY_RESULT);
@@ -455,12 +455,10 @@ describe("PayPalProvider", () => {
455455

456456
await waitFor(() => expectResolvedState(state));
457457

458-
expect(mockFindEligibleMethods).toHaveBeenCalledWith({});
459-
await waitFor(() =>
460-
expect(state.eligiblePaymentMethods).toEqual(
461-
TEST_SDK_ELIGIBILITY_RESULT,
462-
),
463-
);
458+
// Provider no longer automatically calls findEligibleMethods
459+
// Client-side fetching should be done via useFetchEligibleMethods hook
460+
expect(mockFindEligibleMethods).not.toHaveBeenCalled();
461+
expect(state.eligiblePaymentMethods).toBeNull();
464462
});
465463

466464
test("should call hydrateEligibleMethods when eligibleMethodsResponse is provided", async () => {
@@ -497,38 +495,41 @@ describe("PayPalProvider", () => {
497495
);
498496
});
499497

500-
test("should store eligibility result in context state", async () => {
498+
test("should store eligibility result in context state when eligibleMethodsResponse is provided", async () => {
501499
const eligibilityResult = {
502500
paypal: { eligible: true, recommended: true },
503501
venmo: { eligible: false },
504502
};
505503

504+
const mockHydrateEligibleMethods = jest
505+
.fn()
506+
.mockReturnValue(eligibilityResult);
506507
const mockInstance = {
507508
...createMockSdkInstance(),
508-
findEligibleMethods: jest
509-
.fn()
510-
.mockResolvedValue(eligibilityResult),
509+
hydrateEligibleMethods: mockHydrateEligibleMethods,
511510
};
512511

513512
(loadCoreSdkScript as jest.Mock).mockResolvedValue({
514513
createInstance: jest.fn().mockResolvedValue(mockInstance),
515514
});
516515

517-
const { state } = await renderProvider();
516+
const { state } = await renderProvider({
517+
eligibleMethodsResponse: TEST_ELIGIBILITY_HOOK_RESULT,
518+
});
518519

519520
await waitFor(() => expectResolvedState(state));
520521
await waitFor(() =>
521522
expect(state.eligiblePaymentMethods).toEqual(eligibilityResult),
522523
);
523524
});
524525

525-
test("should not run eligibility effect until sdkInstance is available", async () => {
526-
const mockFindEligibleMethods = jest
526+
test("should not run eligibility hydration effect until sdkInstance is available", async () => {
527+
const mockHydrateEligibleMethods = jest
527528
.fn()
528-
.mockResolvedValue(TEST_SDK_ELIGIBILITY_RESULT);
529+
.mockReturnValue(TEST_SDK_ELIGIBILITY_RESULT);
529530
const mockInstance = {
530531
...createMockSdkInstance(),
531-
findEligibleMethods: mockFindEligibleMethods,
532+
hydrateEligibleMethods: mockHydrateEligibleMethods,
532533
};
533534

534535
let resolveCreateInstance: (value: typeof mockInstance) => void;
@@ -552,14 +553,15 @@ describe("PayPalProvider", () => {
552553
components={["paypal-payments"]}
553554
clientToken={TEST_CLIENT_TOKEN}
554555
environment="sandbox"
556+
eligibleMethodsResponse={TEST_ELIGIBILITY_HOOK_RESULT}
555557
>
556558
<TestComponent />
557559
</PayPalProvider>,
558560
);
559561
});
560562

561-
// findEligibleMethods should not be called yet
562-
expect(mockFindEligibleMethods).not.toHaveBeenCalled();
563+
// hydrateEligibleMethods should not be called yet
564+
expect(mockHydrateEligibleMethods).not.toHaveBeenCalled();
563565
expect(state.eligiblePaymentMethods).toBe(null);
564566

565567
// Resolve instance creation
@@ -568,7 +570,9 @@ describe("PayPalProvider", () => {
568570
});
569571

570572
await waitFor(() => {
571-
expect(mockFindEligibleMethods).toHaveBeenCalledWith({});
573+
expect(mockHydrateEligibleMethods).toHaveBeenCalledWith(
574+
TEST_ELIGIBILITY_HOOK_RESULT,
575+
);
572576
});
573577
});
574578

@@ -643,21 +647,25 @@ describe("PayPalProvider", () => {
643647
expect(mockHydrateEligibleMethods).toHaveBeenCalledTimes(2);
644648
});
645649

646-
test("should handle error when findEligibleMethods fails", async () => {
647-
const mockError = new Error("findEligibleMethods failed");
648-
const mockFindEligibleMethods = jest
650+
test("should handle error when hydrateEligibleMethods fails", async () => {
651+
const mockError = new Error("hydrateEligibleMethods failed");
652+
const mockHydrateEligibleMethods = jest
649653
.fn()
650-
.mockRejectedValue(mockError);
654+
.mockImplementation(() => {
655+
throw mockError;
656+
});
651657
const mockInstance = {
652658
...createMockSdkInstance(),
653-
findEligibleMethods: mockFindEligibleMethods,
659+
hydrateEligibleMethods: mockHydrateEligibleMethods,
654660
};
655661

656662
(loadCoreSdkScript as jest.Mock).mockResolvedValue({
657663
createInstance: jest.fn().mockResolvedValue(mockInstance),
658664
});
659665

660-
const { state } = await renderProvider();
666+
const { state } = await renderProvider({
667+
eligibleMethodsResponse: TEST_ELIGIBILITY_HOOK_RESULT,
668+
});
661669

662670
await waitFor(() => expectRejectedState(state, mockError));
663671
expect(state.eligiblePaymentMethods).toBe(null);

packages/react-paypal-js/src/v6/components/PayPalProvider.tsx

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
initialState,
77
instanceReducer,
88
} from "../context/PayPalProviderContext";
9+
import { PayPalDispatchContext } from "../context/PayPalDispatchContext";
910
import { useIsomorphicLayoutEffect } from "../hooks/useIsomorphicLayoutEffect";
1011
import {
1112
INSTANCE_LOADING_STATE,
@@ -265,36 +266,22 @@ export const PayPalProvider: React.FC<PayPalProviderProps> = ({
265266
return;
266267
}
267268

268-
let isSubscribed = true;
269-
270-
const setEligibility = async () => {
271-
try {
272-
const eligiblePaymentMethods = eligibleMethodsResponse
273-
? sdkInstance.hydrateEligibleMethods(
274-
eligibleMethodsResponse,
275-
)
276-
: await sdkInstance.findEligibleMethods({});
277-
269+
try {
270+
if (eligibleMethodsResponse) {
271+
const eligiblePaymentMethods =
272+
sdkInstance.hydrateEligibleMethods(eligibleMethodsResponse);
278273
dispatch({
279274
type: INSTANCE_DISPATCH_ACTION.SET_ELIGIBILITY,
280275
value: eligiblePaymentMethods,
281276
});
282-
} catch (error) {
283-
if (isSubscribed) {
284-
setError(error);
285-
dispatch({
286-
type: INSTANCE_DISPATCH_ACTION.SET_ERROR,
287-
value: toError(error),
288-
});
289-
}
290277
}
291-
};
292-
293-
setEligibility();
294-
295-
return () => {
296-
isSubscribed = false;
297-
};
278+
} catch (error) {
279+
setError(error);
280+
dispatch({
281+
type: INSTANCE_DISPATCH_ACTION.SET_ERROR,
282+
value: toError(error),
283+
});
284+
}
298285
}, [state.sdkInstance, eligibleMethodsResponse, setError]);
299286

300287
const contextValue: PayPalState = useMemo(
@@ -315,8 +302,10 @@ export const PayPalProvider: React.FC<PayPalProviderProps> = ({
315302
);
316303

317304
return (
318-
<PayPalContext.Provider value={contextValue}>
319-
{children}
320-
</PayPalContext.Provider>
305+
<PayPalDispatchContext.Provider value={dispatch}>
306+
<PayPalContext.Provider value={contextValue}>
307+
{children}
308+
</PayPalContext.Provider>
309+
</PayPalDispatchContext.Provider>
321310
);
322311
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createContext, Dispatch } from "react";
2+
3+
import type { InstanceAction } from "./PayPalProviderContext";
4+
5+
/**
6+
* Internal context for dispatching PayPal instance state updates.
7+
* This is NOT exported to external consumers.
8+
* Only hooks like useEligibleMethods can access this internally.
9+
*
10+
* @internal
11+
*/
12+
export const PayPalDispatchContext =
13+
createContext<Dispatch<InstanceAction> | null>(null);

0 commit comments

Comments
 (0)