@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/dist/button.js +1 -1
- package/dist/test/button.js +1 -1
- package/package.json +1 -1
- package/src/hosted-buttons/index.js +59 -15
- package/src/hosted-buttons/index.test.js +70 -2
- package/src/hosted-buttons/types.js +19 -0
- package/src/hosted-buttons/utils.js +95 -4
- package/src/hosted-buttons/utils.test.js +101 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
45
|
+
const createOrder = buildHostedButtonCreateOrder({
|
|
46
|
+
enableDPoP,
|
|
39
47
|
hostedButtonId,
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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
|
|
34
|
-
//
|
|
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 */
|