@paypal/checkout-components 5.0.305-alpha.0 → 5.0.305

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/globals.js CHANGED
@@ -21,7 +21,7 @@ module.exports = {
21
21
  __PAYPAL_CHECKOUT__: {
22
22
  __REMEMBERED_FUNDING__: [],
23
23
  __URI__: {
24
- __CHECKOUT__: "/pay",
24
+ __CHECKOUT__: "/checkoutnow",
25
25
  __BUTTONS__: "/smart/buttons",
26
26
  __MENU__: "/smart/menu",
27
27
  __QRCODE__: "/smart/qrcode",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paypal/checkout-components",
3
- "version": "5.0.305-alpha.0",
3
+ "version": "5.0.305",
4
4
  "description": "PayPal Checkout components, for integrating checkout products.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/lib/api.js ADDED
@@ -0,0 +1,67 @@
1
+ /* @flow */
2
+
3
+ import { getPartnerAttributionID, getSessionID } from "@paypal/sdk-client/src";
4
+ import { inlineMemoize, request } from "@krakenjs/belter/src";
5
+ import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
6
+
7
+ import { HEADERS } from "../constants/api";
8
+
9
+ type RestAPIParams = {|
10
+ method?: string,
11
+ url: string,
12
+ data: Object,
13
+ accessToken: ?string,
14
+ |};
15
+
16
+ export function callRestAPI({
17
+ accessToken,
18
+ method,
19
+ url,
20
+ data,
21
+ }: RestAPIParams): ZalgoPromise<Object> {
22
+ const partnerAttributionID = getPartnerAttributionID() || "";
23
+
24
+ if (!accessToken) {
25
+ throw new Error(`No access token passed to API request ${url}`);
26
+ }
27
+
28
+ const requestHeaders = {
29
+ [HEADERS.AUTHORIZATION]: `Bearer ${accessToken}`,
30
+ [HEADERS.CONTENT_TYPE]: `application/json`,
31
+ [HEADERS.PARTNER_ATTRIBUTION_ID]: partnerAttributionID,
32
+ [HEADERS.CLIENT_METADATA_ID]: getSessionID(),
33
+ };
34
+
35
+ return request({
36
+ method,
37
+ url,
38
+ headers: requestHeaders,
39
+ json: data,
40
+ }).then(({ status, body, headers: responseHeaders }) => {
41
+ if (status >= 300) {
42
+ const error = new Error(
43
+ `${url} returned status ${status}\n\n${JSON.stringify(body)}`
44
+ );
45
+
46
+ // $FlowFixMe
47
+ error.response = { status, headers: responseHeaders, body };
48
+
49
+ throw error;
50
+ }
51
+
52
+ return body;
53
+ });
54
+ }
55
+
56
+ export function callMemoizedRestAPI({
57
+ accessToken,
58
+ method,
59
+ url,
60
+ data,
61
+ }: RestAPIParams): ZalgoPromise<Object> {
62
+ return inlineMemoize(
63
+ callMemoizedRestAPI,
64
+ () => callRestAPI({ accessToken, method, url, data }),
65
+ [accessToken, method, url, JSON.stringify(data)]
66
+ );
67
+ }
package/src/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /* @flow */
2
2
 
3
+ export * from "./api";
3
4
  export * from "./errors";
4
5
  export * from "./isRTLLanguage";
5
6
  export * from "./security";
@@ -0,0 +1,64 @@
1
+ /* @flow */
2
+ import {
3
+ getUserIDToken,
4
+ getPageType,
5
+ getClientToken,
6
+ getSDKToken,
7
+ getLogger,
8
+ getPayPalAPIDomain,
9
+ getCurrency,
10
+ getBuyerCountry,
11
+ getEnv,
12
+ getSessionState,
13
+ } from "@paypal/sdk-client/src";
14
+
15
+ import type { LazyExport } from "../types";
16
+ import { callMemoizedRestAPI } from "../lib";
17
+
18
+ import {
19
+ ShopperSession,
20
+ type ShopperInsightsInterface,
21
+ } from "./shopperSession";
22
+
23
+ const sessionState = {
24
+ get: (key) => {
25
+ let value;
26
+ getSessionState((state) => {
27
+ value = state[key];
28
+ return state;
29
+ });
30
+ return value;
31
+ },
32
+ set: (key, value) => {
33
+ getSessionState((state) => ({
34
+ ...state,
35
+ [key]: value,
36
+ }));
37
+ },
38
+ };
39
+
40
+ export const ShopperInsights: LazyExport<ShopperInsightsInterface> = {
41
+ __get__: () => {
42
+ const shopperSession = new ShopperSession({
43
+ logger: getLogger(),
44
+ // $FlowIssue ZalgoPromise vs Promise
45
+ request: callMemoizedRestAPI,
46
+ sdkConfig: {
47
+ sdkToken: getSDKToken(),
48
+ pageType: getPageType(),
49
+ userIDToken: getUserIDToken(),
50
+ clientToken: getClientToken(),
51
+ paypalApiDomain: getPayPalAPIDomain(),
52
+ environment: getEnv(),
53
+ buyerCountry: getBuyerCountry() || "US",
54
+ currency: getCurrency(),
55
+ },
56
+ sessionState,
57
+ });
58
+
59
+ return {
60
+ getRecommendedPaymentMethods: (payload) =>
61
+ shopperSession.getRecommendedPaymentMethods(payload),
62
+ };
63
+ },
64
+ };
@@ -0,0 +1,348 @@
1
+ /* @flow */
2
+ /* eslint-disable eslint-comments/disable-enable-pair */
3
+ /* eslint-disable no-restricted-globals, promise/no-native */
4
+
5
+ import { type LoggerType } from "@krakenjs/beaver-logger/src";
6
+ import { stringifyError } from "@krakenjs/belter/src";
7
+ import { FPTI_KEY } from "@paypal/sdk-constants/src";
8
+
9
+ import {
10
+ ELIGIBLE_PAYMENT_METHODS,
11
+ FPTI_TRANSITION,
12
+ SHOPPER_INSIGHTS_METRIC_NAME,
13
+ } from "../constants/api";
14
+ import { ValidationError } from "../lib";
15
+
16
+ export type MerchantPayloadData = {|
17
+ email?: string,
18
+ phone?: {|
19
+ countryCode?: string,
20
+ nationalNumber?: string,
21
+ |},
22
+ |};
23
+
24
+ type RecommendedPaymentMethods = {|
25
+ isPayPalRecommended: boolean,
26
+ isVenmoRecommended: boolean,
27
+ |};
28
+
29
+ type RecommendedPaymentMethodsRequestData = {|
30
+ customer: {|
31
+ country_code?: string,
32
+ email?: string,
33
+ phone?: {|
34
+ country_code: string,
35
+ national_number: string,
36
+ |},
37
+ |},
38
+ purchase_units: $ReadOnlyArray<{|
39
+ amount: {|
40
+ currency_code: string,
41
+ |},
42
+ |}>,
43
+ preferences: {|
44
+ include_account_details: boolean,
45
+ |},
46
+ |};
47
+
48
+ type RecommendedPaymentMethodsResponse = {|
49
+ eligible_methods: {
50
+ [paymentMethod: "paypal" | "venmo"]: {|
51
+ can_be_vaulted: boolean,
52
+ eligible_in_paypal_network?: boolean,
53
+ recommended?: boolean,
54
+ recommended_priority?: number,
55
+ |},
56
+ },
57
+ |};
58
+
59
+ type SdkConfig = {|
60
+ sdkToken: ?string,
61
+ pageType: ?string,
62
+ userIDToken: ?string,
63
+ clientToken: ?string,
64
+ paypalApiDomain: string,
65
+ environment: ?string,
66
+ buyerCountry: string,
67
+ currency: string,
68
+ |};
69
+
70
+ // eslint's flow integration is very out of date
71
+ // it doesn't recognize the generics here as used
72
+ // eslint-disable-next-line no-undef
73
+ type Request = <TRequestData, TResponse>({|
74
+ method?: string,
75
+ url: string,
76
+ // eslint-disable-next-line no-undef
77
+ data: TRequestData,
78
+ accessToken: ?string,
79
+ // eslint-disable-next-line no-undef
80
+ |}) => Promise<TResponse>;
81
+
82
+ type Storage = {|
83
+ // eslint's flow integration is very out of date
84
+ // it doesn't recognize the generics here as used
85
+ // eslint-disable-next-line no-undef
86
+ get: <TValue>(key: string) => ?TValue,
87
+ // eslint-disable-next-line flowtype/no-weak-types
88
+ set: (key: string, value: any) => void,
89
+ |};
90
+
91
+ const parseEmail = (merchantPayload): ?{| email: string |} => {
92
+ if (!merchantPayload.email) {
93
+ return;
94
+ }
95
+
96
+ const email = merchantPayload.email;
97
+ const isValidEmail =
98
+ typeof email === "string" && email.length < 320 && /^.+@.+$/.test(email);
99
+
100
+ if (!isValidEmail) {
101
+ throw new ValidationError(
102
+ `Expected shopper information to include a valid email format`
103
+ );
104
+ }
105
+
106
+ return {
107
+ email,
108
+ };
109
+ };
110
+
111
+ const parsePhone = (
112
+ merchantPayload
113
+ ): ?{| phone: {| country_code: string, national_number: string |} |} => {
114
+ if (!merchantPayload.phone) {
115
+ return;
116
+ }
117
+
118
+ if (
119
+ !merchantPayload.phone.nationalNumber ||
120
+ !merchantPayload.phone.countryCode
121
+ ) {
122
+ throw new ValidationError(
123
+ `Expected phone number for shopper insights to include nationalNumber and countryCode`
124
+ );
125
+ }
126
+
127
+ const nationalNumber = merchantPayload.phone.nationalNumber;
128
+ const countryCode = merchantPayload.phone.countryCode;
129
+ const isValidPhone =
130
+ typeof nationalNumber === "string" && /\d{5,}/.test(nationalNumber);
131
+
132
+ if (!isValidPhone) {
133
+ throw new ValidationError(
134
+ `Expected shopper information to be a valid phone number format`
135
+ );
136
+ }
137
+
138
+ return {
139
+ phone: {
140
+ country_code: countryCode,
141
+ national_number: nationalNumber,
142
+ },
143
+ };
144
+ };
145
+
146
+ export const parseMerchantPayload = ({
147
+ merchantPayload,
148
+ sdkConfig,
149
+ }: {|
150
+ merchantPayload: MerchantPayloadData,
151
+ sdkConfig: SdkConfig,
152
+ |}): RecommendedPaymentMethodsRequestData => {
153
+ const email = parseEmail(merchantPayload);
154
+ const phone = parsePhone(merchantPayload);
155
+
156
+ return {
157
+ customer: {
158
+ ...(sdkConfig.environment !== "production" && {
159
+ country_code: sdkConfig.buyerCountry,
160
+ }),
161
+ // $FlowIssue too many cases?
162
+ ...email,
163
+ ...phone,
164
+ },
165
+ purchase_units: [
166
+ {
167
+ amount: {
168
+ currency_code: sdkConfig.currency,
169
+ },
170
+ },
171
+ ],
172
+ // getRecommendedPaymentMethods maps to include_account_details in the API
173
+ preferences: {
174
+ include_account_details: true,
175
+ },
176
+ };
177
+ };
178
+
179
+ const parseSdkConfig = ({
180
+ sdkConfig,
181
+ logger,
182
+ }: {|
183
+ sdkConfig: SdkConfig,
184
+ logger: LoggerType,
185
+ |}): SdkConfig => {
186
+ if (!sdkConfig.sdkToken) {
187
+ logger.metricCounter({
188
+ namespace: SHOPPER_INSIGHTS_METRIC_NAME,
189
+ event: "error",
190
+ dimensions: {
191
+ errorType: "merchant_configuration_validation_error",
192
+ validationDetails: "sdk_token_not_present",
193
+ },
194
+ });
195
+
196
+ throw new ValidationError(
197
+ `script data attribute sdk-client-token is required but was not passed`
198
+ );
199
+ }
200
+
201
+ if (!sdkConfig.pageType) {
202
+ logger.metricCounter({
203
+ namespace: SHOPPER_INSIGHTS_METRIC_NAME,
204
+ event: "error",
205
+ dimensions: {
206
+ errorType: "merchant_configuration_validation_error",
207
+ validationDetails: "page_type_not_present",
208
+ },
209
+ });
210
+
211
+ throw new ValidationError(
212
+ `script data attribute page-type is required but was not passed`
213
+ );
214
+ }
215
+
216
+ if (sdkConfig.userIDToken) {
217
+ logger.metricCounter({
218
+ namespace: SHOPPER_INSIGHTS_METRIC_NAME,
219
+ event: "error",
220
+ dimensions: {
221
+ errorType: "merchant_configuration_validation_error",
222
+ validationDetails: "sdk_token_and_id_token_present",
223
+ },
224
+ });
225
+
226
+ throw new ValidationError(
227
+ `use script data attribute sdk-client-token instead of user-id-token`
228
+ );
229
+ }
230
+
231
+ // Client token has widely adopted integrations in the SDK that we do not want
232
+ // to support anymore. For now, we will be only enforcing a warning. We should
233
+ // expand on this warning with upgrade guides when we have them.
234
+ if (sdkConfig.clientToken) {
235
+ // eslint-disable-next-line no-console
236
+ console.warn(`script data attribute client-token is not recommended`);
237
+ }
238
+
239
+ return sdkConfig;
240
+ };
241
+
242
+ export interface ShopperInsightsInterface {
243
+ getRecommendedPaymentMethods: (
244
+ payload: MerchantPayloadData
245
+ ) => Promise<RecommendedPaymentMethods>;
246
+ }
247
+
248
+ export class ShopperSession {
249
+ logger: LoggerType;
250
+ request: Request;
251
+ requestId: string = "";
252
+ sdkConfig: SdkConfig;
253
+ sessionState: Storage;
254
+
255
+ constructor({
256
+ logger,
257
+ request,
258
+ sdkConfig,
259
+ sessionState,
260
+ }: {|
261
+ logger: LoggerType,
262
+ request: Request,
263
+ sdkConfig: SdkConfig,
264
+ sessionState: Storage,
265
+ |}) {
266
+ this.logger = logger;
267
+ this.request = request;
268
+ this.sdkConfig = parseSdkConfig({ sdkConfig, logger });
269
+ this.sessionState = sessionState;
270
+ }
271
+
272
+ async getRecommendedPaymentMethods(
273
+ merchantPayload: MerchantPayloadData
274
+ ): Promise<RecommendedPaymentMethods> {
275
+ const startTime = Date.now();
276
+ const data = parseMerchantPayload({
277
+ merchantPayload,
278
+ sdkConfig: this.sdkConfig,
279
+ });
280
+ try {
281
+ const body = await this.request<
282
+ RecommendedPaymentMethodsRequestData,
283
+ RecommendedPaymentMethodsResponse
284
+ >({
285
+ method: "POST",
286
+ url: `${this.sdkConfig.paypalApiDomain}/${ELIGIBLE_PAYMENT_METHODS}`,
287
+ data,
288
+ accessToken: this.sdkConfig.sdkToken,
289
+ });
290
+
291
+ this.sessionState.set("shopperInsights", {
292
+ getRecommendedPaymentMethodsUsed: true,
293
+ });
294
+
295
+ const { paypal, venmo } = body?.eligible_methods;
296
+
297
+ const isPayPalRecommended =
298
+ (paypal?.eligible_in_paypal_network && paypal?.recommended) || false;
299
+ const isVenmoRecommended =
300
+ (venmo?.eligible_in_paypal_network && venmo?.recommended) || false;
301
+
302
+ const fptiRecommendedPaymentPayload = {
303
+ paypal: isPayPalRecommended ? "1" : "0",
304
+ venmo: isVenmoRecommended ? "1" : "0",
305
+ };
306
+
307
+ this.logger.track({
308
+ [FPTI_KEY.TRANSITION]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_SUCCESS,
309
+ [FPTI_KEY.EVENT_NAME]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_SUCCESS,
310
+ [FPTI_KEY.RESPONSE_DURATION]: (Date.now() - startTime).toString(),
311
+ [FPTI_KEY.RECOMMENDED_PAYMENT]: JSON.stringify(
312
+ fptiRecommendedPaymentPayload
313
+ ),
314
+ });
315
+
316
+ this.logger.metricCounter({
317
+ namespace: SHOPPER_INSIGHTS_METRIC_NAME,
318
+ event: "success",
319
+ dimensions: {
320
+ isPayPalRecommended: String(isPayPalRecommended),
321
+ isVenmoRecommended: String(isVenmoRecommended),
322
+ },
323
+ });
324
+
325
+ return { isPayPalRecommended, isVenmoRecommended };
326
+ } catch (error) {
327
+ this.logger.metricCounter({
328
+ namespace: SHOPPER_INSIGHTS_METRIC_NAME,
329
+ event: "error",
330
+ dimensions: {
331
+ errorType: "api_error",
332
+ },
333
+ });
334
+
335
+ this.logger.track({
336
+ [FPTI_KEY.TRANSITION]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_ERROR,
337
+ [FPTI_KEY.EVENT_NAME]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_ERROR,
338
+ [FPTI_KEY.RESPONSE_DURATION]: (Date.now() - startTime).toString(),
339
+ });
340
+
341
+ this.logger.error("shopper_insights_api_error", {
342
+ err: stringifyError(error),
343
+ });
344
+
345
+ throw error;
346
+ }
347
+ }
348
+ }