@paypal/checkout-components 5.0.301 → 5.0.302-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ /* @flow */
2
+ import { vi, describe, expect } from "vitest";
3
+
4
+ import { ValidationError } from "../lib";
5
+
6
+ import { ShopperSession } from "./shopperSession";
7
+
8
+ const mockStateObject = {};
9
+ const mockStorage = {
10
+ get: (key) => mockStateObject[key],
11
+ set: (key, value) => {
12
+ mockStateObject[key] = value;
13
+ },
14
+ };
15
+
16
+ const mockFindEligiblePaymentsRequest = (
17
+ eligibility = {
18
+ paypal: {
19
+ can_be_vaulted: true,
20
+ eligible_in_paypal_network: true,
21
+ recommended: true,
22
+ recommended_priority: 1,
23
+ },
24
+ }
25
+ ) =>
26
+ vi.fn().mockResolvedValue({
27
+ body: {
28
+ eligible_methods: eligibility,
29
+ },
30
+ });
31
+
32
+ const defaultSdkConfig = {
33
+ sdkToken: "sdk client token",
34
+ pageType: "checkout",
35
+ clientToken: "",
36
+ userIDToken: "",
37
+ paypalApiDomain: "https://api.paypal.com",
38
+ environment: "test",
39
+ buyerCountry: "US",
40
+ currency: "USD",
41
+ };
42
+
43
+ const createShopperSession = ({
44
+ sdkConfig = defaultSdkConfig,
45
+ logger = {
46
+ info: vi.fn(),
47
+ warn: vi.fn(),
48
+ error: vi.fn(),
49
+ track: vi.fn(),
50
+ metricCounter: vi.fn(),
51
+ },
52
+ sessionState = mockStorage,
53
+ request = mockFindEligiblePaymentsRequest(),
54
+ } = {}) =>
55
+ new ShopperSession({
56
+ sdkConfig,
57
+ // $FlowIssue
58
+ logger,
59
+ sessionState,
60
+ request,
61
+ });
62
+
63
+ describe("shopper insights component - getRecommendedPaymentMethods()", () => {
64
+ afterEach(() => {
65
+ vi.clearAllMocks();
66
+ });
67
+
68
+ test("should get recommended payment methods using the shopper insights API", async () => {
69
+ const shopperSession = createShopperSession();
70
+ const recommendedPaymentMethods =
71
+ await shopperSession.getRecommendedPaymentMethods({
72
+ email: "email@test.com",
73
+ phone: {
74
+ countryCode: "1",
75
+ nationalNumber: "2345678901",
76
+ },
77
+ });
78
+
79
+ expect.assertions(1);
80
+ expect(recommendedPaymentMethods).toEqual({
81
+ isPayPalRecommended: true,
82
+ isVenmoRecommended: false,
83
+ });
84
+ });
85
+
86
+ test("catch errors from the API", async () => {
87
+ const mockRequest = vi.fn().mockRejectedValue(new Error("Error with API"));
88
+ const shopperSession = createShopperSession({ request: mockRequest });
89
+
90
+ expect.assertions(2);
91
+ await expect(() =>
92
+ shopperSession.getRecommendedPaymentMethods({
93
+ email: "email@test.com",
94
+ phone: {
95
+ countryCode: "1",
96
+ nationalNumber: "2345678905",
97
+ },
98
+ })
99
+ ).rejects.toThrow(new Error("Error with API"));
100
+ expect(mockRequest).toHaveBeenCalled();
101
+ });
102
+
103
+ test("create payload with email and phone number", async () => {
104
+ const mockRequest = mockFindEligiblePaymentsRequest();
105
+ const shopperSession = createShopperSession({ request: mockRequest });
106
+ await shopperSession.getRecommendedPaymentMethods({
107
+ email: "email10@test.com",
108
+ phone: {
109
+ countryCode: "1",
110
+ nationalNumber: "2345678906",
111
+ },
112
+ });
113
+
114
+ expect(mockRequest).toHaveBeenCalledWith(
115
+ expect.objectContaining({
116
+ data: expect.objectContaining({
117
+ customer: expect.objectContaining({
118
+ email: "email10@test.com",
119
+ phone: expect.objectContaining({
120
+ country_code: "1",
121
+ national_number: "2345678906",
122
+ }),
123
+ }),
124
+ }),
125
+ })
126
+ );
127
+ });
128
+
129
+ test("create payload with email only", async () => {
130
+ const mockRequest = mockFindEligiblePaymentsRequest();
131
+ const shopperSession = createShopperSession({ request: mockRequest });
132
+ await shopperSession.getRecommendedPaymentMethods({
133
+ email: "email2@test.com",
134
+ });
135
+
136
+ expect(mockRequest).toHaveBeenCalledWith(
137
+ expect.objectContaining({
138
+ data: expect.objectContaining({
139
+ customer: expect.objectContaining({
140
+ email: "email2@test.com",
141
+ }),
142
+ }),
143
+ })
144
+ );
145
+ });
146
+
147
+ test("create payload with phone only", async () => {
148
+ const mockRequest = mockFindEligiblePaymentsRequest();
149
+ const shopperSession = createShopperSession({ request: mockRequest });
150
+ await shopperSession.getRecommendedPaymentMethods({
151
+ email: "email5@test.com",
152
+ phone: {
153
+ countryCode: "1",
154
+ nationalNumber: "2345678901",
155
+ },
156
+ });
157
+
158
+ expect(mockRequest).toHaveBeenCalledWith(
159
+ expect.objectContaining({
160
+ data: expect.objectContaining({
161
+ customer: expect.objectContaining({
162
+ phone: expect.objectContaining({
163
+ country_code: "1",
164
+ national_number: "2345678901",
165
+ }),
166
+ }),
167
+ }),
168
+ })
169
+ );
170
+ });
171
+
172
+ test("throw error for invalid email", () => {
173
+ const mockRequest = mockFindEligiblePaymentsRequest();
174
+ const shopperSession = createShopperSession({ request: mockRequest });
175
+
176
+ expect(
177
+ shopperSession.getRecommendedPaymentMethods({
178
+ email: "not_an_email",
179
+ phone: {
180
+ countryCode: "1",
181
+ nationalNumber: "2345678901",
182
+ },
183
+ })
184
+ ).rejects.toEqual(
185
+ new ValidationError(
186
+ "Expected shopper information to include a valid email format"
187
+ )
188
+ );
189
+ });
190
+
191
+ test("throw error for invalid phone number", () => {
192
+ const mockRequest = mockFindEligiblePaymentsRequest();
193
+ const shopperSession = createShopperSession({ request: mockRequest });
194
+
195
+ expect(
196
+ shopperSession.getRecommendedPaymentMethods({
197
+ phone: {
198
+ countryCode: "1",
199
+ nationalNumber: "not a phone",
200
+ },
201
+ })
202
+ ).rejects.toEqual(
203
+ new ValidationError(
204
+ "Expected shopper information to be a valid phone number format"
205
+ )
206
+ );
207
+ });
208
+
209
+ test("throw error for missing phone number attributes", () => {
210
+ const mockRequest = mockFindEligiblePaymentsRequest();
211
+ const shopperSession = createShopperSession({ request: mockRequest });
212
+
213
+ expect(
214
+ shopperSession.getRecommendedPaymentMethods({
215
+ phone: {
216
+ nationalNumber: "2345678901",
217
+ },
218
+ })
219
+ ).rejects.toEqual(
220
+ new ValidationError(
221
+ "Expected phone number for shopper insights to include nationalNumber and countryCode"
222
+ )
223
+ );
224
+
225
+ expect(
226
+ shopperSession.getRecommendedPaymentMethods({
227
+ phone: {
228
+ countryCode: "1",
229
+ },
230
+ })
231
+ ).rejects.toEqual(
232
+ new ValidationError(
233
+ "Expected phone number for shopper insights to include nationalNumber and countryCode"
234
+ )
235
+ );
236
+ });
237
+
238
+ test("should default purchase units with currency code in the payload", async () => {
239
+ const mockRequest = mockFindEligiblePaymentsRequest();
240
+ const shopperSession = createShopperSession({ request: mockRequest });
241
+ await shopperSession.getRecommendedPaymentMethods({
242
+ email: "email6@test.com",
243
+ });
244
+
245
+ expect(mockRequest).toHaveBeenCalledWith(
246
+ expect.objectContaining({
247
+ data: expect.objectContaining({
248
+ purchase_units: expect.arrayContaining([
249
+ expect.objectContaining({
250
+ amount: expect.objectContaining({
251
+ currency_code: "USD",
252
+ }),
253
+ }),
254
+ ]),
255
+ }),
256
+ })
257
+ );
258
+ });
259
+
260
+ test("should use the SDK buyer-country parameter if country code is not passed in a non-prod env", async () => {
261
+ const mockRequest = mockFindEligiblePaymentsRequest();
262
+ const shopperSession = createShopperSession({
263
+ request: mockRequest,
264
+ sdkConfig: {
265
+ ...defaultSdkConfig,
266
+ environment: "test",
267
+ buyerCountry: "US",
268
+ },
269
+ });
270
+ await shopperSession.getRecommendedPaymentMethods({
271
+ email: "email7@test.com",
272
+ });
273
+
274
+ expect(mockRequest).toHaveBeenCalledWith(
275
+ expect.objectContaining({
276
+ data: expect.objectContaining({
277
+ customer: expect.objectContaining({
278
+ country_code: "US",
279
+ }),
280
+ }),
281
+ })
282
+ );
283
+ });
284
+
285
+ test("should not set country code in prod env in the payload", async () => {
286
+ const mockRequest = mockFindEligiblePaymentsRequest();
287
+ const shopperSession = createShopperSession({
288
+ request: mockRequest,
289
+ sdkConfig: {
290
+ ...defaultSdkConfig,
291
+ environment: "production",
292
+ buyerCountry: "US",
293
+ },
294
+ });
295
+ await shopperSession.getRecommendedPaymentMethods({
296
+ email: "email@test.com",
297
+ });
298
+
299
+ // $FlowIssue
300
+ expect(mockRequest.mock.calls[0][0].data.customer.country_code).toEqual(
301
+ undefined
302
+ );
303
+ });
304
+
305
+ test("should request recommended payment methods by setting account details in the payload", async () => {
306
+ const mockRequest = mockFindEligiblePaymentsRequest();
307
+ const shopperSession = createShopperSession({ request: mockRequest });
308
+ await shopperSession.getRecommendedPaymentMethods({
309
+ email: "email9@test.com",
310
+ });
311
+
312
+ expect(mockRequest).toHaveBeenCalledWith(
313
+ expect.objectContaining({
314
+ data: expect.objectContaining({
315
+ preferences: expect.objectContaining({
316
+ include_account_details: true,
317
+ }),
318
+ }),
319
+ })
320
+ );
321
+ });
322
+ });
323
+
324
+ describe("shopper insights component - validateSdkConfig()", () => {
325
+ afterEach(() => {
326
+ vi.clearAllMocks();
327
+ });
328
+
329
+ test("should throw if sdk token is not passed", () => {
330
+ expect(() =>
331
+ createShopperSession({
332
+ sdkConfig: {
333
+ ...defaultSdkConfig,
334
+ sdkToken: "",
335
+ pageType: "",
336
+ userIDToken: "",
337
+ clientToken: "",
338
+ },
339
+ })
340
+ ).toThrowError(
341
+ "script data attribute sdk-client-token is required but was not passed"
342
+ );
343
+ });
344
+
345
+ test("should throw if page type is not passed", () => {
346
+ expect(() =>
347
+ createShopperSession({
348
+ sdkConfig: {
349
+ ...defaultSdkConfig,
350
+ sdkToken: "sdk-token",
351
+ pageType: "",
352
+ userIDToken: "",
353
+ clientToken: "",
354
+ },
355
+ })
356
+ ).toThrowError(
357
+ "script data attribute page-type is required but was not passed"
358
+ );
359
+ });
360
+
361
+ test("should throw if ID token is passed", () => {
362
+ expect(() =>
363
+ createShopperSession({
364
+ sdkConfig: {
365
+ ...defaultSdkConfig,
366
+ sdkToken: "sdk-token",
367
+ pageType: "product-listing",
368
+ userIDToken: "id-token",
369
+ clientToken: "",
370
+ },
371
+ })
372
+ ).toThrowError(
373
+ "use script data attribute sdk-client-token instead of user-id-token"
374
+ );
375
+ });
376
+ });
@@ -1,120 +0,0 @@
1
- /* @flow */
2
-
3
- import { getLogger } from "@paypal/sdk-client/src";
4
-
5
- import {
6
- SHOPPER_INSIGHTS_METRIC_NAME,
7
- type MerchantPayloadData,
8
- } from "../../constants/api";
9
- import { ValidationError } from "../../lib";
10
-
11
- type MerchantConfigParams = {|
12
- sdkToken: ?string,
13
- pageType: ?string,
14
- userIDToken: ?string,
15
- clientToken: ?string,
16
- |};
17
-
18
- export function validateMerchantConfig({
19
- sdkToken,
20
- pageType,
21
- userIDToken,
22
- clientToken,
23
- }: MerchantConfigParams) {
24
- const logger = getLogger();
25
- if (!sdkToken) {
26
- logger.metricCounter({
27
- namespace: SHOPPER_INSIGHTS_METRIC_NAME,
28
- event: "error",
29
- dimensions: {
30
- errorType: "merchant_configuration_validation_error",
31
- validationDetails: "sdk_token_not_present",
32
- },
33
- });
34
-
35
- throw new ValidationError(
36
- `script data attribute sdk-client-token is required but was not passed`
37
- );
38
- }
39
-
40
- if (!pageType) {
41
- logger.metricCounter({
42
- namespace: SHOPPER_INSIGHTS_METRIC_NAME,
43
- event: "error",
44
- dimensions: {
45
- errorType: "merchant_configuration_validation_error",
46
- validationDetails: "page_type_not_present",
47
- },
48
- });
49
-
50
- throw new ValidationError(
51
- `script data attribute page-type is required but was not passed`
52
- );
53
- }
54
-
55
- if (userIDToken) {
56
- logger.metricCounter({
57
- namespace: SHOPPER_INSIGHTS_METRIC_NAME,
58
- event: "error",
59
- dimensions: {
60
- errorType: "merchant_configuration_validation_error",
61
- validationDetails: "sdk_token_and_id_token_present",
62
- },
63
- });
64
-
65
- throw new ValidationError(
66
- `use script data attribute sdk-client-token instead of user-id-token`
67
- );
68
- }
69
-
70
- // Client token has widely adopted integrations in the SDK that we do not want
71
- // to support anymore. For now, we will be only enforcing a warning. We should
72
- // expand on this warning with upgrade guides when we have them.
73
- if (clientToken) {
74
- // eslint-disable-next-line no-console
75
- console.warn(`script data attribute client-token is not recommended`);
76
- }
77
- }
78
-
79
- export const hasEmail = (merchantPayload: MerchantPayloadData): boolean =>
80
- Boolean(merchantPayload?.email);
81
-
82
- export const hasPhoneNumber = (merchantPayload: MerchantPayloadData): boolean =>
83
- Boolean(
84
- merchantPayload?.phone?.countryCode &&
85
- merchantPayload?.phone?.nationalNumber
86
- );
87
-
88
- const isValidEmailFormat = (email: ?string): boolean =>
89
- typeof email === "string" && email.length < 320 && /^.+@.+$/.test(email);
90
-
91
- const isValidPhoneNumberFormat = (phoneNumber: ?string): boolean =>
92
- typeof phoneNumber === "string" && /\d{5,}/.test(phoneNumber);
93
-
94
- export function validateMerchantPayload(merchantPayload: MerchantPayloadData) {
95
- const hasEmailOrPhoneNumber =
96
- hasEmail(merchantPayload) || hasPhoneNumber(merchantPayload);
97
- if (typeof merchantPayload !== "object" || !hasEmailOrPhoneNumber) {
98
- throw new ValidationError(
99
- `Expected either email or phone number for get recommended payment methods`
100
- );
101
- }
102
-
103
- if (
104
- hasEmail(merchantPayload) &&
105
- !isValidEmailFormat(merchantPayload?.email)
106
- ) {
107
- throw new ValidationError(
108
- `Expected shopper information to include a valid email format`
109
- );
110
- }
111
-
112
- if (
113
- hasPhoneNumber(merchantPayload) &&
114
- !isValidPhoneNumberFormat(merchantPayload?.phone?.nationalNumber)
115
- ) {
116
- throw new ValidationError(
117
- `Expected shopper information to be a valid phone number format`
118
- );
119
- }
120
- }
@@ -1,150 +0,0 @@
1
- /* @flow */
2
- import { vi, describe, expect } from "vitest";
3
-
4
- import { validateMerchantConfig, validateMerchantPayload } from "./validation";
5
-
6
- vi.mock("@paypal/sdk-client/src", () => {
7
- return {
8
- getLogger: () => ({
9
- metricCounter: vi.fn().mockReturnThis(),
10
- }),
11
- };
12
- });
13
-
14
- describe("shopper insights merchant SDK config validation", () => {
15
- test("should throw if sdk token is not passed", () => {
16
- expect(() =>
17
- validateMerchantConfig({
18
- sdkToken: "",
19
- pageType: "",
20
- userIDToken: "",
21
- clientToken: "",
22
- })
23
- ).toThrowError(
24
- "script data attribute sdk-client-token is required but was not passed"
25
- );
26
- });
27
-
28
- test("should throw if page type is not passed", () => {
29
- expect(() =>
30
- validateMerchantConfig({
31
- sdkToken: "sdk-token",
32
- pageType: "",
33
- userIDToken: "",
34
- clientToken: "",
35
- })
36
- ).toThrowError(
37
- "script data attribute page-type is required but was not passed"
38
- );
39
- });
40
-
41
- test("should throw if ID token is passed", () => {
42
- expect(() =>
43
- validateMerchantConfig({
44
- sdkToken: "sdk-token",
45
- pageType: "product-listing",
46
- userIDToken: "id-token",
47
- clientToken: "",
48
- })
49
- ).toThrowError(
50
- "use script data attribute sdk-client-token instead of user-id-token"
51
- );
52
- });
53
- });
54
-
55
- describe("shopper insights merchant payload validation", () => {
56
- test("should have successful validation if email is only passed", () => {
57
- expect(() =>
58
- validateMerchantPayload({
59
- email: "email@test.com",
60
- })
61
- ).not.toThrowError();
62
- });
63
-
64
- test("should have successful validation if phone is only passed", () => {
65
- expect(() =>
66
- validateMerchantPayload({
67
- phone: {
68
- countryCode: "1",
69
- nationalNumber: "2345678901",
70
- },
71
- })
72
- ).not.toThrowError();
73
- });
74
-
75
- test("should have successful validation if email and phone is passed", () => {
76
- expect(() =>
77
- validateMerchantPayload({
78
- email: "email@test.com",
79
- phone: {
80
- countryCode: "1",
81
- nationalNumber: "2345678901",
82
- },
83
- })
84
- ).not.toThrowError();
85
- });
86
-
87
- test("should throw if email or phone is not passed", () => {
88
- expect(() => validateMerchantPayload({})).toThrowError(
89
- "Expected either email or phone number for get recommended payment methods"
90
- );
91
-
92
- expect(() =>
93
- // $FlowIssue
94
- validateMerchantPayload()
95
- ).toThrowError(
96
- "Expected either email or phone number for get recommended payment methods"
97
- );
98
- });
99
-
100
- test("should throw if countryCode or nationalNumber in phone is not passed or is empty", () => {
101
- expect.assertions(2);
102
- expect(() =>
103
- validateMerchantPayload({
104
- phone: {
105
- nationalNumber: "",
106
- countryCode: "",
107
- },
108
- })
109
- ).toThrowError(
110
- "Expected either email or phone number for get recommended payment methods"
111
- );
112
-
113
- expect(() =>
114
- validateMerchantPayload(
115
- // $FlowFixMe
116
- { phone: {} }
117
- )
118
- ).toThrowError(
119
- "Expected either email or phone number for get recommended payment methods"
120
- );
121
- });
122
-
123
- test("should throw if phone is in an invalid format", () => {
124
- expect(() =>
125
- validateMerchantPayload({
126
- phone: { countryCode: "1", nationalNumber: "2.354" },
127
- })
128
- ).toThrowError(
129
- "Expected shopper information to be a valid phone number format"
130
- );
131
- expect(() =>
132
- validateMerchantPayload({
133
- phone: { countryCode: "1", nationalNumber: "2-354" },
134
- })
135
- ).toThrowError(
136
- "Expected shopper information to be a valid phone number format"
137
- );
138
- expect.assertions(2);
139
- });
140
-
141
- test("should throw if email is in an invalid format", () => {
142
- expect(() =>
143
- validateMerchantPayload({
144
- email: "123",
145
- })
146
- ).toThrowError(
147
- "Expected shopper information to include a valid email format"
148
- );
149
- });
150
- });