Skip to content

Commit 6453079

Browse files
authored
Merge pull request #771 from paypal/feature/4086-cardfields-savepayment-hook
Card fields save payment hook
2 parents 62bf29b + 0859966 commit 6453079

File tree

8 files changed

+298
-24
lines changed

8 files changed

+298
-24
lines changed

.changeset/bumpy-drinks-greet.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+
Created usePayPalCardFieldsSavePaymentSession hook

.changeset/slick-bottles-retire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@paypal/paypal-js": patch
3+
---
4+
5+
Renamed V6 Card Fields submit methods arguments names to better represent expected values

packages/paypal-js/types/v6/components/card-fields.d.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,22 +200,23 @@ export type OneTimePaymentFlowResponse = {
200200

201201
export type OneTimePaymentSubmitOptions = [
202202
orderId: string,
203-
data?: SubmitOptions,
203+
options?: SubmitOptions,
204204
];
205205

206206
export type CardFieldsOneTimePaymentSession = BaseCardFieldsSession & {
207207
/**
208208
* Use this method to submit a one-time payment using Card Fields.
209209
*
210210
* @param orderId - The unique identifier for the order to be processed.
211-
* @param data - Additional payment data.
211+
* @param options - Additional payment options.
212212
* @returns A promise that resolves to the result of the payment flow.
213213
*
214214
* @example
215215
* ```typescript
216216
* const response = await cardFieldsInstance.submit("your-order-id", {
217217
billingAddress: {
218218
postalCode: "12345",
219+
countryCode: "US",
219220
},
220221
});
221222
* ```
@@ -236,22 +237,23 @@ export type SavePaymentFlowResponse = {
236237

237238
export type SavePaymentSubmitOptions = [
238239
vaultSetupToken: string,
239-
data?: SubmitOptions,
240+
options?: SubmitOptions,
240241
];
241242

242243
export type CardFieldsSavePaymentSession = BaseCardFieldsSession & {
243244
/**
244245
* Use this method to submit and save a payment method using Card Fields.
245246
*
246247
* @param vaultSetupToken - The unique token for the vault setup to be processed.
247-
* @param data - Additional payment data.
248+
* @param options - Additional payment options.
248249
* @returns A promise that resolves to the result of the save payment flow.
249250
*
250251
* @example
251252
* ```typescript
252253
* const response = await cardFieldsInstance.submit("your-vault-setup-token", {
253254
billingAddress: {
254255
postalCode: "12345",
256+
countryCode: "US",
255257
},
256258
});
257259
* ```

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
CardFieldsStatusState,
2626
} from "../context/PayPalCardFieldsProviderContext";
2727
import type { usePayPalCardFieldsOneTimePaymentSession } from "../hooks/usePayPalCardFieldsOneTimePaymentSession";
28+
import type { usePayPalCardFieldsSavePaymentSession } from "../hooks/usePayPalCardFieldsSavePaymentSession";
2829

2930
export type CardFieldsSession =
3031
| CardFieldsOneTimePaymentSession
@@ -47,7 +48,7 @@ type CardFieldsProviderProps = {
4748
*
4849
* @remarks
4950
* Child components must use either {@link usePayPalCardFieldsOneTimePaymentSession} or
50-
* usePayPalCardFieldsSavePaymentSession to initialize the appropriate session type.
51+
* {@link usePayPalCardFieldsSavePaymentSession} to initialize the appropriate session type.
5152
* The session will not be created until one of these hooks is called.
5253
*
5354
* @example

packages/react-paypal-js/src/v6/hooks/usePayPalCardFieldsOneTimePaymentSession.test.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe("usePayPalCardFieldsOneTimePaymentSession", () => {
7373
});
7474

7575
describe("submit", () => {
76-
test("should call the session's submit method with correct arguments", async () => {
76+
test("should update submitResponse with the session's submit response", async () => {
7777
const { result } = renderHook(() =>
7878
usePayPalCardFieldsOneTimePaymentSession(),
7979
);
@@ -85,25 +85,14 @@ describe("usePayPalCardFieldsOneTimePaymentSession", () => {
8585
expect(
8686
mockCardFieldsOneTimePaymentSession.submit,
8787
).toHaveBeenCalledWith(mockOrderId, mockOptions);
88-
});
89-
90-
test("should update submitResponse with the session's submit response", async () => {
91-
const { result } = renderHook(() =>
92-
usePayPalCardFieldsOneTimePaymentSession(),
93-
);
94-
95-
await act(async () => {
96-
await result.current.submit(mockOrderId, mockOptions);
97-
});
98-
9988
expect(result.current.submitResponse).toEqual(
10089
mockSuccessfulSubmitResponse,
10190
);
10291
expect(result.current.error).toBeNull();
10392
expectCurrentErrorValue(result.current.error);
10493
});
10594

106-
test("should set error when session is not available", () => {
95+
test("should set error when session is not available", async () => {
10796
mockUsePayPalCardFieldsSession.mockReturnValueOnce({
10897
cardFieldsSession: null,
10998
setCardFieldsSessionType: mockSetCardFieldsSessionType,
@@ -113,21 +102,24 @@ describe("usePayPalCardFieldsOneTimePaymentSession", () => {
113102
usePayPalCardFieldsOneTimePaymentSession(),
114103
);
115104

116-
act(() => {
117-
result.current.submit(mockOrderId, mockOptions);
105+
await act(async () => {
106+
await result.current.submit(mockOrderId, mockOptions);
118107
});
119108

120109
const expectedError = toError(
121110
"Submit error: CardFields session not available",
122111
);
123112

113+
expect(
114+
mockCardFieldsOneTimePaymentSession.submit,
115+
).not.toHaveBeenCalled();
124116
expect(result.current.submitResponse).toBeNull();
125117
expect(result.current.error).toEqual(expectedError);
126118
expectCurrentErrorValue(result.current.error);
127119
});
128120

129121
test("should set error when session's submit rejects", async () => {
130-
const submitRejectError = new Error("Submit failed");
122+
const submitRejectError = toError("Submit failed");
131123
const newMockCardFieldsOneTimePaymentSession =
132124
createMockCardFieldsOneTimePaymentSession({
133125
submit: jest.fn().mockRejectedValue(submitRejectError),
@@ -151,15 +143,15 @@ describe("usePayPalCardFieldsOneTimePaymentSession", () => {
151143
expectCurrentErrorValue(result.current.error);
152144
});
153145

154-
test("should handle orderId promise", async () => {
146+
test("should handle orderId being a promise", async () => {
155147
const { result } = renderHook(() =>
156148
usePayPalCardFieldsOneTimePaymentSession(),
157149
);
158150

159151
const orderIdPromise = Promise.resolve(mockOrderId);
160152

161153
await act(async () => {
162-
result.current.submit(orderIdPromise, mockOptions);
154+
await result.current.submit(orderIdPromise, mockOptions);
163155
});
164156

165157
expect(
@@ -177,13 +169,16 @@ describe("usePayPalCardFieldsOneTimePaymentSession", () => {
177169
usePayPalCardFieldsOneTimePaymentSession(),
178170
);
179171

180-
const orderIdError = new Error("Order ID fetch failed");
172+
const orderIdError = toError("Order ID fetch failed");
181173
const orderIdPromise = Promise.reject(orderIdError);
182174

183175
await act(async () => {
184176
await result.current.submit(orderIdPromise, mockOptions);
185177
});
186178

179+
expect(
180+
mockCardFieldsOneTimePaymentSession.submit,
181+
).not.toHaveBeenCalled();
187182
expect(result.current.submitResponse).toBeNull();
188183
expect(result.current.error).toEqual(orderIdError);
189184
expectCurrentErrorValue(result.current.error);
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { act, renderHook } from "@testing-library/react-hooks";
2+
3+
import { expectCurrentErrorValue } from "./useErrorTestUtil";
4+
import { usePayPalCardFieldsSession } from "./usePayPalCardFields";
5+
import { usePayPalCardFieldsSavePaymentSession } from "./usePayPalCardFieldsSavePaymentSession";
6+
import { CARD_FIELDS_SESSION_TYPES } from "../components/PayPalCardFieldsProvider";
7+
import { toError } from "../utils";
8+
9+
import type {
10+
CardFieldsSavePaymentSession,
11+
SavePaymentFlowResponse,
12+
} from "../types";
13+
14+
jest.mock("./usePayPalCardFields");
15+
16+
const mockUsePayPalCardFieldsSession =
17+
usePayPalCardFieldsSession as jest.MockedFunction<
18+
typeof usePayPalCardFieldsSession
19+
>;
20+
21+
const mockVaultSetupToken = "test-vault-setup-token";
22+
23+
const mockOptions = { billingAddress: { countryCode: "US" } };
24+
25+
const mockSuccessfulSubmitResponse: SavePaymentFlowResponse = {
26+
data: {
27+
vaultSetupToken: mockVaultSetupToken,
28+
},
29+
state: "succeeded",
30+
};
31+
32+
const mockSetCardFieldsSessionType = jest.fn();
33+
34+
// Mock Factories
35+
const createMockCardFieldsSavePaymentSession = (
36+
overrides?: Partial<CardFieldsSavePaymentSession>,
37+
): CardFieldsSavePaymentSession => ({
38+
submit: jest.fn().mockResolvedValue(undefined),
39+
on: jest.fn(),
40+
update: jest.fn(),
41+
createCardFieldsComponent: jest.fn(),
42+
...overrides,
43+
});
44+
45+
describe("usePayPalCardFieldsSavePaymentSession", () => {
46+
let mockCardFieldsSavePaymentSession: CardFieldsSavePaymentSession;
47+
48+
beforeEach(() => {
49+
mockCardFieldsSavePaymentSession =
50+
createMockCardFieldsSavePaymentSession({
51+
submit: jest
52+
.fn()
53+
.mockResolvedValue(mockSuccessfulSubmitResponse),
54+
});
55+
56+
mockUsePayPalCardFieldsSession.mockReturnValue({
57+
cardFieldsSession: mockCardFieldsSavePaymentSession,
58+
setCardFieldsSessionType: mockSetCardFieldsSessionType,
59+
});
60+
});
61+
62+
afterEach(() => {
63+
jest.clearAllMocks();
64+
});
65+
66+
test("should call setCardFieldsSessionType with 'save-payment' value on mount", () => {
67+
renderHook(() => usePayPalCardFieldsSavePaymentSession());
68+
69+
expect(mockSetCardFieldsSessionType).toHaveBeenCalledTimes(1);
70+
expect(mockSetCardFieldsSessionType).toHaveBeenCalledWith(
71+
CARD_FIELDS_SESSION_TYPES.SAVE_PAYMENT,
72+
);
73+
});
74+
75+
describe("submit", () => {
76+
test("should update submitResponse with the session's submit response", async () => {
77+
const { result } = renderHook(() =>
78+
usePayPalCardFieldsSavePaymentSession(),
79+
);
80+
81+
await act(async () => {
82+
await result.current.submit(mockVaultSetupToken, mockOptions);
83+
});
84+
85+
expect(
86+
mockCardFieldsSavePaymentSession.submit,
87+
).toHaveBeenCalledWith(mockVaultSetupToken, mockOptions);
88+
expect(result.current.submitResponse).toEqual(
89+
mockSuccessfulSubmitResponse,
90+
);
91+
expect(result.current.error).toBeNull();
92+
expectCurrentErrorValue(result.current.error);
93+
});
94+
95+
test("should set error when session is not available", async () => {
96+
mockUsePayPalCardFieldsSession.mockReturnValueOnce({
97+
cardFieldsSession: null,
98+
setCardFieldsSessionType: mockSetCardFieldsSessionType,
99+
});
100+
101+
const { result } = renderHook(() =>
102+
usePayPalCardFieldsSavePaymentSession(),
103+
);
104+
105+
await act(async () => {
106+
await result.current.submit(mockVaultSetupToken, mockOptions);
107+
});
108+
109+
const expectedError = toError(
110+
"Submit error: CardFields session not available",
111+
);
112+
113+
expect(
114+
mockCardFieldsSavePaymentSession.submit,
115+
).not.toHaveBeenCalled();
116+
expect(result.current.submitResponse).toBeNull();
117+
expect(result.current.error).toEqual(expectedError);
118+
expectCurrentErrorValue(result.current.error);
119+
});
120+
121+
test("should set error when session's submit rejects", async () => {
122+
const submitRejectError = toError("Submit failed");
123+
const newMockCardFieldsSavePaymentSession =
124+
createMockCardFieldsSavePaymentSession({
125+
submit: jest.fn().mockRejectedValue(submitRejectError),
126+
});
127+
128+
mockUsePayPalCardFieldsSession.mockReturnValueOnce({
129+
cardFieldsSession: newMockCardFieldsSavePaymentSession,
130+
setCardFieldsSessionType: mockSetCardFieldsSessionType,
131+
});
132+
133+
const { result } = renderHook(() =>
134+
usePayPalCardFieldsSavePaymentSession(),
135+
);
136+
137+
await act(async () => {
138+
await result.current.submit(mockVaultSetupToken, mockOptions);
139+
});
140+
141+
expect(result.current.submitResponse).toBeNull();
142+
expect(result.current.error).toEqual(submitRejectError);
143+
expectCurrentErrorValue(result.current.error);
144+
});
145+
146+
test("should handle vaultSetupToken being a promise", async () => {
147+
const { result } = renderHook(() =>
148+
usePayPalCardFieldsSavePaymentSession(),
149+
);
150+
151+
const vaultSetupTokenPromise = Promise.resolve(mockVaultSetupToken);
152+
153+
await act(async () => {
154+
await result.current.submit(
155+
vaultSetupTokenPromise,
156+
mockOptions,
157+
);
158+
});
159+
160+
expect(
161+
mockCardFieldsSavePaymentSession.submit,
162+
).toHaveBeenCalledWith(mockVaultSetupToken, mockOptions);
163+
expect(result.current.submitResponse).toEqual(
164+
mockSuccessfulSubmitResponse,
165+
);
166+
expect(result.current.error).toBeNull();
167+
expectCurrentErrorValue(result.current.error);
168+
});
169+
170+
test("should set error when vaultSetupToken promise rejects", async () => {
171+
const { result } = renderHook(() =>
172+
usePayPalCardFieldsSavePaymentSession(),
173+
);
174+
175+
const vaultSetupTokenError = toError("Vault Setup Token failed");
176+
const vaultSetupTokenPromise = Promise.reject(vaultSetupTokenError);
177+
178+
await act(async () => {
179+
await result.current.submit(
180+
vaultSetupTokenPromise,
181+
mockOptions,
182+
);
183+
});
184+
185+
expect(
186+
mockCardFieldsSavePaymentSession.submit,
187+
).not.toHaveBeenCalled();
188+
expect(result.current.submitResponse).toBeNull();
189+
expect(result.current.error).toEqual(vaultSetupTokenError);
190+
expectCurrentErrorValue(result.current.error);
191+
});
192+
});
193+
});

0 commit comments

Comments
 (0)