Skip to content

Commit 2f0f473

Browse files
nityaspNitya
andauthored
Feature/adding isPending to hooks an buttons to allow deferred clientToken (#787)
* Adding isPending to hook and buttons to allow deferred clientToken * changeset added * updating isPending behavior to prevent layout shift --------- Co-authored-by: Nitya <nsattaru@paypal.com>
1 parent f55c494 commit 2f0f473

25 files changed

+364
-10
lines changed

.changeset/cuddly-roses-attack.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": patch
3+
---
4+
5+
Adding isPending to hook and buttons to allow deferred clientToken

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe("PayLaterOneTimePaymentButton", () => {
2727
jest.clearAllMocks();
2828
mockUsePayLaterOneTimePaymentSession.mockReturnValue({
2929
error: null,
30+
isPending: false,
3031
handleClick: mockHandleClick,
3132
});
3233
mockUsePayPal.mockReturnValue({
@@ -96,6 +97,7 @@ describe("PayLaterOneTimePaymentButton", () => {
9697
jest.spyOn(console, "error").mockImplementation();
9798
mockUsePayLaterOneTimePaymentSession.mockReturnValue({
9899
error: new Error("Test error"),
100+
isPending: false,
99101
handleClick: mockHandleClick,
100102
});
101103
const { container } = render(
@@ -112,6 +114,7 @@ describe("PayLaterOneTimePaymentButton", () => {
112114
it("should not disable button when error is null", () => {
113115
mockUsePayLaterOneTimePaymentSession.mockReturnValue({
114116
error: null,
117+
isPending: false,
115118
handleClick: mockHandleClick,
116119
});
117120
const { container } = render(
@@ -125,6 +128,25 @@ describe("PayLaterOneTimePaymentButton", () => {
125128
expect(button).not.toHaveAttribute("disabled");
126129
});
127130

131+
it("should return null when isPending is true", () => {
132+
mockUsePayLaterOneTimePaymentSession.mockReturnValue({
133+
error: null,
134+
isPending: true,
135+
handleClick: mockHandleClick,
136+
});
137+
const { container } = render(
138+
<PayLaterOneTimePaymentButton
139+
onApprove={() => Promise.resolve()}
140+
orderId="123"
141+
presentationMode="auto"
142+
/>,
143+
);
144+
expect(
145+
container.querySelector("paypal-pay-later-button"),
146+
).not.toBeInTheDocument();
147+
expect(container.firstChild).toBeNull();
148+
});
149+
128150
it("should pass hook props to usePayLaterOneTimePaymentSession", () => {
129151
const hookProps = {
130152
clientToken: "test-token",

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ export const PayLaterOneTimePaymentButton = ({
3535
disabled = false,
3636
...hookProps
3737
}: PayLaterOneTimePaymentButtonProps): JSX.Element | null => {
38-
const { error, handleClick } = usePayLaterOneTimePaymentSession(hookProps);
3938
const { eligiblePaymentMethods } = usePayPal();
39+
const { error, isPending, handleClick } =
40+
usePayLaterOneTimePaymentSession(hookProps);
4041
const isServerSide = isServer();
4142

4243
const payLaterDetails =
@@ -50,6 +51,10 @@ export const PayLaterOneTimePaymentButton = ({
5051
}
5152
}, [error]);
5253

54+
if (isPending) {
55+
return null;
56+
}
57+
5358
return isServerSide ? (
5459
<div />
5560
) : (

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe("PayPalOneTimePaymentButton", () => {
2222
jest.clearAllMocks();
2323
mockUsePayPalOneTimePaymentSession.mockReturnValue({
2424
error: null,
25+
isPending: false,
2526
handleClick: mockHandleClick,
2627
});
2728
mockIsServer.mockReturnValue(false);
@@ -85,6 +86,7 @@ describe("PayPalOneTimePaymentButton", () => {
8586
jest.spyOn(console, "error").mockImplementation();
8687
mockUsePayPalOneTimePaymentSession.mockReturnValue({
8788
error: new Error("Test error"),
89+
isPending: false,
8890
handleClick: mockHandleClick,
8991
});
9092
const { container } = render(
@@ -101,6 +103,7 @@ describe("PayPalOneTimePaymentButton", () => {
101103
it("should not disable button when error is null", () => {
102104
mockUsePayPalOneTimePaymentSession.mockReturnValue({
103105
error: null,
106+
isPending: false,
104107
handleClick: mockHandleClick,
105108
});
106109
const { container } = render(
@@ -114,6 +117,24 @@ describe("PayPalOneTimePaymentButton", () => {
114117
expect(button).not.toHaveAttribute("disabled");
115118
});
116119

120+
it("should disable button when isPending is true", () => {
121+
mockUsePayPalOneTimePaymentSession.mockReturnValue({
122+
error: null,
123+
isPending: true,
124+
handleClick: mockHandleClick,
125+
});
126+
const { container } = render(
127+
<PayPalOneTimePaymentButton
128+
onApprove={() => Promise.resolve()}
129+
orderId="123"
130+
presentationMode="auto"
131+
/>,
132+
);
133+
const button = container.querySelector("paypal-button");
134+
expect(button).toBeInTheDocument();
135+
expect(button).toHaveAttribute("disabled");
136+
});
137+
117138
it("should pass type prop to paypal-button", () => {
118139
const { container } = render(
119140
<PayPalOneTimePaymentButton
@@ -149,6 +170,7 @@ describe("PayPalOneTimePaymentButton", () => {
149170
const testError = new Error("Test error");
150171
mockUsePayPalOneTimePaymentSession.mockReturnValue({
151172
error: testError,
173+
isPending: false,
152174
handleClick: mockHandleClick,
153175
});
154176
render(

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export const PayPalOneTimePaymentButton = ({
3535
disabled = false,
3636
...hookProps
3737
}: PayPalOneTimePaymentButtonProps): JSX.Element | null => {
38-
const { error, handleClick } = usePayPalOneTimePaymentSession(hookProps);
38+
const { error, isPending, handleClick } =
39+
usePayPalOneTimePaymentSession(hookProps);
3940
const isServerSide = isServer();
4041

4142
useEffect(() => {
@@ -50,7 +51,9 @@ export const PayPalOneTimePaymentButton = ({
5051
<paypal-button
5152
onClick={handleClick}
5253
type={type}
53-
disabled={disabled || error !== null ? true : undefined}
54+
disabled={
55+
disabled || isPending || error !== null ? true : undefined
56+
}
5457
></paypal-button>
5558
);
5659
};

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ type PayPalProviderProps = Omit<
8181
* >
8282
* <PayPalButton />
8383
* </PayPalProvider>
84+
*
85+
* @example
86+
* // Show custom loader while SDK initializes
87+
* function MyCheckout() {
88+
* const { loadingStatus } = usePayPal();
89+
* const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
90+
*
91+
* if (isPending) {
92+
* return <div>Loading PayPal SDK...</div>;
93+
* }
94+
*
95+
* return <PayPalButton orderId="ORDER-123" />;
96+
* }
97+
*
98+
* <PayPalProvider clientToken={token} pageType="checkout">
99+
* <MyCheckout />
100+
* </PayPalProvider>
84101
*/
85102
export const PayPalProvider: React.FC<PayPalProviderProps> = ({
86103
clientMetadataId,

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe("PayPalSavePaymentButton", () => {
2828
jest.clearAllMocks();
2929
mockUsePayPalSavePaymentSession.mockReturnValue({
3030
error: null,
31+
isPending: false,
3132
handleClick: mockHandleClick,
3233
});
3334
mockIsServer.mockReturnValue(false);
@@ -74,6 +75,7 @@ describe("PayPalSavePaymentButton", () => {
7475
jest.spyOn(console, "error").mockImplementation();
7576
mockUsePayPalSavePaymentSession.mockReturnValue({
7677
error: new Error("Test error"),
78+
isPending: false,
7779
handleClick: mockHandleClick,
7880
});
7981
const { container } = render(
@@ -86,6 +88,7 @@ describe("PayPalSavePaymentButton", () => {
8688
it("should not disable button when error is null", () => {
8789
mockUsePayPalSavePaymentSession.mockReturnValue({
8890
error: null,
91+
isPending: false,
8992
handleClick: mockHandleClick,
9093
});
9194
const { container } = render(
@@ -95,6 +98,20 @@ describe("PayPalSavePaymentButton", () => {
9598
expect(button).not.toHaveAttribute("disabled");
9699
});
97100

101+
it("should disable button when isPending is true", () => {
102+
mockUsePayPalSavePaymentSession.mockReturnValue({
103+
error: null,
104+
isPending: true,
105+
handleClick: mockHandleClick,
106+
});
107+
const { container } = render(
108+
<PayPalSavePaymentButton {...defaultProps} />,
109+
);
110+
const button = container.querySelector("paypal-button");
111+
expect(button).toBeInTheDocument();
112+
expect(button).toHaveAttribute("disabled");
113+
});
114+
98115
it("should pass type prop to paypal-button", () => {
99116
const { container } = render(
100117
<PayPalSavePaymentButton {...defaultProps} type="subscribe" />,
@@ -120,6 +137,7 @@ describe("PayPalSavePaymentButton", () => {
120137
const testError = new Error("Test error");
121138
mockUsePayPalSavePaymentSession.mockReturnValue({
122139
error: testError,
140+
isPending: false,
123141
handleClick: mockHandleClick,
124142
});
125143
render(<PayPalSavePaymentButton {...defaultProps} />);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export const PayPalSavePaymentButton = ({
3636
disabled = false,
3737
...hookProps
3838
}: PayPalSavePaymentButtonProps): JSX.Element | null => {
39-
const { error, handleClick } = usePayPalSavePaymentSession(hookProps);
39+
const { error, isPending, handleClick } =
40+
usePayPalSavePaymentSession(hookProps);
4041
const isServerSide = isServer();
4142

4243
useEffect(() => {
@@ -50,7 +51,9 @@ export const PayPalSavePaymentButton = ({
5051
) : (
5152
<paypal-button
5253
type={type}
53-
disabled={disabled || error !== null ? true : undefined}
54+
disabled={
55+
disabled || isPending || error !== null ? true : undefined
56+
}
5457
onClick={handleClick}
5558
></paypal-button>
5659
);

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe("VenmoOneTimePaymentButton", () => {
2222
jest.clearAllMocks();
2323
mockUseVenmoOneTimePaymentSession.mockReturnValue({
2424
error: null,
25+
isPending: false,
2526
handleClick: mockHandleClick,
2627
});
2728
mockIsServer.mockReturnValue(false);
@@ -84,6 +85,7 @@ describe("VenmoOneTimePaymentButton", () => {
8485
jest.spyOn(console, "error").mockImplementation();
8586
mockUseVenmoOneTimePaymentSession.mockReturnValue({
8687
error: new Error("Test error"),
88+
isPending: false,
8789
handleClick: mockHandleClick,
8890
});
8991
const { container } = render(
@@ -100,6 +102,7 @@ describe("VenmoOneTimePaymentButton", () => {
100102
it("should not disable button when error is null", () => {
101103
mockUseVenmoOneTimePaymentSession.mockReturnValue({
102104
error: null,
105+
isPending: false,
103106
handleClick: mockHandleClick,
104107
});
105108
const { container } = render(
@@ -113,6 +116,24 @@ describe("VenmoOneTimePaymentButton", () => {
113116
expect(button).not.toHaveAttribute("disabled");
114117
});
115118

119+
it("should disable button when isPending is true", () => {
120+
mockUseVenmoOneTimePaymentSession.mockReturnValue({
121+
error: null,
122+
isPending: true,
123+
handleClick: mockHandleClick,
124+
});
125+
const { container } = render(
126+
<VenmoOneTimePaymentButton
127+
onApprove={() => Promise.resolve()}
128+
orderId="123"
129+
presentationMode="auto"
130+
/>,
131+
);
132+
const button = container.querySelector("venmo-button");
133+
expect(button).toBeInTheDocument();
134+
expect(button).toHaveAttribute("disabled");
135+
});
136+
116137
it("should pass type prop to venmo-button", () => {
117138
const { container } = render(
118139
<VenmoOneTimePaymentButton
@@ -148,6 +169,7 @@ describe("VenmoOneTimePaymentButton", () => {
148169
const testError = new Error("Test error");
149170
mockUseVenmoOneTimePaymentSession.mockReturnValue({
150171
error: testError,
172+
isPending: false,
151173
handleClick: mockHandleClick,
152174
});
153175
render(

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export const VenmoOneTimePaymentButton = ({
3131
disabled = false,
3232
...hookProps
3333
}: VenmoOneTimePaymentButtonProps): JSX.Element | null => {
34-
const { error, handleClick } = useVenmoOneTimePaymentSession(hookProps);
34+
const { error, isPending, handleClick } =
35+
useVenmoOneTimePaymentSession(hookProps);
3536
const isServerSide = isServer();
3637

3738
useEffect(() => {
@@ -46,7 +47,9 @@ export const VenmoOneTimePaymentButton = ({
4647
<venmo-button
4748
onClick={handleClick}
4849
type={type}
49-
disabled={disabled || error !== null ? true : undefined}
50+
disabled={
51+
disabled || isPending || error !== null ? true : undefined
52+
}
5053
></venmo-button>
5154
);
5255
};

0 commit comments

Comments
 (0)