@paypal/checkout-components 5.0.301 → 5.0.302-alpha.1
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/lib/api.js +67 -0
- package/src/lib/index.js +1 -0
- package/src/shopper-insights/interface.js +64 -0
- package/src/shopper-insights/shopperSession.js +342 -0
- package/src/shopper-insights/shopperSession.test.js +376 -0
- package/src/api/shopper-insights/validation.js +0 -120
- package/src/api/shopper-insights/validation.test.js +0 -150
package/package.json
CHANGED
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
|
@@ -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,342 @@
|
|
|
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
|
+
body: {|
|
|
50
|
+
eligible_methods: {
|
|
51
|
+
[paymentMethod: "paypal" | "venmo"]: {|
|
|
52
|
+
can_be_vaulted: boolean,
|
|
53
|
+
eligible_in_paypal_network?: boolean,
|
|
54
|
+
recommended?: boolean,
|
|
55
|
+
recommended_priority?: number,
|
|
56
|
+
|},
|
|
57
|
+
},
|
|
58
|
+
|},
|
|
59
|
+
|};
|
|
60
|
+
|
|
61
|
+
type SdkConfig = {|
|
|
62
|
+
sdkToken: ?string,
|
|
63
|
+
pageType: ?string,
|
|
64
|
+
userIDToken: ?string,
|
|
65
|
+
clientToken: ?string,
|
|
66
|
+
paypalApiDomain: string,
|
|
67
|
+
environment: ?string,
|
|
68
|
+
buyerCountry: string,
|
|
69
|
+
currency: string,
|
|
70
|
+
|};
|
|
71
|
+
|
|
72
|
+
// eslint's flow integration is very out of date
|
|
73
|
+
// it doesn't recognize the generics here as used
|
|
74
|
+
// eslint-disable-next-line no-undef
|
|
75
|
+
type Request = <TRequestData, TResponse>({|
|
|
76
|
+
method?: string,
|
|
77
|
+
url: string,
|
|
78
|
+
// eslint-disable-next-line no-undef
|
|
79
|
+
data: TRequestData,
|
|
80
|
+
accessToken: ?string,
|
|
81
|
+
// eslint-disable-next-line no-undef
|
|
82
|
+
|}) => Promise<TResponse>;
|
|
83
|
+
|
|
84
|
+
type Storage = {|
|
|
85
|
+
// eslint's flow integration is very out of date
|
|
86
|
+
// it doesn't recognize the generics here as used
|
|
87
|
+
// eslint-disable-next-line no-undef
|
|
88
|
+
get: <TValue>(key: string) => ?TValue,
|
|
89
|
+
// eslint-disable-next-line flowtype/no-weak-types
|
|
90
|
+
set: (key: string, value: any) => void,
|
|
91
|
+
|};
|
|
92
|
+
|
|
93
|
+
const parseEmail = (merchantPayload): ?{| email: string |} => {
|
|
94
|
+
if (!merchantPayload.email) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const email = merchantPayload.email;
|
|
99
|
+
const isValidEmail =
|
|
100
|
+
typeof email === "string" && email.length < 320 && /^.+@.+$/.test(email);
|
|
101
|
+
|
|
102
|
+
if (!isValidEmail) {
|
|
103
|
+
throw new ValidationError(
|
|
104
|
+
`Expected shopper information to include a valid email format`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
email,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const parsePhone = (
|
|
114
|
+
merchantPayload
|
|
115
|
+
): ?{| phone: {| country_code: string, national_number: string |} |} => {
|
|
116
|
+
if (!merchantPayload.phone) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
!merchantPayload.phone.nationalNumber ||
|
|
122
|
+
!merchantPayload.phone.countryCode
|
|
123
|
+
) {
|
|
124
|
+
throw new ValidationError(
|
|
125
|
+
`Expected phone number for shopper insights to include nationalNumber and countryCode`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nationalNumber = merchantPayload.phone.nationalNumber;
|
|
130
|
+
const countryCode = merchantPayload.phone.countryCode;
|
|
131
|
+
const isValidPhone =
|
|
132
|
+
typeof nationalNumber === "string" && /\d{5,}/.test(nationalNumber);
|
|
133
|
+
|
|
134
|
+
if (!isValidPhone) {
|
|
135
|
+
throw new ValidationError(
|
|
136
|
+
`Expected shopper information to be a valid phone number format`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
phone: {
|
|
142
|
+
country_code: countryCode,
|
|
143
|
+
national_number: nationalNumber,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const parseMerchantPayload = ({
|
|
149
|
+
merchantPayload,
|
|
150
|
+
sdkConfig,
|
|
151
|
+
}: {|
|
|
152
|
+
merchantPayload: MerchantPayloadData,
|
|
153
|
+
sdkConfig: SdkConfig,
|
|
154
|
+
|}): RecommendedPaymentMethodsRequestData => {
|
|
155
|
+
const email = parseEmail(merchantPayload);
|
|
156
|
+
const phone = parsePhone(merchantPayload);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
customer: {
|
|
160
|
+
...(sdkConfig.environment !== "production" && {
|
|
161
|
+
country_code: sdkConfig.buyerCountry,
|
|
162
|
+
}),
|
|
163
|
+
// $FlowIssue too many cases?
|
|
164
|
+
...email,
|
|
165
|
+
...phone,
|
|
166
|
+
},
|
|
167
|
+
purchase_units: [
|
|
168
|
+
{
|
|
169
|
+
amount: {
|
|
170
|
+
currency_code: sdkConfig.currency,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
// getRecommendedPaymentMethods maps to include_account_details in the API
|
|
175
|
+
preferences: {
|
|
176
|
+
include_account_details: true,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const parseSdkConfig = ({
|
|
182
|
+
sdkConfig,
|
|
183
|
+
logger,
|
|
184
|
+
}: {|
|
|
185
|
+
sdkConfig: SdkConfig,
|
|
186
|
+
logger: LoggerType,
|
|
187
|
+
|}): SdkConfig => {
|
|
188
|
+
if (!sdkConfig.sdkToken) {
|
|
189
|
+
logger.metricCounter({
|
|
190
|
+
namespace: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
191
|
+
event: "error",
|
|
192
|
+
dimensions: {
|
|
193
|
+
errorType: "merchant_configuration_validation_error",
|
|
194
|
+
validationDetails: "sdk_token_not_present",
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
throw new ValidationError(
|
|
199
|
+
`script data attribute sdk-client-token is required but was not passed`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!sdkConfig.pageType) {
|
|
204
|
+
logger.metricCounter({
|
|
205
|
+
namespace: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
206
|
+
event: "error",
|
|
207
|
+
dimensions: {
|
|
208
|
+
errorType: "merchant_configuration_validation_error",
|
|
209
|
+
validationDetails: "page_type_not_present",
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
throw new ValidationError(
|
|
214
|
+
`script data attribute page-type is required but was not passed`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (sdkConfig.userIDToken) {
|
|
219
|
+
logger.metricCounter({
|
|
220
|
+
namespace: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
221
|
+
event: "error",
|
|
222
|
+
dimensions: {
|
|
223
|
+
errorType: "merchant_configuration_validation_error",
|
|
224
|
+
validationDetails: "sdk_token_and_id_token_present",
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
throw new ValidationError(
|
|
229
|
+
`use script data attribute sdk-client-token instead of user-id-token`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Client token has widely adopted integrations in the SDK that we do not want
|
|
234
|
+
// to support anymore. For now, we will be only enforcing a warning. We should
|
|
235
|
+
// expand on this warning with upgrade guides when we have them.
|
|
236
|
+
if (sdkConfig.clientToken) {
|
|
237
|
+
// eslint-disable-next-line no-console
|
|
238
|
+
console.warn(`script data attribute client-token is not recommended`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return sdkConfig;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export interface ShopperInsightsInterface {
|
|
245
|
+
getRecommendedPaymentMethods: (
|
|
246
|
+
payload: MerchantPayloadData
|
|
247
|
+
) => Promise<RecommendedPaymentMethods>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export class ShopperSession {
|
|
251
|
+
logger: LoggerType;
|
|
252
|
+
request: Request;
|
|
253
|
+
requestId: string = "";
|
|
254
|
+
sdkConfig: SdkConfig;
|
|
255
|
+
sessionState: Storage;
|
|
256
|
+
|
|
257
|
+
constructor({
|
|
258
|
+
logger,
|
|
259
|
+
request,
|
|
260
|
+
sdkConfig,
|
|
261
|
+
sessionState,
|
|
262
|
+
}: {|
|
|
263
|
+
logger: LoggerType,
|
|
264
|
+
request: Request,
|
|
265
|
+
sdkConfig: SdkConfig,
|
|
266
|
+
sessionState: Storage,
|
|
267
|
+
|}) {
|
|
268
|
+
this.logger = logger;
|
|
269
|
+
this.request = request;
|
|
270
|
+
this.sdkConfig = parseSdkConfig({ sdkConfig, logger });
|
|
271
|
+
this.sessionState = sessionState;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async getRecommendedPaymentMethods(
|
|
275
|
+
merchantPayload: MerchantPayloadData
|
|
276
|
+
): Promise<RecommendedPaymentMethods> {
|
|
277
|
+
const startTime = Date.now();
|
|
278
|
+
const data = parseMerchantPayload({
|
|
279
|
+
merchantPayload,
|
|
280
|
+
sdkConfig: this.sdkConfig,
|
|
281
|
+
});
|
|
282
|
+
try {
|
|
283
|
+
const { body } = await this.request<
|
|
284
|
+
RecommendedPaymentMethodsRequestData,
|
|
285
|
+
RecommendedPaymentMethodsResponse
|
|
286
|
+
>({
|
|
287
|
+
method: "POST",
|
|
288
|
+
url: `${this.sdkConfig.paypalApiDomain}/${ELIGIBLE_PAYMENT_METHODS}`,
|
|
289
|
+
data,
|
|
290
|
+
accessToken: this.sdkConfig.sdkToken,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
this.sessionState.set("shopperInsights", {
|
|
294
|
+
getRecommendedPaymentMethodsUsed: true,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const { paypal, venmo } = body?.eligible_methods;
|
|
298
|
+
|
|
299
|
+
const isPayPalRecommended =
|
|
300
|
+
(paypal?.eligible_in_paypal_network && paypal?.recommended) || false;
|
|
301
|
+
const isVenmoRecommended =
|
|
302
|
+
(venmo?.eligible_in_paypal_network && venmo?.recommended) || false;
|
|
303
|
+
|
|
304
|
+
this.logger.track({
|
|
305
|
+
[FPTI_KEY.TRANSITION]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_SUCCESS,
|
|
306
|
+
[FPTI_KEY.EVENT_NAME]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_SUCCESS,
|
|
307
|
+
[FPTI_KEY.RESPONSE_DURATION]: (Date.now() - startTime).toString(),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.logger.metricCounter({
|
|
311
|
+
namespace: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
312
|
+
event: "success",
|
|
313
|
+
dimensions: {
|
|
314
|
+
isPayPalRecommended: String(isPayPalRecommended),
|
|
315
|
+
isVenmoRecommended: String(isVenmoRecommended),
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return { isPayPalRecommended, isVenmoRecommended };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
this.logger.metricCounter({
|
|
322
|
+
namespace: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
323
|
+
event: "error",
|
|
324
|
+
dimensions: {
|
|
325
|
+
errorType: "api_error",
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
this.logger.track({
|
|
330
|
+
[FPTI_KEY.TRANSITION]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_ERROR,
|
|
331
|
+
[FPTI_KEY.EVENT_NAME]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_ERROR,
|
|
332
|
+
[FPTI_KEY.RESPONSE_DURATION]: (Date.now() - startTime).toString(),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
this.logger.error("shopper_insights_api_error", {
|
|
336
|
+
err: stringifyError(error),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|