@paypal/checkout-components 5.0.291 → 5.0.292
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 +3 -0
- package/dist/button.js +1 -1
- package/dist/test/button.js +1 -1
- package/package.json +1 -1
- package/src/api/api.js +54 -0
- package/src/api/shopper-insights/component.jsx +197 -0
- package/src/api/shopper-insights/component.test.js +282 -0
- package/src/api/shopper-insights/interface.js +13 -0
- package/src/api/shopper-insights/validation.js +137 -0
- package/src/api/shopper-insights/validation.test.js +155 -0
- package/src/constants/api.js +29 -0
package/package.json
CHANGED
package/src/api/api.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import { getPartnerAttributionID, getSessionID } from "@paypal/sdk-client/src";
|
|
4
|
+
import { 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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getUserIDToken,
|
|
5
|
+
getPageType,
|
|
6
|
+
getClientToken,
|
|
7
|
+
getSDKToken,
|
|
8
|
+
getLogger,
|
|
9
|
+
getPayPalAPIDomain,
|
|
10
|
+
getCurrency,
|
|
11
|
+
getBuyerCountry,
|
|
12
|
+
getEnv,
|
|
13
|
+
getSessionState,
|
|
14
|
+
sendCountMetric,
|
|
15
|
+
} from "@paypal/sdk-client/src";
|
|
16
|
+
import { FPTI_KEY } from "@paypal/sdk-constants/src";
|
|
17
|
+
import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
|
|
18
|
+
import { stringifyError } from "@krakenjs/belter/src";
|
|
19
|
+
|
|
20
|
+
import { callRestAPI } from "../api";
|
|
21
|
+
import {
|
|
22
|
+
ELIGIBLE_PAYMENT_METHODS,
|
|
23
|
+
FPTI_TRANSITION,
|
|
24
|
+
SHOPPER_INSIGHTS_METRIC_NAME,
|
|
25
|
+
type MerchantPayloadData,
|
|
26
|
+
} from "../../constants/api";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
validateMerchantConfig,
|
|
30
|
+
validateMerchantPayload,
|
|
31
|
+
hasEmail,
|
|
32
|
+
hasPhoneNumber,
|
|
33
|
+
} from "./validation";
|
|
34
|
+
|
|
35
|
+
type RecommendedPaymentMethods = {|
|
|
36
|
+
isPayPalRecommended: boolean,
|
|
37
|
+
isVenmoRecommended: boolean,
|
|
38
|
+
|};
|
|
39
|
+
|
|
40
|
+
type getRecommendedPaymentMethodsRequestPayload = {|
|
|
41
|
+
customer: {|
|
|
42
|
+
country_code?: string,
|
|
43
|
+
email?: string,
|
|
44
|
+
phone?: {|
|
|
45
|
+
country_code: string,
|
|
46
|
+
national_number: string,
|
|
47
|
+
|},
|
|
48
|
+
|},
|
|
49
|
+
purchase_units: $ReadOnlyArray<{|
|
|
50
|
+
amount: {|
|
|
51
|
+
currency_code: string,
|
|
52
|
+
|},
|
|
53
|
+
|}>,
|
|
54
|
+
preferences: {|
|
|
55
|
+
include_account_details: boolean,
|
|
56
|
+
|},
|
|
57
|
+
|};
|
|
58
|
+
|
|
59
|
+
export type ShopperInsightsComponent = {|
|
|
60
|
+
getRecommendedPaymentMethods: (MerchantPayloadData) => ZalgoPromise<RecommendedPaymentMethods>,
|
|
61
|
+
|};
|
|
62
|
+
|
|
63
|
+
function createRecommendedPaymentMethodsRequestPayload(
|
|
64
|
+
merchantPayload: MerchantPayloadData
|
|
65
|
+
): getRecommendedPaymentMethodsRequestPayload {
|
|
66
|
+
const isNonProdEnvironment = getEnv() !== "production";
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
customer: {
|
|
70
|
+
...(isNonProdEnvironment && {
|
|
71
|
+
country_code: getBuyerCountry() || "US",
|
|
72
|
+
}),
|
|
73
|
+
// $FlowIssue
|
|
74
|
+
...(hasEmail(merchantPayload) && {
|
|
75
|
+
email: merchantPayload?.customer?.email,
|
|
76
|
+
}),
|
|
77
|
+
...(hasPhoneNumber(merchantPayload) && {
|
|
78
|
+
phone: {
|
|
79
|
+
country_code: merchantPayload?.customer?.phone?.countryCode,
|
|
80
|
+
national_number: merchantPayload?.customer?.phone?.nationalNumber,
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
purchase_units: [
|
|
85
|
+
{
|
|
86
|
+
amount: {
|
|
87
|
+
currency_code: getCurrency(),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
// getRecommendedPaymentMethods maps to include_account_details in the API
|
|
92
|
+
preferences: {
|
|
93
|
+
include_account_details: true,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setShopperInsightsUsage() {
|
|
99
|
+
getSessionState((state) => {
|
|
100
|
+
return {
|
|
101
|
+
...state,
|
|
102
|
+
shopperInsights: {
|
|
103
|
+
getRecommendedPaymentMethodsUsed: true,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getShopperInsightsComponent(): ShopperInsightsComponent {
|
|
110
|
+
const startTime = Date.now();
|
|
111
|
+
|
|
112
|
+
sendCountMetric({
|
|
113
|
+
name: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
114
|
+
event: "init",
|
|
115
|
+
dimensions: {},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const sdkToken = getSDKToken();
|
|
119
|
+
const pageType = getPageType();
|
|
120
|
+
const clientToken = getClientToken();
|
|
121
|
+
const userIDToken = getUserIDToken();
|
|
122
|
+
|
|
123
|
+
getLogger().track({
|
|
124
|
+
[FPTI_KEY.TRANSITION]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_INIT,
|
|
125
|
+
[FPTI_KEY.EVENT_NAME]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_INIT,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
validateMerchantConfig({ sdkToken, pageType, userIDToken, clientToken });
|
|
129
|
+
|
|
130
|
+
const shopperInsights = {
|
|
131
|
+
getRecommendedPaymentMethods: (merchantPayload) => {
|
|
132
|
+
validateMerchantPayload(merchantPayload);
|
|
133
|
+
|
|
134
|
+
const requestPayload =
|
|
135
|
+
createRecommendedPaymentMethodsRequestPayload(merchantPayload);
|
|
136
|
+
|
|
137
|
+
return callRestAPI({
|
|
138
|
+
method: "POST",
|
|
139
|
+
url: `${getPayPalAPIDomain()}/${ELIGIBLE_PAYMENT_METHODS}`,
|
|
140
|
+
data: requestPayload,
|
|
141
|
+
accessToken: sdkToken,
|
|
142
|
+
})
|
|
143
|
+
.then((body) => {
|
|
144
|
+
setShopperInsightsUsage();
|
|
145
|
+
|
|
146
|
+
const paypal = body?.eligible_methods?.paypal;
|
|
147
|
+
const venmo = body?.eligible_methods?.venmo;
|
|
148
|
+
|
|
149
|
+
const isPayPalRecommended =
|
|
150
|
+
(paypal?.eligible_in_paypal_network && paypal?.recommended) ||
|
|
151
|
+
false;
|
|
152
|
+
const isVenmoRecommended =
|
|
153
|
+
(venmo?.eligible_in_paypal_network && venmo?.recommended) || false;
|
|
154
|
+
|
|
155
|
+
getLogger().track({
|
|
156
|
+
[FPTI_KEY.TRANSITION]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_SUCCESS,
|
|
157
|
+
[FPTI_KEY.EVENT_NAME]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_SUCCESS,
|
|
158
|
+
[FPTI_KEY.RESPONSE_DURATION]: (Date.now() - startTime).toString(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
sendCountMetric({
|
|
162
|
+
name: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
163
|
+
event: "success",
|
|
164
|
+
dimensions: {
|
|
165
|
+
isPayPalRecommended: String(isPayPalRecommended),
|
|
166
|
+
isVenmoRecommended: String(isVenmoRecommended),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return { isPayPalRecommended, isVenmoRecommended };
|
|
171
|
+
})
|
|
172
|
+
.catch((err) => {
|
|
173
|
+
sendCountMetric({
|
|
174
|
+
name: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
175
|
+
event: "error",
|
|
176
|
+
dimensions: {
|
|
177
|
+
errorType: "api_error",
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
getLogger().track({
|
|
182
|
+
[FPTI_KEY.TRANSITION]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_ERROR,
|
|
183
|
+
[FPTI_KEY.EVENT_NAME]: FPTI_TRANSITION.SHOPPER_INSIGHTS_API_ERROR,
|
|
184
|
+
[FPTI_KEY.RESPONSE_DURATION]: (Date.now() - startTime).toString(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
getLogger().error("shopper_insights_api_error", {
|
|
188
|
+
err: stringifyError(err),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
throw err;
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return shopperInsights;
|
|
197
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
import { ZalgoPromise } from "@krakenjs/zalgo-promise/src";
|
|
3
|
+
import { getEnv, getBuyerCountry } from "@paypal/sdk-client/src";
|
|
4
|
+
import { vi, describe, expect } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { callRestAPI } from "../api";
|
|
7
|
+
|
|
8
|
+
import { getShopperInsightsComponent } from "./component";
|
|
9
|
+
|
|
10
|
+
vi.mock("@paypal/sdk-client/src", () => {
|
|
11
|
+
return {
|
|
12
|
+
sendCountMetric: vi.fn(),
|
|
13
|
+
getSDKToken: vi.fn(() => "sdk-token"),
|
|
14
|
+
getPageType: vi.fn(() => "product-details"),
|
|
15
|
+
getClientToken: vi.fn(() => ""),
|
|
16
|
+
getUserIDToken: vi.fn(() => ""),
|
|
17
|
+
getEnv: vi.fn(() => "production"),
|
|
18
|
+
getCurrency: vi.fn(() => "USD"),
|
|
19
|
+
getBuyerCountry: vi.fn(() => "US"),
|
|
20
|
+
getPayPalAPIDomain: vi.fn(() => "https://api.paypal.com"),
|
|
21
|
+
getPartnerAttributionID: vi.fn(() => ""),
|
|
22
|
+
getSessionID: vi.fn(() => "sdk-session-ID-123"),
|
|
23
|
+
getSessionState: vi.fn(),
|
|
24
|
+
getLogger: vi.fn(() => ({ track: vi.fn(), error: vi.fn() })),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
vi.mock("../api", async () => {
|
|
29
|
+
const actual = await vi.importActual("../api");
|
|
30
|
+
return {
|
|
31
|
+
...actual,
|
|
32
|
+
callRestAPI: vi.fn(() =>
|
|
33
|
+
ZalgoPromise.resolve({
|
|
34
|
+
eligible_methods: {
|
|
35
|
+
paypal: {
|
|
36
|
+
can_be_vaulted: true,
|
|
37
|
+
eligible_in_paypal_network: true,
|
|
38
|
+
recommended: true,
|
|
39
|
+
recommended_priority: 1,
|
|
40
|
+
},
|
|
41
|
+
venmo: {
|
|
42
|
+
can_be_vaulted: true,
|
|
43
|
+
eligible_in_paypal_network: true,
|
|
44
|
+
recommended: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("shopper insights component - getRecommendedPaymentMethods()", () => {
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("should get recommended payment methods using the shopper insights API", async () => {
|
|
58
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
59
|
+
const recommendedPaymentMethods =
|
|
60
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
61
|
+
customer: {
|
|
62
|
+
email: "email@test.com",
|
|
63
|
+
phone: {
|
|
64
|
+
countryCode: "1",
|
|
65
|
+
nationalNumber: "2345678901",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(callRestAPI).toHaveBeenCalled();
|
|
71
|
+
expect(recommendedPaymentMethods).toEqual({
|
|
72
|
+
isPayPalRecommended: true,
|
|
73
|
+
isVenmoRecommended: false,
|
|
74
|
+
});
|
|
75
|
+
expect.assertions(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("catch errors from the API", async () => {
|
|
79
|
+
// $FlowFixMe
|
|
80
|
+
callRestAPI.mockImplementationOnce(() =>
|
|
81
|
+
ZalgoPromise.reject({
|
|
82
|
+
name: "ERROR",
|
|
83
|
+
message: "This is an API error",
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
88
|
+
|
|
89
|
+
await expect(() =>
|
|
90
|
+
shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
91
|
+
customer: {
|
|
92
|
+
email: "email@test.com",
|
|
93
|
+
phone: {
|
|
94
|
+
countryCode: "1",
|
|
95
|
+
nationalNumber: "2345678901",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
).rejects.toThrow("This is an API error");
|
|
100
|
+
expect(callRestAPI).toHaveBeenCalled();
|
|
101
|
+
expect.assertions(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("create customer payload with email and phone number", async () => {
|
|
105
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
106
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
107
|
+
customer: {
|
|
108
|
+
email: "email@test.com",
|
|
109
|
+
phone: {
|
|
110
|
+
countryCode: "1",
|
|
111
|
+
nationalNumber: "2345678901",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
data: expect.objectContaining({
|
|
119
|
+
customer: expect.objectContaining({
|
|
120
|
+
email: "email@test.com",
|
|
121
|
+
phone: expect.objectContaining({
|
|
122
|
+
country_code: "1",
|
|
123
|
+
national_number: "2345678901",
|
|
124
|
+
}),
|
|
125
|
+
}),
|
|
126
|
+
}),
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("create customer payload with email only", async () => {
|
|
132
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
133
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
134
|
+
customer: {
|
|
135
|
+
email: "email@test.com",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
data: expect.objectContaining({
|
|
142
|
+
customer: expect.objectContaining({
|
|
143
|
+
email: "email@test.com",
|
|
144
|
+
}),
|
|
145
|
+
}),
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("create customer payload with phone only", async () => {
|
|
151
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
152
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
153
|
+
customer: {
|
|
154
|
+
email: "email@test.com",
|
|
155
|
+
phone: {
|
|
156
|
+
countryCode: "1",
|
|
157
|
+
nationalNumber: "2345678901",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
data: expect.objectContaining({
|
|
165
|
+
customer: expect.objectContaining({
|
|
166
|
+
phone: expect.objectContaining({
|
|
167
|
+
country_code: "1",
|
|
168
|
+
national_number: "2345678901",
|
|
169
|
+
}),
|
|
170
|
+
}),
|
|
171
|
+
}),
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("should default purchase units with currency code in the customer payload", async () => {
|
|
177
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
178
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
179
|
+
customer: {
|
|
180
|
+
email: "email@test.com",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
185
|
+
expect.objectContaining({
|
|
186
|
+
data: expect.objectContaining({
|
|
187
|
+
purchase_units: expect.arrayContaining([
|
|
188
|
+
expect.objectContaining({
|
|
189
|
+
amount: expect.objectContaining({
|
|
190
|
+
currency_code: "USD",
|
|
191
|
+
}),
|
|
192
|
+
}),
|
|
193
|
+
]),
|
|
194
|
+
}),
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("should use the SDK buyer-country parameter if country code is not passed in a non-prod env", async () => {
|
|
200
|
+
// $FlowFixMe
|
|
201
|
+
getEnv.mockImplementationOnce(() => "stage");
|
|
202
|
+
|
|
203
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
204
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
205
|
+
customer: {
|
|
206
|
+
email: "email@test.com",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
data: expect.objectContaining({
|
|
213
|
+
customer: expect.objectContaining({
|
|
214
|
+
country_code: "US",
|
|
215
|
+
}),
|
|
216
|
+
}),
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("should default US country code if SDK buyer-country parameter not passed in a non-prod env", async () => {
|
|
222
|
+
// $FlowFixMe
|
|
223
|
+
getEnv.mockImplementationOnce(() => "stage");
|
|
224
|
+
// $FlowFixMe
|
|
225
|
+
getBuyerCountry.mockImplementationOnce(() => "");
|
|
226
|
+
|
|
227
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
228
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
229
|
+
customer: {
|
|
230
|
+
email: "email@test.com",
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
data: expect.objectContaining({
|
|
237
|
+
customer: expect.objectContaining({
|
|
238
|
+
country_code: "US",
|
|
239
|
+
}),
|
|
240
|
+
}),
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("should not set country code in prod env in the customer payload", async () => {
|
|
246
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
247
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
248
|
+
customer: {
|
|
249
|
+
email: "email@test.com",
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
data: expect.objectContaining({
|
|
256
|
+
customer: expect.not.objectContaining({
|
|
257
|
+
country_code: expect.anything(),
|
|
258
|
+
}),
|
|
259
|
+
}),
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("should request recommended payment methods by setting account details in the payload", async () => {
|
|
265
|
+
const shopperInsightsComponent = getShopperInsightsComponent();
|
|
266
|
+
await shopperInsightsComponent.getRecommendedPaymentMethods({
|
|
267
|
+
customer: {
|
|
268
|
+
email: "email@test.com",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(callRestAPI).toHaveBeenCalledWith(
|
|
273
|
+
expect.objectContaining({
|
|
274
|
+
data: expect.objectContaining({
|
|
275
|
+
preferences: expect.objectContaining({
|
|
276
|
+
include_account_details: true,
|
|
277
|
+
}),
|
|
278
|
+
}),
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import type { LazyProtectedExport } from "../../types";
|
|
4
|
+
import { protectedExport } from "../../lib";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getShopperInsightsComponent,
|
|
8
|
+
type ShopperInsightsComponent,
|
|
9
|
+
} from "./component";
|
|
10
|
+
|
|
11
|
+
export const ShopperInsights: LazyProtectedExport<ShopperInsightsComponent> = {
|
|
12
|
+
__get__: () => protectedExport(getShopperInsightsComponent()),
|
|
13
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import { sendCountMetric } from "@paypal/sdk-client/src";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SHOPPER_INSIGHTS_METRIC_NAME,
|
|
7
|
+
type MerchantPayloadData,
|
|
8
|
+
} from "../../constants/api";
|
|
9
|
+
import { ValidationError } from "../../lib";
|
|
10
|
+
|
|
11
|
+
type MerchantConfigParams = {|
|
|
12
|
+
sdkToken: ?string,
|
|
13
|
+
pageType: ?string,
|
|
14
|
+
userIDToken: ?string,
|
|
15
|
+
clientToken: ?string,
|
|
16
|
+
|};
|
|
17
|
+
|
|
18
|
+
export function validateMerchantConfig({
|
|
19
|
+
sdkToken,
|
|
20
|
+
pageType,
|
|
21
|
+
userIDToken,
|
|
22
|
+
clientToken,
|
|
23
|
+
}: MerchantConfigParams) {
|
|
24
|
+
if (!sdkToken) {
|
|
25
|
+
sendCountMetric({
|
|
26
|
+
name: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
27
|
+
event: "error",
|
|
28
|
+
dimensions: {
|
|
29
|
+
errorType: "merchant_configuration_validation_error",
|
|
30
|
+
validationDetails: "sdk_token_not_present",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
throw new ValidationError(
|
|
35
|
+
`script data attribute sdk-client-token is required but was not passed`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!pageType) {
|
|
40
|
+
sendCountMetric({
|
|
41
|
+
name: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
42
|
+
event: "error",
|
|
43
|
+
dimensions: {
|
|
44
|
+
errorType: "merchant_configuration_validation_error",
|
|
45
|
+
validationDetails: "page_type_not_present",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
throw new ValidationError(
|
|
50
|
+
`script data attribute page-type is required but was not passed`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (userIDToken) {
|
|
55
|
+
sendCountMetric({
|
|
56
|
+
name: SHOPPER_INSIGHTS_METRIC_NAME,
|
|
57
|
+
event: "error",
|
|
58
|
+
dimensions: {
|
|
59
|
+
errorType: "merchant_configuration_validation_error",
|
|
60
|
+
validationDetails: "sdk_token_and_id_token_present",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
throw new ValidationError(
|
|
65
|
+
`use script data attribute sdk-client-token instead of user-id-token`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Client token has widely adopted integrations in the SDK that we do not want
|
|
70
|
+
// to support anymore. For now, we will be only enforcing a warning. We should
|
|
71
|
+
// expand on this warning with upgrade guides when we have them.
|
|
72
|
+
if (clientToken) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.warn(`script data attribute client-token is not recommended`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const hasEmail = (merchantPayload: MerchantPayloadData): boolean => {
|
|
79
|
+
return Boolean(merchantPayload?.customer?.email);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const hasPhoneNumber = (
|
|
83
|
+
merchantPayload: MerchantPayloadData
|
|
84
|
+
): boolean => {
|
|
85
|
+
return Boolean(
|
|
86
|
+
merchantPayload?.customer?.phone?.countryCode &&
|
|
87
|
+
merchantPayload?.customer?.phone?.nationalNumber
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const isValidEmailFormat = (email: string): boolean => {
|
|
92
|
+
const emailRegex = /^.+@.+$/;
|
|
93
|
+
return email.length < 320 && emailRegex.test(email);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const isValidPhoneNumberFormat = (phoneNumber: string): boolean => {
|
|
97
|
+
const phoneNumberRegex = /\d{5,}/;
|
|
98
|
+
return phoneNumberRegex.test(phoneNumber);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function validateMerchantPayload(merchantPayload: MerchantPayloadData) {
|
|
102
|
+
if (
|
|
103
|
+
typeof merchantPayload !== "object" ||
|
|
104
|
+
Object.keys(merchantPayload).length === 0 ||
|
|
105
|
+
!Object.keys(merchantPayload).includes("customer")
|
|
106
|
+
) {
|
|
107
|
+
throw new ValidationError(
|
|
108
|
+
`Expected shopper information to be passed into customer object`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const hasEmailOrPhoneNumber =
|
|
113
|
+
hasEmail(merchantPayload) || hasPhoneNumber(merchantPayload);
|
|
114
|
+
if (!hasEmailOrPhoneNumber) {
|
|
115
|
+
throw new ValidationError(
|
|
116
|
+
`Expected shopper information to include an email or phone number`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const merchantPayloadEmail = merchantPayload?.customer?.email || "";
|
|
121
|
+
if (hasEmail(merchantPayload) && !isValidEmailFormat(merchantPayloadEmail)) {
|
|
122
|
+
throw new ValidationError(
|
|
123
|
+
`Expected shopper information to include a valid email format`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const merchantPhonePayload = merchantPayload?.customer?.phone || {};
|
|
128
|
+
const nationalNumber = merchantPhonePayload?.nationalNumber || "";
|
|
129
|
+
if (
|
|
130
|
+
hasPhoneNumber(merchantPayload) &&
|
|
131
|
+
!isValidPhoneNumberFormat(nationalNumber)
|
|
132
|
+
) {
|
|
133
|
+
throw new ValidationError(
|
|
134
|
+
`Expected shopper information to a valid phone number format`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|