@paypal/checkout-components 5.0.298 → 5.0.299

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.298",
3
+ "version": "5.0.299",
4
4
  "description": "PayPal Checkout components, for integrating checkout products.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -29,7 +29,7 @@
29
29
  "release": "./scripts/publish.sh",
30
30
  "start": "npm run webpack -- --progress --watch",
31
31
  "test": "npm run format:check && npm run test:unit && npm run jest-ssr && npm run karma",
32
- "test:unit": "vitest run",
32
+ "test:unit": "vitest run --coverage",
33
33
  "percy-screenshot": "npx playwright install && babel-node ./test/percy/server/createButtonConfigs.js && percy exec -- playwright test --config=./test/percy/playwright.config.js --reporter=dot --pass-with-no-tests",
34
34
  "typecheck": "npm run flow-typed && npm run flow",
35
35
  "version": "./scripts/version.sh",
@@ -74,6 +74,7 @@
74
74
  "@percy/cli": "1.27.2",
75
75
  "@percy/playwright": "^1.0.4",
76
76
  "@playwright/test": "^1.38.1",
77
+ "@vitest/coverage-v8": "^1.3.1",
77
78
  "babel-core": "^7.0.0-bridge.0",
78
79
  "bundlemon": "^1.1.0",
79
80
  "conventional-changelog-cli": "^2.0.34",
@@ -99,7 +100,7 @@
99
100
  "puppeteer": "^1.20.0",
100
101
  "serve": "^14.0.0",
101
102
  "vite": "^3.2.4",
102
- "vitest": "^0.25.3"
103
+ "vitest": "^1.3.1"
103
104
  },
104
105
  "dependencies": {
105
106
  "@krakenjs/beaver-logger": "^5.7.0",
@@ -1,14 +1,10 @@
1
1
  /* @flow */
2
- /* eslint-disable eslint-comments/disable-enable-pair */
3
- /* eslint-disable no-restricted-globals, promise/no-native */
4
2
 
5
- import { memoize, type Memoized } from "@krakenjs/belter/src";
3
+ import { memoize } from "@krakenjs/belter/src";
6
4
 
7
5
  import { getConnectComponent } from "./component";
8
6
 
9
7
  type MerchantProps = {||};
10
- // This needs to be typed, this is coming from the fastlane team i believe
11
- type FastlaneExternalComponent = {||};
12
8
 
13
9
  type ConnectComponent = (merchantProps: MerchantProps) => ConnectComponent;
14
10
  // $FlowFixMe
@@ -18,7 +14,8 @@ export const Connect: (merchantProps: MerchantProps) => ConnectComponent =
18
14
  return await getConnectComponent(merchantProps);
19
15
  });
20
16
 
21
- export const Fastlane = (
22
- merchantProps: MerchantProps
23
- ): Memoized<() => Promise<FastlaneExternalComponent>> =>
24
- memoize(() => getConnectComponent(merchantProps));
17
+ export const Fastlane: (merchantProps: MerchantProps) => ConnectComponent =
18
+ memoize(async (merchantProps: MerchantProps): ConnectComponent => {
19
+ // $FlowFixMe
20
+ return await getConnectComponent(merchantProps);
21
+ });
@@ -17,38 +17,40 @@ import type {
17
17
 
18
18
  export const getHostedButtonsComponent = (): HostedButtonsComponent => {
19
19
  function HostedButtons({
20
+ enableDPoP = false,
20
21
  hostedButtonId,
21
22
  }: HostedButtonsComponentProps): HostedButtonsInstance {
22
23
  const Buttons = getButtonsComponent();
23
- const render = (selector) => {
24
+ const render = async (selector) => {
24
25
  const merchantId = getMerchantID();
26
+ const { html, htmlScript, style } = await getHostedButtonDetails({
27
+ hostedButtonId,
28
+ });
25
29
 
26
- getHostedButtonDetails({ hostedButtonId }).then(
27
- ({ html, htmlScript, style }) => {
28
- const { onInit, onClick } = renderForm({
29
- hostedButtonId,
30
- html,
31
- htmlScript,
32
- selector,
33
- });
30
+ const { onInit, onClick } = renderForm({
31
+ hostedButtonId,
32
+ html,
33
+ htmlScript,
34
+ selector,
35
+ });
34
36
 
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
- );
37
+ // $FlowFixMe
38
+ Buttons({
39
+ hostedButtonId,
40
+ style,
41
+ onInit,
42
+ onClick,
43
+ createOrder: buildHostedButtonCreateOrder({
44
+ enableDPoP,
45
+ hostedButtonId,
46
+ merchantId,
47
+ }),
48
+ onApprove: buildHostedButtonOnApprove({
49
+ enableDPoP,
50
+ hostedButtonId,
51
+ merchantId,
52
+ }),
53
+ }).render(selector);
52
54
  };
53
55
  return {
54
56
  render,
@@ -1,8 +1,8 @@
1
1
  /* @flow */
2
+ /* eslint-disable no-restricted-globals, promise/no-native */
2
3
 
3
4
  import { describe, test, expect, vi } from "vitest";
4
5
  import { request } from "@krakenjs/belter/src";
5
- import { ZalgoPromise } from "@krakenjs/zalgo-promise";
6
6
 
7
7
  import { getButtonsComponent } from "../zoid/buttons";
8
8
 
@@ -61,16 +61,17 @@ const getHostedButtonDetailsResponse = {
61
61
  };
62
62
 
63
63
  describe("HostedButtons", () => {
64
- test("paypal.Buttons calls getHostedButtonDetails and invokes v5 of the SDK", () => {
64
+ test("paypal.Buttons calls getHostedButtonDetails and invokes v5 of the SDK", async () => {
65
65
  const Buttons = vi.fn(() => ({ render: vi.fn() }));
66
66
  // $FlowIssue
67
67
  getButtonsComponent.mockImplementationOnce(() => Buttons);
68
68
  const HostedButtons = getHostedButtonsComponent();
69
69
  // $FlowIssue
70
70
  request.mockImplementationOnce(() =>
71
- ZalgoPromise.resolve(getHostedButtonDetailsResponse)
71
+ // eslint-disable-next-line compat/compat
72
+ Promise.resolve(getHostedButtonDetailsResponse)
72
73
  );
73
- HostedButtons({
74
+ await HostedButtons({
74
75
  hostedButtonId: "B1234567890",
75
76
  }).render("#example");
76
77
  expect(Buttons).toHaveBeenCalledWith(
@@ -81,3 +82,5 @@ describe("HostedButtons", () => {
81
82
  expect.assertions(1);
82
83
  });
83
84
  });
85
+
86
+ /* eslint-enable no-restricted-globals, promise/no-native */
@@ -1,22 +1,22 @@
1
1
  /* @flow */
2
-
3
- import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
2
+ /* eslint-disable no-restricted-globals, promise/no-native */
4
3
 
5
4
  export type HostedButtonsComponentProps = {|
6
5
  hostedButtonId: string,
7
6
  |};
8
7
 
9
8
  export type GetCallbackProps = {|
9
+ enableDPoP?: boolean,
10
10
  hostedButtonId: string,
11
11
  merchantId?: string,
12
12
  |};
13
13
 
14
14
  export type HostedButtonsInstance = {|
15
- render: (string | HTMLElement) => void,
15
+ render: (string | HTMLElement) => Promise<void>,
16
16
  |};
17
17
 
18
18
  export type HostedButtonDetailsParams =
19
- (HostedButtonsComponentProps) => ZalgoPromise<{|
19
+ (HostedButtonsComponentProps) => Promise<{|
20
20
  html: string,
21
21
  htmlScript: string,
22
22
  style: {|
@@ -34,14 +34,17 @@ export type ButtonVariables = $ReadOnlyArray<{|
34
34
 
35
35
  export type CreateOrder = (data: {|
36
36
  paymentSource: string,
37
- |}) => ZalgoPromise<string | void>;
37
+ |}) => Promise<string | void>;
38
38
 
39
39
  export type OnApprove = (data: {|
40
40
  orderID: string,
41
41
  paymentSource: string,
42
- |}) => ZalgoPromise<mixed>;
42
+ |}) => Promise<mixed>;
43
43
 
44
- export type CreateAccessToken = (clientID: string) => ZalgoPromise<string>;
44
+ export type CreateAccessToken = ({|
45
+ clientId: string,
46
+ enableDPoP?: boolean,
47
+ |}) => Promise<{| accessToken: string, nonce: string |}>;
45
48
 
46
49
  export type HostedButtonsComponent =
47
50
  (HostedButtonsComponentProps) => HostedButtonsInstance;
@@ -55,3 +58,5 @@ export type RenderForm = ({|
55
58
  onInit: (data: mixed, actions: mixed) => void,
56
59
  onClick: (data: mixed, actions: mixed) => void,
57
60
  |};
61
+
62
+ /* eslint-enable no-restricted-globals, promise/no-native */
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { request, memoize } from "@krakenjs/belter/src";
4
4
  import {
5
+ buildDPoPHeaders,
5
6
  getSDKHost,
6
7
  getClientID,
7
8
  getMerchantID as getSDKMerchantID,
@@ -39,41 +40,60 @@ export const getMerchantID = (): string | void => {
39
40
  };
40
41
 
41
42
  export const createAccessToken: CreateAccessToken = memoize<CreateAccessToken>(
42
- (clientId) => {
43
- return request({
44
- url: `${apiUrl}/v1/oauth2/token`,
45
- method: "POST",
43
+ async ({ clientId, enableDPoP }) => {
44
+ const url = `${apiUrl}/v1/oauth2/token`;
45
+ const method = "POST";
46
+ const DPoPHeaders = enableDPoP
47
+ ? await buildDPoPHeaders({
48
+ uri: url,
49
+ method,
50
+ })
51
+ : {};
52
+ const response = await request({
53
+ url,
54
+ method,
46
55
  body: "grant_type=client_credentials",
56
+ // $FlowIssue optional properties are not compatible with [key: string]: string
47
57
  headers: {
48
58
  Authorization: `Basic ${btoa(clientId)}`,
49
59
  "Content-Type": "application/json",
60
+ // $FlowIssue exponential-spread
61
+ ...DPoPHeaders,
50
62
  },
51
- }).then((response) => response.body.access_token);
63
+ });
64
+ // $FlowIssue request returns ZalgoPromise
65
+ const { access_token: accessToken, nonce } = response.body;
66
+ return {
67
+ accessToken,
68
+ nonce,
69
+ };
52
70
  }
53
71
  );
54
72
 
55
73
  const getButtonVariable = (variables: ButtonVariables, key: string): string =>
56
74
  variables?.find((variable) => variable.name === key)?.value ?? "";
57
75
 
58
- export const getHostedButtonDetails: HostedButtonDetailsParams = ({
76
+ export const getHostedButtonDetails: HostedButtonDetailsParams = async ({
59
77
  hostedButtonId,
60
78
  }) => {
61
- return request({
79
+ const response = await request({
62
80
  url: `${baseUrl}/ncp/api/form-fields/${hostedButtonId}`,
63
81
  headers: getHeaders(),
64
- }).then(({ body }) => {
65
- const variables = body.button_details.link_variables;
66
- return {
67
- style: {
68
- layout: getButtonVariable(variables, "layout"),
69
- shape: getButtonVariable(variables, "shape"),
70
- color: getButtonVariable(variables, "color"),
71
- label: getButtonVariable(variables, "button_text"),
72
- },
73
- html: body.html,
74
- htmlScript: body.html_script,
75
- };
76
82
  });
83
+
84
+ // $FlowIssue request returns ZalgoPromise
85
+ const { body } = response;
86
+ const variables = body.button_details.link_variables;
87
+ return {
88
+ style: {
89
+ layout: getButtonVariable(variables, "layout"),
90
+ shape: getButtonVariable(variables, "shape"),
91
+ color: getButtonVariable(variables, "color"),
92
+ label: getButtonVariable(variables, "button_text"),
93
+ },
94
+ html: body.html,
95
+ htmlScript: body.html_script,
96
+ };
77
97
  };
78
98
 
79
99
  /**
@@ -106,47 +126,88 @@ export const renderForm: RenderForm = ({
106
126
  };
107
127
 
108
128
  export const buildHostedButtonCreateOrder = ({
129
+ enableDPoP,
109
130
  hostedButtonId,
110
131
  merchantId,
111
132
  }: GetCallbackProps): CreateOrder => {
112
- return (data) => {
133
+ return async (data) => {
113
134
  const userInputs =
114
135
  window[`__pp_form_fields_${hostedButtonId}`]?.getUserInputs?.() || {};
115
136
  const onError = window[`__pp_form_fields_${hostedButtonId}`]?.onError;
116
- return createAccessToken(getClientID()).then((accessToken) => {
117
- return request({
118
- url: `${apiUrl}/v1/checkout/links/${hostedButtonId}/create-context`,
119
- headers: getHeaders(accessToken),
120
- method: "POST",
137
+ const { accessToken, nonce } = await createAccessToken({
138
+ clientId: getClientID(),
139
+ enableDPoP,
140
+ });
141
+ try {
142
+ const url = `${apiUrl}/v1/checkout/links/${hostedButtonId}/create-context`;
143
+ const method = "POST";
144
+ const DPoPHeaders = enableDPoP
145
+ ? await buildDPoPHeaders({
146
+ uri: url,
147
+ method,
148
+ accessToken,
149
+ nonce,
150
+ })
151
+ : {};
152
+ const response = await request({
153
+ url,
154
+ // $FlowIssue optional properties are not compatible with [key: string]: string
155
+ headers: {
156
+ ...getHeaders(accessToken),
157
+ // $FlowIssue exponential-spread
158
+ ...DPoPHeaders,
159
+ },
160
+ method,
121
161
  body: JSON.stringify({
122
162
  entry_point: entryPoint,
123
163
  funding_source: data.paymentSource.toUpperCase(),
124
164
  merchant_id: merchantId,
125
165
  ...userInputs,
126
166
  }),
127
- })
128
- .then(({ body }) => body.context_id || onError(body.name))
129
- .catch(() => onError("REQUEST_FAILED"));
130
- });
167
+ });
168
+ // $FlowIssue request returns ZalgoPromise
169
+ const { body } = response;
170
+ return body.context_id || onError(body.name);
171
+ } catch (e) {
172
+ return onError("REQUEST_FAILED");
173
+ }
131
174
  };
132
175
  };
133
176
 
134
177
  export const buildHostedButtonOnApprove = ({
178
+ enableDPoP,
135
179
  hostedButtonId,
136
180
  merchantId,
137
181
  }: GetCallbackProps): OnApprove => {
138
- return (data) => {
139
- return createAccessToken(getClientID()).then((accessToken) => {
140
- return request({
141
- url: `${apiUrl}/v1/checkout/links/${hostedButtonId}/pay`,
142
- headers: getHeaders(accessToken),
143
- method: "POST",
144
- body: JSON.stringify({
145
- entry_point: entryPoint,
146
- merchant_id: merchantId,
147
- context_id: data.orderID,
148
- }),
149
- });
182
+ return async (data) => {
183
+ const { accessToken, nonce } = await createAccessToken({
184
+ clientId: getClientID(),
185
+ enableDPoP,
186
+ });
187
+ const url = `${apiUrl}/v1/checkout/links/${hostedButtonId}/pay`;
188
+ const method = "POST";
189
+ const DPoPHeaders = enableDPoP
190
+ ? await buildDPoPHeaders({
191
+ uri: url,
192
+ method,
193
+ accessToken,
194
+ nonce,
195
+ })
196
+ : {};
197
+ return request({
198
+ url,
199
+ // $FlowIssue optional properties are not compatible with [key: string]: string
200
+ headers: {
201
+ ...getHeaders(accessToken),
202
+ // $FlowIssue exponential-spread
203
+ ...DPoPHeaders,
204
+ },
205
+ method,
206
+ body: JSON.stringify({
207
+ entry_point: entryPoint,
208
+ merchant_id: merchantId,
209
+ context_id: data.orderID,
210
+ }),
150
211
  });
151
212
  };
152
213
  };
@@ -1,12 +1,12 @@
1
1
  /* @flow */
2
-
2
+ /* eslint-disable no-restricted-globals, promise/no-native */
3
3
  import { test, expect, vi } from "vitest";
4
4
  import { request } from "@krakenjs/belter/src";
5
- import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
6
5
 
7
6
  import {
8
7
  buildHostedButtonCreateOrder,
9
8
  buildHostedButtonOnApprove,
9
+ createAccessToken,
10
10
  getHostedButtonDetails,
11
11
  } from "./utils";
12
12
 
@@ -26,9 +26,11 @@ vi.mock("@paypal/sdk-client/src", async () => {
26
26
  };
27
27
  });
28
28
 
29
+ const accessToken = "AT1234567890";
29
30
  const hostedButtonId = "B1234567890";
30
31
  const merchantId = "M1234567890";
31
32
  const orderID = "EC-1234567890";
33
+ const clientId = "C1234567890";
32
34
 
33
35
  const getHostedButtonDetailsResponse = {
34
36
  body: {
@@ -59,10 +61,19 @@ const getHostedButtonDetailsResponse = {
59
61
  },
60
62
  };
61
63
 
64
+ const mockCreateAccessTokenRequest = () =>
65
+ // eslint-disable-next-line compat/compat
66
+ Promise.resolve({
67
+ body: {
68
+ access_token: accessToken,
69
+ },
70
+ });
71
+
62
72
  test("getHostedButtonDetails", async () => {
63
73
  // $FlowIssue
64
74
  request.mockImplementationOnce(() =>
65
- ZalgoPromise.resolve(getHostedButtonDetailsResponse)
75
+ // eslint-disable-next-line compat/compat
76
+ Promise.resolve(getHostedButtonDetailsResponse)
66
77
  );
67
78
  await getHostedButtonDetails({
68
79
  hostedButtonId,
@@ -77,25 +88,100 @@ test("getHostedButtonDetails", async () => {
77
88
  expect.assertions(1);
78
89
  });
79
90
 
91
+ describe("createAccessToken", () => {
92
+ test("basic functionality", async () => {
93
+ request
94
+ // $FlowIssue
95
+ .mockImplementationOnce(mockCreateAccessTokenRequest);
96
+ await createAccessToken({ clientId });
97
+ expect(request).toHaveBeenCalledWith(
98
+ expect.objectContaining({
99
+ headers: expect.objectContaining({
100
+ Authorization: expect.stringContaining("Basic "),
101
+ }),
102
+ })
103
+ );
104
+ expect.assertions(1);
105
+ });
106
+ test("with DPoP enabled", async () => {
107
+ request
108
+ // $FlowIssue
109
+ .mockImplementationOnce(mockCreateAccessTokenRequest);
110
+ await createAccessToken({ clientId, enableDPoP: true });
111
+ expect(request).toHaveBeenCalledWith(
112
+ expect.objectContaining({
113
+ headers: expect.objectContaining({
114
+ Authorization: expect.stringContaining("Basic "),
115
+ DPoP: expect.any(String),
116
+ }),
117
+ })
118
+ );
119
+ expect.assertions(1);
120
+ });
121
+ });
122
+
80
123
  test("buildHostedButtonCreateOrder", async () => {
81
124
  const createOrder = buildHostedButtonCreateOrder({
82
125
  hostedButtonId,
83
126
  merchantId,
84
127
  });
85
128
 
86
- // $FlowIssue
87
- request.mockImplementation(() =>
88
- ZalgoPromise.resolve({
89
- body: {
90
- link_id: hostedButtonId,
91
- merchant_id: merchantId,
92
- context_id: orderID,
93
- status: "CREATED",
94
- },
129
+ request
130
+ // $FlowIssue
131
+ .mockImplementationOnce(mockCreateAccessTokenRequest)
132
+ .mockImplementation(() =>
133
+ // eslint-disable-next-line compat/compat
134
+ Promise.resolve({
135
+ body: {
136
+ link_id: hostedButtonId,
137
+ merchant_id: merchantId,
138
+ context_id: orderID,
139
+ status: "CREATED",
140
+ },
141
+ })
142
+ );
143
+ const createdOrderID = await createOrder({ paymentSource: "paypal" });
144
+ expect(request).toHaveBeenCalledWith(
145
+ expect.objectContaining({
146
+ headers: expect.objectContaining({
147
+ Authorization: `Bearer ${accessToken}`,
148
+ }),
95
149
  })
96
150
  );
97
- const createdOrderID = await createOrder({ paymentSource: "paypal" });
98
151
  expect(createdOrderID).toBe(orderID);
152
+ expect.assertions(2);
153
+ });
154
+
155
+ test("buildHostedButtonCreateOrder with DPoP enabled", async () => {
156
+ const createOrder = buildHostedButtonCreateOrder({
157
+ enableDPoP: true,
158
+ hostedButtonId,
159
+ merchantId,
160
+ });
161
+
162
+ request
163
+ // $FlowIssue
164
+ .mockImplementationOnce(mockCreateAccessTokenRequest)
165
+ .mockImplementation(() =>
166
+ // eslint-disable-next-line compat/compat
167
+ Promise.resolve({
168
+ body: {
169
+ link_id: hostedButtonId,
170
+ merchant_id: merchantId,
171
+ context_id: orderID,
172
+ status: "CREATED",
173
+ },
174
+ })
175
+ );
176
+ await createOrder({ paymentSource: "paypal" });
177
+ expect(request).toHaveBeenCalledWith(
178
+ expect.objectContaining({
179
+ headers: expect.objectContaining({
180
+ Authorization: `DPoP ${accessToken}`,
181
+ DPoP: expect.any(String),
182
+ }),
183
+ })
184
+ );
99
185
  expect.assertions(1);
100
186
  });
101
187
 
@@ -107,7 +193,8 @@ test("buildHostedButtonCreateOrder error handling", async () => {
107
193
 
108
194
  // $FlowIssue
109
195
  request.mockImplementation(() =>
110
- ZalgoPromise.resolve({
196
+ // eslint-disable-next-line compat/compat
197
+ Promise.resolve({
111
198
  body: {
112
199
  name: "RESOURCE_NOT_FOUND",
113
200
  },
@@ -133,7 +220,8 @@ describe("buildHostedButtonOnApprove", () => {
133
220
 
134
221
  // $FlowIssue
135
222
  request.mockImplementation(() =>
136
- ZalgoPromise.resolve({
223
+ // eslint-disable-next-line compat/compat
224
+ Promise.resolve({
137
225
  body: {},
138
226
  })
139
227
  );
@@ -149,4 +237,33 @@ describe("buildHostedButtonOnApprove", () => {
149
237
  );
150
238
  expect.assertions(1);
151
239
  });
240
+
241
+ test("with DPoP enabled", async () => {
242
+ const onApprove = buildHostedButtonOnApprove({
243
+ enableDPoP: true,
244
+ hostedButtonId,
245
+ merchantId,
246
+ });
247
+ request
248
+ // $FlowIssue
249
+ .mockImplementationOnce(mockCreateAccessTokenRequest)
250
+ .mockImplementation(() =>
251
+ // eslint-disable-next-line compat/compat
252
+ Promise.resolve({
253
+ body: {},
254
+ })
255
+ );
256
+ await onApprove({ orderID, paymentSource: "paypal" });
257
+ expect(request).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ headers: expect.objectContaining({
260
+ Authorization: `DPoP ${accessToken}`,
261
+ DPoP: expect.any(String),
262
+ }),
263
+ })
264
+ );
265
+ expect.assertions(1);
266
+ });
152
267
  });
268
+
269
+ /* eslint-enable no-restricted-globals, promise/no-native */