@paypal/checkout-components 5.0.308 → 5.0.309

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paypal/checkout-components",
3
- "version": "5.0.308",
3
+ "version": "5.0.309",
4
4
  "description": "PayPal Checkout components, for integrating checkout products.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,5 +1,7 @@
1
1
  /* @flow */
2
2
 
3
+ import { getLogger } from "@paypal/sdk-client/src";
4
+
3
5
  import { getButtonsComponent } from "../zoid/buttons";
4
6
 
5
7
  import {
@@ -8,6 +10,10 @@ import {
8
10
  getHostedButtonDetails,
9
11
  renderForm,
10
12
  getMerchantID,
13
+ shouldRenderSDKButtons,
14
+ getFlexDirection,
15
+ appendButtonContainer,
16
+ getButtonColor,
11
17
  } from "./utils";
12
18
  import type {
13
19
  HostedButtonsComponent,
@@ -19,12 +25,14 @@ export const getHostedButtonsComponent = (): HostedButtonsComponent => {
19
25
  function HostedButtons({
20
26
  enableDPoP = false,
21
27
  hostedButtonId,
28
+ fundingSources = [],
22
29
  }: HostedButtonsComponentProps): HostedButtonsInstance {
23
30
  const Buttons = getButtonsComponent();
24
31
  const render = async (selector) => {
25
32
  const merchantId = getMerchantID();
26
33
  const { html, htmlScript, style } = await getHostedButtonDetails({
27
34
  hostedButtonId,
35
+ fundingSources,
28
36
  });
29
37
 
30
38
  const { onInit, onClick } = renderForm({
@@ -34,24 +42,60 @@ export const getHostedButtonsComponent = (): HostedButtonsComponent => {
34
42
  selector,
35
43
  });
36
44
 
37
- // $FlowFixMe
38
- Buttons({
45
+ const createOrder = buildHostedButtonCreateOrder({
46
+ enableDPoP,
39
47
  hostedButtonId,
40
- style,
41
- onInit,
48
+ merchantId,
49
+ });
50
+ const onApprove = buildHostedButtonOnApprove({
51
+ enableDPoP,
52
+ hostedButtonId,
53
+ merchantId,
54
+ });
55
+
56
+ const buttonOptions = {
57
+ createOrder,
58
+ hostedButtonId,
59
+ merchantId,
60
+ onApprove,
42
61
  onClick,
43
- createOrder: buildHostedButtonCreateOrder({
44
- enableDPoP,
45
- hostedButtonId,
46
- merchantId,
47
- }),
48
- onApprove: buildHostedButtonOnApprove({
49
- enableDPoP,
50
- hostedButtonId,
51
- merchantId,
52
- }),
53
- }).render(selector);
62
+ onInit,
63
+ style,
64
+ };
65
+
66
+ if (shouldRenderSDKButtons(fundingSources)) {
67
+ const { flexDirection } = getFlexDirection({ ...style });
68
+
69
+ appendButtonContainer({ flexDirection, selector });
70
+
71
+ // Only render 2 buttons max
72
+ // This will be refactored in https://paypal.atlassian.net/browse/DTPPCPSDK-2112 when NCPS team updates their API response
73
+ fundingSources.slice(0, 2).forEach((fundingSource, index) => {
74
+ // $FlowFixMe
75
+ const standaloneButton = Buttons({
76
+ ...buttonOptions,
77
+ fundingSource,
78
+ style: {
79
+ ...style,
80
+ color: getButtonColor(style.color, fundingSource),
81
+ },
82
+ });
83
+
84
+ if (standaloneButton.isEligible()) {
85
+ standaloneButton.render(
86
+ index === 0 ? "#ncp-primary-button" : "#ncp-secondary-button"
87
+ );
88
+ } else {
89
+ getLogger().error(`ncps_standalone_${fundingSource}_ineligible`);
90
+ }
91
+ });
92
+ } else {
93
+ // V1 Experience
94
+ // $FlowFixMe
95
+ Buttons(buttonOptions).render(selector);
96
+ }
54
97
  };
98
+
55
99
  return {
56
100
  render,
57
101
  };
@@ -73,14 +73,82 @@ describe("HostedButtons", () => {
73
73
  );
74
74
  await HostedButtons({
75
75
  hostedButtonId: "B1234567890",
76
+ fundingSources: [],
76
77
  }).render("#example");
77
78
  expect(Buttons).toHaveBeenCalledWith(
78
79
  expect.objectContaining({
79
80
  hostedButtonId: "B1234567890",
80
81
  })
81
82
  );
82
- expect.assertions(1);
83
+ expect(Buttons).toHaveBeenCalledTimes(1);
84
+ expect.assertions(2);
83
85
  });
84
- });
85
86
 
87
+ describe("NCP V2", () => {
88
+ beforeEach(() => {
89
+ const containerId = "#container-id";
90
+ const selector = document.createElement("div");
91
+ selector.setAttribute("id", containerId.slice(1));
92
+ vi.spyOn(document, "querySelector").mockReturnValue(selector);
93
+ });
94
+
95
+ test("paypal.Buttons calls getHostedButtonDetails, invokes v5 of the SDK", async () => {
96
+ const renderMock = vi.fn();
97
+
98
+ const Buttons = vi.fn(() => ({
99
+ render: renderMock,
100
+ isEligible: vi.fn(() => true),
101
+ }));
102
+ // $FlowIssue
103
+ getButtonsComponent.mockImplementationOnce(() => Buttons);
104
+ const HostedButtons = getHostedButtonsComponent();
105
+ // $FlowIssue
106
+ request.mockImplementationOnce(() =>
107
+ // eslint-disable-next-line compat/compat
108
+ Promise.resolve(getHostedButtonDetailsResponse)
109
+ );
110
+ await HostedButtons({
111
+ hostedButtonId: "B1234567890",
112
+ fundingSources: ["paypal", "venmo"],
113
+ }).render("#example");
114
+ expect(Buttons).toHaveBeenCalledWith(
115
+ expect.objectContaining({
116
+ hostedButtonId: "B1234567890",
117
+ })
118
+ );
119
+ expect(Buttons).toHaveBeenCalledTimes(2);
120
+ expect(renderMock).toHaveBeenCalledTimes(2);
121
+ expect.assertions(3);
122
+ });
123
+ });
124
+
125
+ test("only eligible buttons are rendered", async () => {
126
+ const renderMock = vi.fn();
127
+
128
+ const Buttons = vi.fn(() => ({
129
+ render: renderMock,
130
+ isEligible: vi.fn(() => false),
131
+ }));
132
+ // $FlowIssue
133
+ getButtonsComponent.mockImplementationOnce(() => Buttons);
134
+ const HostedButtons = getHostedButtonsComponent();
135
+ // $FlowIssue
136
+ request.mockImplementationOnce(() =>
137
+ // eslint-disable-next-line compat/compat
138
+ Promise.resolve(getHostedButtonDetailsResponse)
139
+ );
140
+ await HostedButtons({
141
+ hostedButtonId: "B1234567890",
142
+ fundingSources: ["paypal", "venmo"],
143
+ }).render("#example");
144
+ expect(Buttons).toHaveBeenCalledWith(
145
+ expect.objectContaining({
146
+ hostedButtonId: "B1234567890",
147
+ })
148
+ );
149
+ expect(Buttons).toHaveBeenCalledTimes(2);
150
+ expect(renderMock).toHaveBeenCalledTimes(0);
151
+ expect.assertions(3);
152
+ });
153
+ });
86
154
  /* eslint-enable no-restricted-globals, promise/no-native */
@@ -1,8 +1,27 @@
1
1
  /* @flow */
2
2
  /* eslint-disable no-restricted-globals, promise/no-native */
3
3
 
4
+ export type Color = string;
5
+ export type FlexDirection = string;
6
+ export type Layout = string;
7
+
8
+ export type FundingSources = string;
9
+ export interface GetFlexDirection {
10
+ flexDirection: FlexDirection;
11
+ }
12
+
13
+ export interface GetFlexDirectionArgs {
14
+ layout: Layout;
15
+ }
16
+
17
+ export interface BuildButtonContainerArgs {
18
+ flexDirection: FlexDirection;
19
+ selector: string | HTMLElement;
20
+ }
21
+
4
22
  export type HostedButtonsComponentProps = {|
5
23
  hostedButtonId: string,
24
+ fundingSources: $ReadOnlyArray<FundingSources>,
6
25
  |};
7
26
 
8
27
  export type GetCallbackProps = {|
@@ -17,6 +17,11 @@ import type {
17
17
  HostedButtonDetailsParams,
18
18
  OnApprove,
19
19
  RenderForm,
20
+ GetFlexDirectionArgs,
21
+ GetFlexDirection,
22
+ BuildButtonContainerArgs,
23
+ Color,
24
+ FundingSources,
20
25
  } from "./types";
21
26
 
22
27
  const entryPoint = "SDK";
@@ -30,9 +35,10 @@ const getHeaders = (accessToken?: string) => ({
30
35
  });
31
36
 
32
37
  export const getMerchantID = (): string | void => {
33
- // The SDK supports mutiple merchant IDs, but hosted buttons only
34
- // have one merchant id as a query parameter to the SDK script.
38
+ // The SDK supports Multi-Seller Payments (MSP, i.e sending multiple merchant IDs), but hosted buttons
39
+ // does not support this. Only one merchant id can be passed as a query parameter to the SDK script
35
40
  // https://github.com/paypal/paypal-sdk-client/blob/c58e35f8f7adbab76523eb25b9c10543449d2d29/src/script.js#L144
41
+ // https://developer.paypal.com/docs/multiparty/checkout/multiseller-payments/
36
42
  const merchantIds = getSDKMerchantID();
37
43
  if (merchantIds.length > 1) {
38
44
  throw new Error("Multiple merchant-ids are not supported.");
@@ -97,6 +103,14 @@ export const getHostedButtonDetails: HostedButtonDetailsParams = async ({
97
103
  };
98
104
  };
99
105
 
106
+ export function getElementFromSelector(
107
+ selector: string | HTMLElement
108
+ ): HTMLElement | null {
109
+ return typeof selector === "string"
110
+ ? document.querySelector(selector)
111
+ : selector;
112
+ }
113
+
100
114
  /**
101
115
  * Attaches form fields (html) to the given selector, and
102
116
  * initializes window.__pp_form_fields (htmlScript).
@@ -107,8 +121,7 @@ export const renderForm: RenderForm = ({
107
121
  htmlScript,
108
122
  selector,
109
123
  }) => {
110
- const elm =
111
- typeof selector === "string" ? document.querySelector(selector) : selector;
124
+ const elm = getElementFromSelector(selector);
112
125
  if (elm) {
113
126
  elm.innerHTML = html + htmlScript;
114
127
  const newScriptEl = document.createElement("script");
@@ -225,3 +238,81 @@ export const buildHostedButtonOnApprove = ({
225
238
  });
226
239
  };
227
240
  };
241
+
242
+ export function getFlexDirection({
243
+ layout,
244
+ }: GetFlexDirectionArgs): GetFlexDirection {
245
+ return { flexDirection: layout === "horizontal" ? "row" : "column" };
246
+ }
247
+
248
+ export function getButtonColor(
249
+ color: Color,
250
+ fundingSource: FundingSources
251
+ ): Color {
252
+ const colorMap = {
253
+ gold: {
254
+ paypal: "gold",
255
+ venmo: "blue",
256
+ paylater: "gold",
257
+ },
258
+ blue: {
259
+ paypal: "blue",
260
+ venmo: "silver",
261
+ paylater: "blue",
262
+ },
263
+ black: {
264
+ paypal: "black",
265
+ venmo: "black",
266
+ paylater: "black",
267
+ },
268
+ white: {
269
+ paypal: "white",
270
+ venmo: "white",
271
+ paylater: "white",
272
+ },
273
+ silver: {
274
+ paypal: "silver",
275
+ venmo: "blue",
276
+ paylater: "silver",
277
+ },
278
+ };
279
+
280
+ return colorMap[color][fundingSource];
281
+ }
282
+
283
+ export function shouldRenderSDKButtons(
284
+ fundingSources: $ReadOnlyArray<FundingSources>
285
+ ): boolean {
286
+ return Boolean(fundingSources.length);
287
+ }
288
+
289
+ export function appendButtonContainer({
290
+ flexDirection,
291
+ selector,
292
+ }: BuildButtonContainerArgs) {
293
+ const elm = getElementFromSelector(selector);
294
+
295
+ if (!elm) {
296
+ throw new Error("PayPal button container selector was not found");
297
+ }
298
+
299
+ const buttonContainer = document.createElement("div");
300
+
301
+ buttonContainer.setAttribute(
302
+ "style",
303
+ `display: flex; flex-wrap: nowrap; gap: 16px; max-width: 750px; flex-direction: ${flexDirection}`
304
+ );
305
+
306
+ const primaryButton = document.createElement("div");
307
+ primaryButton.setAttribute("id", `ncp-primary-button`);
308
+ primaryButton.setAttribute("style", "flex-grow: 1");
309
+
310
+ const secondaryButton = document.createElement("div");
311
+ secondaryButton.setAttribute("id", `ncp-secondary-button`);
312
+ secondaryButton.setAttribute("style", "flex-grow: 1");
313
+
314
+ buttonContainer.appendChild(primaryButton);
315
+ buttonContainer.appendChild(secondaryButton);
316
+
317
+ elm?.appendChild(buttonContainer);
318
+ }
@@ -8,6 +8,11 @@ import {
8
8
  buildHostedButtonOnApprove,
9
9
  createAccessToken,
10
10
  getHostedButtonDetails,
11
+ getFlexDirection,
12
+ getButtonColor,
13
+ shouldRenderSDKButtons,
14
+ appendButtonContainer,
15
+ getElementFromSelector,
11
16
  } from "./utils";
12
17
 
13
18
  vi.mock("@krakenjs/belter/src", async () => {
@@ -77,6 +82,7 @@ test("getHostedButtonDetails", async () => {
77
82
  );
78
83
  await getHostedButtonDetails({
79
84
  hostedButtonId,
85
+ fundingSources: [],
80
86
  }).then(({ style }) => {
81
87
  expect(style).toEqual({
82
88
  layout: "vertical",
@@ -309,4 +315,99 @@ describe("buildHostedButtonOnApprove", () => {
309
315
  });
310
316
  });
311
317
 
318
+ test("getFlexDirection", () => {
319
+ expect(getFlexDirection({ layout: "horizontal" })).toStrictEqual({
320
+ flexDirection: "row",
321
+ });
322
+ expect(getFlexDirection({ layout: "vertical" })).toStrictEqual({
323
+ flexDirection: "column",
324
+ });
325
+ });
326
+
327
+ test("getButtonColor", () => {
328
+ const colors = ["gold", "blue", "silver", "white", "black"];
329
+ const fundingSources = ["paypal", "venmo", "paylater"];
330
+ const colorMap = {
331
+ gold: {
332
+ paypal: "gold",
333
+ venmo: "blue",
334
+ paylater: "gold",
335
+ },
336
+ blue: {
337
+ paypal: "blue",
338
+ venmo: "silver",
339
+ paylater: "blue",
340
+ },
341
+ black: {
342
+ paypal: "black",
343
+ venmo: "black",
344
+ paylater: "black",
345
+ },
346
+ white: {
347
+ paypal: "white",
348
+ venmo: "white",
349
+ paylater: "white",
350
+ },
351
+ silver: {
352
+ paypal: "silver",
353
+ venmo: "blue",
354
+ paylater: "silver",
355
+ },
356
+ };
357
+
358
+ colors.forEach((color) => {
359
+ fundingSources.forEach((fundingSource) => {
360
+ expect(getButtonColor(color, fundingSource)).toBe(
361
+ colorMap[color][fundingSource]
362
+ );
363
+ });
364
+ });
365
+ });
366
+
367
+ test("shouldRenderSDKButtons", () => {
368
+ expect(shouldRenderSDKButtons([])).toBe(false);
369
+ expect(shouldRenderSDKButtons(["paypal"])).toBe(true);
370
+ expect(shouldRenderSDKButtons(["paypal", "venmo"])).toBe(true);
371
+ });
372
+
373
+ test("buildButtonContainer", () => {
374
+ const containerId = "#container-id";
375
+ const selector = document.createElement("div");
376
+
377
+ selector.setAttribute("id", containerId.slice(1));
378
+
379
+ vi.spyOn(document, "querySelector").mockReturnValueOnce(selector);
380
+
381
+ expect(() =>
382
+ appendButtonContainer({ flexDirection: "row", selector: containerId })
383
+ ).not.toThrow();
384
+
385
+ expect(() =>
386
+ appendButtonContainer({ flexDirection: "row", selector })
387
+ ).not.toThrow();
388
+
389
+ expect(() =>
390
+ appendButtonContainer({
391
+ flexDirection: "row",
392
+ selector: `${containerId}-not-found`,
393
+ })
394
+ ).toThrow("PayPal button container selector was not found");
395
+ });
396
+
397
+ test("getElementFromSelector", () => {
398
+ const containerId = "#container-id";
399
+ const selector = document.createElement("div");
400
+
401
+ selector.setAttribute("id", containerId.slice(1));
402
+
403
+ const mockQuerySelector = vi
404
+ .spyOn(document, "querySelector")
405
+ .mockReturnValueOnce(selector);
406
+
407
+ expect(getElementFromSelector(containerId)).toBe(selector);
408
+ expect(getElementFromSelector(selector)).toBe(selector);
409
+ expect(mockQuerySelector).toBeCalledTimes(1);
410
+ expect(mockQuerySelector).toHaveBeenCalledWith(containerId);
411
+ });
412
+
312
413
  /* eslint-enable no-restricted-globals, promise/no-native */