@paypal/checkout-components 5.0.294 → 5.0.295-alpha.11

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/__sdk__.js CHANGED
@@ -86,4 +86,8 @@ module.exports = {
86
86
  entry: "./src/interface/card-fields",
87
87
  globals,
88
88
  },
89
+ "hosted-buttons": {
90
+ entry: "./src/interface/hosted-buttons",
91
+ globals,
92
+ },
89
93
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paypal/checkout-components",
3
- "version": "5.0.294",
3
+ "version": "5.0.295-alpha.11",
4
4
  "description": "PayPal Checkout components, for integrating checkout products.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,58 @@
1
+ /* @flow */
2
+
3
+ import { getButtonsComponent } from "../zoid/buttons";
4
+
5
+ import {
6
+ buildHostedButtonCreateOrder,
7
+ buildHostedButtonOnApprove,
8
+ getHostedButtonDetails,
9
+ renderForm,
10
+ getMerchantID,
11
+ } from "./utils";
12
+ import type {
13
+ HostedButtonsComponent,
14
+ HostedButtonsComponentProps,
15
+ HostedButtonsInstance,
16
+ } from "./types";
17
+
18
+ export const getHostedButtonsComponent = (): HostedButtonsComponent => {
19
+ function HostedButtons({
20
+ hostedButtonId,
21
+ }: HostedButtonsComponentProps): HostedButtonsInstance {
22
+ const Buttons = getButtonsComponent();
23
+ const render = (selector) => {
24
+ const merchantId = getMerchantID();
25
+
26
+ getHostedButtonDetails({ hostedButtonId }).then(
27
+ ({ html, htmlScript, style }) => {
28
+ const { onInit, onClick } = renderForm({
29
+ hostedButtonId,
30
+ html,
31
+ htmlScript,
32
+ selector,
33
+ });
34
+
35
+ // $FlowFixMe
36
+ Buttons({
37
+ hostedButtonId,
38
+ style,
39
+ onInit,
40
+ onClick,
41
+ createOrder: buildHostedButtonCreateOrder({
42
+ hostedButtonId,
43
+ merchantId,
44
+ }),
45
+ onApprove: buildHostedButtonOnApprove({
46
+ hostedButtonId,
47
+ merchantId,
48
+ }),
49
+ }).render(selector);
50
+ }
51
+ );
52
+ };
53
+ return {
54
+ render,
55
+ };
56
+ }
57
+ return HostedButtons;
58
+ };
@@ -0,0 +1,83 @@
1
+ /* @flow */
2
+
3
+ import { describe, test, expect, vi } from "vitest";
4
+ import { request } from "@krakenjs/belter/src";
5
+ import { ZalgoPromise } from "@krakenjs/zalgo-promise";
6
+
7
+ import { getButtonsComponent } from "../zoid/buttons";
8
+
9
+ import { getHostedButtonsComponent } from ".";
10
+
11
+ vi.mock("@krakenjs/belter/src", async () => {
12
+ return {
13
+ ...(await vi.importActual("@krakenjs/belter/src")),
14
+ request: vi.fn(),
15
+ };
16
+ });
17
+
18
+ vi.mock("@paypal/sdk-client/src", async () => {
19
+ return {
20
+ ...(await vi.importActual("@paypal/sdk-client/src")),
21
+ getSDKHost: () => "example.com",
22
+ getClientID: () => "client_id_123",
23
+ getMerchantID: () => ["merchant_id_123"],
24
+ };
25
+ });
26
+
27
+ vi.mock("../zoid/buttons", async () => {
28
+ return {
29
+ ...(await vi.importActual("../zoid/buttons")),
30
+ getButtonsComponent: vi.fn(),
31
+ };
32
+ });
33
+
34
+ const getHostedButtonDetailsResponse = {
35
+ body: {
36
+ button_details: {
37
+ link_variables: [
38
+ {
39
+ name: "shape",
40
+ value: "rect",
41
+ },
42
+ {
43
+ name: "layout",
44
+ value: "vertical",
45
+ },
46
+ {
47
+ name: "color",
48
+ value: "gold",
49
+ },
50
+ {
51
+ name: "button_text",
52
+ value: "paypal",
53
+ },
54
+ {
55
+ name: "button_type",
56
+ value: "FIXED_PRICE",
57
+ },
58
+ ],
59
+ },
60
+ },
61
+ };
62
+
63
+ describe("HostedButtons", () => {
64
+ test("paypal.Buttons calls getHostedButtonDetails and invokes v5 of the SDK", () => {
65
+ const Buttons = vi.fn(() => ({ render: vi.fn() }));
66
+ // $FlowIssue
67
+ getButtonsComponent.mockImplementationOnce(() => Buttons);
68
+ const HostedButtons = getHostedButtonsComponent();
69
+ // $FlowIssue
70
+ request.mockImplementationOnce(() =>
71
+ ZalgoPromise.resolve(getHostedButtonDetailsResponse)
72
+ );
73
+ HostedButtons({
74
+ hostedButtonId: "B1234567890",
75
+ }).render("#example");
76
+ expect(Buttons).toHaveBeenCalledWith(
77
+ expect.objectContaining({
78
+ hostedButtonId: "B1234567890",
79
+ })
80
+ );
81
+ expect.assertions(1);
82
+ });
83
+ });
@@ -0,0 +1,57 @@
1
+ /* @flow */
2
+
3
+ import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
4
+
5
+ export type HostedButtonsComponentProps = {|
6
+ hostedButtonId: string,
7
+ |};
8
+
9
+ export type GetCallbackProps = {|
10
+ hostedButtonId: string,
11
+ merchantId?: string,
12
+ |};
13
+
14
+ export type HostedButtonsInstance = {|
15
+ render: (string | HTMLElement) => void,
16
+ |};
17
+
18
+ export type HostedButtonDetailsParams =
19
+ (HostedButtonsComponentProps) => ZalgoPromise<{|
20
+ html: string,
21
+ htmlScript: string,
22
+ style: {|
23
+ layout: string,
24
+ shape: string,
25
+ color: string,
26
+ label: string,
27
+ |},
28
+ |}>;
29
+
30
+ export type ButtonVariables = $ReadOnlyArray<{|
31
+ name: string,
32
+ value: string,
33
+ |}>;
34
+
35
+ export type CreateOrder = (data: {|
36
+ paymentSource: string,
37
+ |}) => ZalgoPromise<string>;
38
+
39
+ export type OnApprove = (data: {|
40
+ orderID: string,
41
+ paymentSource: string,
42
+ |}) => ZalgoPromise<mixed>;
43
+
44
+ export type CreateAccessToken = (clientID: string) => ZalgoPromise<string>;
45
+
46
+ export type HostedButtonsComponent =
47
+ (HostedButtonsComponentProps) => HostedButtonsInstance;
48
+
49
+ export type RenderForm = ({|
50
+ hostedButtonId: string,
51
+ html: string,
52
+ htmlScript: string,
53
+ selector: string | HTMLElement,
54
+ |}) => {|
55
+ onInit: (data: mixed, actions: mixed) => void,
56
+ onClick: (data: mixed, actions: mixed) => void,
57
+ |};
@@ -0,0 +1,170 @@
1
+ /* @flow */
2
+
3
+ import { request, memoize, popup, supportsPopups } from "@krakenjs/belter/src";
4
+ import {
5
+ getSDKHost,
6
+ getClientID,
7
+ getMerchantID as getSDKMerchantID,
8
+ } from "@paypal/sdk-client/src";
9
+ import { FUNDING } from "@paypal/sdk-constants/src";
10
+
11
+ import { DEFAULT_POPUP_SIZE } from "../zoid/checkout";
12
+
13
+ import type {
14
+ ButtonVariables,
15
+ CreateAccessToken,
16
+ CreateOrder,
17
+ GetCallbackProps,
18
+ HostedButtonDetailsParams,
19
+ OnApprove,
20
+ RenderForm,
21
+ } from "./types";
22
+
23
+ const entryPoint = "SDK";
24
+ const baseUrl = `https://${getSDKHost()}`;
25
+ const apiUrl = baseUrl.replace("www", "api");
26
+
27
+ const getHeaders = (accessToken?: string) => ({
28
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
29
+ "Content-Type": "application/json",
30
+ "PayPal-Entry-Point": entryPoint,
31
+ });
32
+
33
+ export const getMerchantID = (): string | void => {
34
+ // The SDK supports mutiple merchant IDs, but hosted buttons only
35
+ // have one merchant id as a query parameter to the SDK script.
36
+ // https://github.com/paypal/paypal-sdk-client/blob/c58e35f8f7adbab76523eb25b9c10543449d2d29/src/script.js#L144
37
+ const merchantIds = getSDKMerchantID();
38
+ if (merchantIds.length > 1) {
39
+ throw new Error("Multiple merchant-ids are not supported.");
40
+ }
41
+ return merchantIds[0];
42
+ };
43
+
44
+ export const createAccessToken: CreateAccessToken = memoize<CreateAccessToken>(
45
+ (clientId) => {
46
+ return request({
47
+ url: `${apiUrl}/v1/oauth2/token`,
48
+ method: "POST",
49
+ body: "grant_type=client_credentials",
50
+ headers: {
51
+ Authorization: `Basic ${btoa(clientId)}`,
52
+ "Content-Type": "application/json",
53
+ },
54
+ }).then((response) => response.body.access_token);
55
+ }
56
+ );
57
+
58
+ const getButtonVariable = (variables: ButtonVariables, key: string): string =>
59
+ variables?.find((variable) => variable.name === key)?.value ?? "";
60
+
61
+ export const getHostedButtonDetails: HostedButtonDetailsParams = ({
62
+ hostedButtonId,
63
+ }) => {
64
+ return request({
65
+ url: `${baseUrl}/ncp/api/form-fields/${hostedButtonId}`,
66
+ headers: getHeaders(),
67
+ }).then(({ body }) => {
68
+ const variables = body.button_details.link_variables;
69
+ return {
70
+ style: {
71
+ layout: getButtonVariable(variables, "layout"),
72
+ shape: getButtonVariable(variables, "shape"),
73
+ color: getButtonVariable(variables, "color"),
74
+ label: getButtonVariable(variables, "button_text"),
75
+ },
76
+ html: body.html,
77
+ htmlScript: body.html_script,
78
+ };
79
+ });
80
+ };
81
+
82
+ /**
83
+ * Attaches form fields (html) to the given selector, and
84
+ * initializes window.__pp_form_fields (htmlScript).
85
+ */
86
+ export const renderForm: RenderForm = ({
87
+ hostedButtonId,
88
+ html,
89
+ htmlScript,
90
+ selector,
91
+ }) => {
92
+ const elm =
93
+ typeof selector === "string" ? document.querySelector(selector) : selector;
94
+ if (elm) {
95
+ elm.innerHTML = html + htmlScript;
96
+ const newScriptEl = document.createElement("script");
97
+ const oldScriptEl = elm.querySelector("script");
98
+ newScriptEl.innerHTML = oldScriptEl?.innerHTML ?? "";
99
+ oldScriptEl?.parentNode?.replaceChild(newScriptEl, oldScriptEl);
100
+ }
101
+ return {
102
+ // disable the button, listen for input changes,
103
+ // and enable the button when the form is valid
104
+ // using actions.disable() and actions.enable()
105
+ onInit: window[`__pp_form_fields_${hostedButtonId}`]?.onInit,
106
+ // render form errors, if present
107
+ onClick: window[`__pp_form_fields_${hostedButtonId}`]?.onClick,
108
+ };
109
+ };
110
+
111
+ export const buildHostedButtonCreateOrder = ({
112
+ hostedButtonId,
113
+ merchantId,
114
+ }: GetCallbackProps): CreateOrder => {
115
+ return (data) => {
116
+ const userInputs =
117
+ window[`__pp_form_fields_${hostedButtonId}`]?.getUserInputs?.() || {};
118
+ return createAccessToken(getClientID()).then((accessToken) => {
119
+ return request({
120
+ url: `${apiUrl}/v1/checkout/links/${hostedButtonId}/create-context`,
121
+ headers: getHeaders(accessToken),
122
+ method: "POST",
123
+ body: JSON.stringify({
124
+ entry_point: entryPoint,
125
+ funding_source: data.paymentSource.toUpperCase(),
126
+ merchant_id: merchantId,
127
+ ...userInputs,
128
+ }),
129
+ }).then(({ body }) => {
130
+ return body.context_id;
131
+ });
132
+ });
133
+ };
134
+ };
135
+
136
+ export const buildHostedButtonOnApprove = ({
137
+ hostedButtonId,
138
+ merchantId,
139
+ }: GetCallbackProps): OnApprove => {
140
+ return (data) => {
141
+ return createAccessToken(getClientID()).then((accessToken) => {
142
+ return request({
143
+ url: `${apiUrl}/v1/checkout/links/${hostedButtonId}/pay`,
144
+ headers: getHeaders(accessToken),
145
+ method: "POST",
146
+ body: JSON.stringify({
147
+ entry_point: entryPoint,
148
+ merchant_id: merchantId,
149
+ context_id: data.orderID,
150
+ }),
151
+ }).then((response) => {
152
+ // The "Debit or Credit Card" button does not open a popup
153
+ // so we need to open a new popup for buyers who complete
154
+ // a checkout via "Debit or Credit Card".
155
+ if (data.paymentSource === FUNDING.CARD) {
156
+ const url = `${baseUrl}/ncp/payment/${hostedButtonId}/${data.orderID}`;
157
+ if (supportsPopups()) {
158
+ popup(url, {
159
+ width: DEFAULT_POPUP_SIZE.WIDTH,
160
+ height: DEFAULT_POPUP_SIZE.HEIGHT,
161
+ });
162
+ } else {
163
+ window.location = url;
164
+ }
165
+ }
166
+ return response;
167
+ });
168
+ });
169
+ };
170
+ };
@@ -0,0 +1,157 @@
1
+ /* @flow */
2
+
3
+ import { test, expect, vi } from "vitest";
4
+ import { request, popup, supportsPopups } from "@krakenjs/belter/src";
5
+ import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
6
+
7
+ import {
8
+ buildHostedButtonCreateOrder,
9
+ buildHostedButtonOnApprove,
10
+ getHostedButtonDetails,
11
+ } from "./utils";
12
+
13
+ vi.mock("@krakenjs/belter/src", async () => {
14
+ return {
15
+ ...(await vi.importActual("@krakenjs/belter/src")),
16
+ request: vi.fn(),
17
+ popup: vi.fn(),
18
+ supportsPopups: vi.fn(),
19
+ };
20
+ });
21
+
22
+ vi.mock("@paypal/sdk-client/src", async () => {
23
+ return {
24
+ ...(await vi.importActual("@paypal/sdk-client/src")),
25
+ getSDKHost: () => "example.com",
26
+ getClientID: () => "client_id_123",
27
+ getMerchantID: () => ["merchant_id_123"],
28
+ };
29
+ });
30
+
31
+ const hostedButtonId = "B1234567890";
32
+ const merchantId = "M1234567890";
33
+ const orderID = "EC-1234567890";
34
+
35
+ const getHostedButtonDetailsResponse = {
36
+ body: {
37
+ button_details: {
38
+ link_variables: [
39
+ {
40
+ name: "business",
41
+ value: merchantId,
42
+ },
43
+ {
44
+ name: "shape",
45
+ value: "rect",
46
+ },
47
+ {
48
+ name: "layout",
49
+ value: "vertical",
50
+ },
51
+ {
52
+ name: "color",
53
+ value: "gold",
54
+ },
55
+ {
56
+ name: "button_text",
57
+ value: "paypal",
58
+ },
59
+ ],
60
+ },
61
+ },
62
+ };
63
+
64
+ test("getHostedButtonDetails", async () => {
65
+ // $FlowIssue
66
+ request.mockImplementationOnce(() =>
67
+ ZalgoPromise.resolve(getHostedButtonDetailsResponse)
68
+ );
69
+ await getHostedButtonDetails({
70
+ hostedButtonId,
71
+ }).then(({ style }) => {
72
+ expect(style).toEqual({
73
+ layout: "vertical",
74
+ shape: "rect",
75
+ color: "gold",
76
+ label: "paypal",
77
+ });
78
+ });
79
+ expect.assertions(1);
80
+ });
81
+
82
+ test("buildHostedButtonCreateOrder", async () => {
83
+ const createOrder = buildHostedButtonCreateOrder({
84
+ hostedButtonId,
85
+ merchantId,
86
+ });
87
+
88
+ // $FlowIssue
89
+ request.mockImplementation(() =>
90
+ ZalgoPromise.resolve({
91
+ body: {
92
+ link_id: hostedButtonId,
93
+ merchant_id: merchantId,
94
+ context_id: orderID,
95
+ status: "CREATED",
96
+ },
97
+ })
98
+ );
99
+ const createdOrderID = await createOrder({ paymentSource: "paypal" });
100
+ expect(createdOrderID).toBe(orderID);
101
+ expect.assertions(1);
102
+ });
103
+
104
+ describe("buildHostedButtonOnApprove", () => {
105
+ test("makes a request to the Hosted Buttons API", async () => {
106
+ const onApprove = buildHostedButtonOnApprove({
107
+ hostedButtonId,
108
+ merchantId,
109
+ });
110
+
111
+ // $FlowIssue
112
+ request.mockImplementation(() =>
113
+ ZalgoPromise.resolve({
114
+ body: {},
115
+ })
116
+ );
117
+ await onApprove({ orderID, paymentSource: "paypal" });
118
+ expect(request).toHaveBeenCalledWith(
119
+ expect.objectContaining({
120
+ body: JSON.stringify({
121
+ entry_point: "SDK",
122
+ merchant_id: merchantId,
123
+ context_id: orderID,
124
+ }),
125
+ })
126
+ );
127
+ expect.assertions(1);
128
+ });
129
+
130
+ test("provides its own popup for inline guest", async () => {
131
+ const onApprove = buildHostedButtonOnApprove({
132
+ hostedButtonId,
133
+ merchantId,
134
+ });
135
+ // $FlowIssue
136
+ request.mockImplementation(() =>
137
+ ZalgoPromise.resolve({
138
+ body: {},
139
+ })
140
+ );
141
+
142
+ // $FlowIssue
143
+ supportsPopups.mockImplementation(() => true);
144
+ await onApprove({ orderID, paymentSource: "card" });
145
+ expect(popup).toHaveBeenCalled();
146
+
147
+ // but redirects if popups are not supported
148
+ // $FlowIssue
149
+ supportsPopups.mockImplementation(() => false);
150
+ await onApprove({ orderID, paymentSource: "card" });
151
+ expect(window.location).toMatch(
152
+ `/ncp/payment/${hostedButtonId}/${orderID}`
153
+ );
154
+
155
+ expect.assertions(2);
156
+ });
157
+ });
@@ -0,0 +1,29 @@
1
+ /* @flow */
2
+
3
+ import { getHostedButtonsComponent } from "../hosted-buttons";
4
+ import type { HostedButtonsComponent } from "../hosted-buttons/types";
5
+ import { getButtonsComponent } from "../zoid/buttons";
6
+ import {
7
+ getCardFormComponent,
8
+ type CardFormComponent,
9
+ } from "../zoid/card-form";
10
+ import { getCheckoutComponent, type CheckoutComponent } from "../zoid/checkout";
11
+ import type { LazyExport, LazyProtectedExport } from "../types";
12
+ import { protectedExport } from "../lib";
13
+
14
+ export const HostedButtons: LazyExport<HostedButtonsComponent> = {
15
+ __get__: () => getHostedButtonsComponent(),
16
+ };
17
+
18
+ export const Checkout: LazyProtectedExport<CheckoutComponent> = {
19
+ __get__: () => protectedExport(getCheckoutComponent()),
20
+ };
21
+
22
+ export const CardForm: LazyProtectedExport<CardFormComponent> = {
23
+ __get__: () => protectedExport(getCardFormComponent()),
24
+ };
25
+
26
+ export function setup() {
27
+ getButtonsComponent();
28
+ getCheckoutComponent();
29
+ }
@@ -521,6 +521,7 @@ export type ButtonProps = {|
521
521
  renderedButtons: $ReadOnlyArray<$Values<typeof FUNDING>>,
522
522
  createVaultSetupToken: CreateVaultSetupToken,
523
523
  displayOnly?: $ReadOnlyArray<$Values<typeof DISPLAY_ONLY_VALUES>>,
524
+ hostedButtonId?: string,
524
525
  |};
525
526
 
526
527
  // eslint-disable-next-line flowtype/require-exact-type
@@ -158,8 +158,13 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
158
158
  <PrerenderedButtons
159
159
  nonce={props.nonce}
160
160
  props={props}
161
- onRenderCheckout={({ win, fundingSource, card }) => {
162
- state.prerenderDetails = { win, fundingSource, card };
161
+ onRenderCheckout={({ win, fundingSource, card, hostedButtonId }) => {
162
+ state.prerenderDetails = {
163
+ win,
164
+ fundingSource,
165
+ card,
166
+ hostedButtonId,
167
+ };
163
168
  }}
164
169
  />
165
170
  ).render(dom({ doc }));
@@ -849,6 +854,12 @@ export const getButtonsComponent: () => ButtonsComponent = memoize(() => {
849
854
  value: getExperimentation,
850
855
  },
851
856
 
857
+ hostedButtonId: {
858
+ type: "string",
859
+ required: false,
860
+ queryParam: true,
861
+ },
862
+
852
863
  displayOnly: {
853
864
  type: "array",
854
865
  queryParam: true,
@@ -27,6 +27,7 @@ type PrerenderedButtonsProps = {|
27
27
  win?: CrossDomainWindowType,
28
28
  fundingSource: $Values<typeof FUNDING>,
29
29
  card: ?$Values<typeof CARD>,
30
+ hostedButtonId?: string,
30
31
  |}) => void,
31
32
  |};
32
33
 
@@ -77,7 +78,12 @@ export function PrerenderedButtons({
77
78
 
78
79
  writeElementToWindow(win, spinner);
79
80
 
80
- onRenderCheckout({ win, fundingSource, card });
81
+ onRenderCheckout({
82
+ win,
83
+ fundingSource,
84
+ card,
85
+ hostedButtonId: props.hostedButtonId,
86
+ });
81
87
  } else {
82
88
  onRenderCheckout({ fundingSource, card });
83
89
  }
@@ -317,6 +317,12 @@ export function getCheckoutComponent(): CheckoutComponent {
317
317
  queryParam: true,
318
318
  required: false,
319
319
  },
320
+
321
+ hostedButtonId: {
322
+ type: "string",
323
+ required: false,
324
+ queryParam: true,
325
+ },
320
326
  },
321
327
 
322
328
  dimensions: ({ props }) => {