@paypal/checkout-components 5.0.404 → 5.0.405-alpha-da545e5.0
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/appSwitchResume.js +42 -1
- package/src/lib/appSwithResume.test.js +196 -3
package/package.json
CHANGED
|
@@ -16,13 +16,54 @@ export type AppSwitchResumeParams = {|
|
|
|
16
16
|
checkoutState: "onApprove" | "onCancel" | "onError",
|
|
17
17
|
|};
|
|
18
18
|
|
|
19
|
+
// When the merchant's return_url contains a hash fragment (e.g. /checkout/#payment),
|
|
20
|
+
// PayPal params (token, PayerID) end up inside the hash as /checkout/#payment?token=...&PayerID=...
|
|
21
|
+
// because the base URL cannot be modified (iOS Safari uses it for tab matching in app switch).
|
|
22
|
+
// This helper extracts query-style params embedded in the hash fragment.
|
|
23
|
+
function getParamsFromHashFragment(): { [string]: string } {
|
|
24
|
+
const hashString =
|
|
25
|
+
window.location.hash && String(window.location.hash).slice(1);
|
|
26
|
+
if (!hashString) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for ? delimiter first (e.g. #payment?token=...)
|
|
31
|
+
const questionMarkIndex = hashString.indexOf("?");
|
|
32
|
+
if (questionMarkIndex !== -1) {
|
|
33
|
+
const queryString = hashString.slice(questionMarkIndex + 1);
|
|
34
|
+
return Object.fromEntries(new URLSearchParams(queryString));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback to & delimiter (e.g. #payment&token=...)
|
|
38
|
+
const ampersandIndex = hashString.indexOf("&");
|
|
39
|
+
if (ampersandIndex !== -1) {
|
|
40
|
+
const queryString = hashString.slice(ampersandIndex + 1);
|
|
41
|
+
return Object.fromEntries(new URLSearchParams(queryString));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
// The Web fallback flow uses different set of query params then appswitch flow.
|
|
20
48
|
function getAppSwitchParamsWebFallback(): AppSwitchResumeParams | null {
|
|
21
49
|
try {
|
|
22
|
-
const
|
|
50
|
+
const searchParams = Object.fromEntries(
|
|
23
51
|
// eslint-disable-next-line compat/compat
|
|
24
52
|
new URLSearchParams(window.location.search)
|
|
25
53
|
);
|
|
54
|
+
|
|
55
|
+
// If no PayPal params found in query string, check if they are embedded
|
|
56
|
+
// inside the hash fragment. This happens when the merchant's return_url
|
|
57
|
+
// contains a hash (e.g. /checkout/#payment) and PayPal params were appended
|
|
58
|
+
// after the fragment: /checkout/#payment?token=...&PayerID=...
|
|
59
|
+
const params =
|
|
60
|
+
searchParams.token ||
|
|
61
|
+
searchParams.vaultSetupToken ||
|
|
62
|
+
searchParams.approval_token_id ||
|
|
63
|
+
searchParams.approval_session_id
|
|
64
|
+
? searchParams
|
|
65
|
+
: { ...getParamsFromHashFragment(), ...searchParams };
|
|
66
|
+
|
|
26
67
|
const {
|
|
27
68
|
button_session_id: buttonSessionID,
|
|
28
69
|
fundingSource,
|
|
@@ -70,16 +70,24 @@ describe("app switch resume flow", () => {
|
|
|
70
70
|
expect(isAppSwitchResumeFlow()).toEqual(false);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
test("should
|
|
73
|
+
test("should extract resume params from hash with non-action prefix via web fallback", () => {
|
|
74
|
+
// When hash is not a known action (e.g. #Unknown) but contains PayPal params,
|
|
75
|
+
// the web fallback should extract them from the hash fragment.
|
|
74
76
|
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
75
77
|
hash: `#Unknown?button_session_id=${buttonSessionID}&token=${orderID}&fundingSource=${fundingSource}`,
|
|
78
|
+
search: "",
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
const params = getAppSwitchResumeParams();
|
|
79
82
|
|
|
80
83
|
expect.assertions(2);
|
|
81
|
-
expect(params).toEqual(
|
|
82
|
-
|
|
84
|
+
expect(params).toEqual({
|
|
85
|
+
buttonSessionID,
|
|
86
|
+
checkoutState: "onCancel",
|
|
87
|
+
fundingSource,
|
|
88
|
+
orderID,
|
|
89
|
+
});
|
|
90
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
83
91
|
});
|
|
84
92
|
|
|
85
93
|
test("should test fetching multiple resume params when parameters are correctly passed", () => {
|
|
@@ -140,4 +148,189 @@ describe("app switch resume flow", () => {
|
|
|
140
148
|
});
|
|
141
149
|
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
142
150
|
});
|
|
151
|
+
|
|
152
|
+
test("should extract resume params when merchant return_url has hash fragment with ? delimiter", () => {
|
|
153
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
154
|
+
hash: `#payment?button_session_id=${buttonSessionID}&token=${orderID}&fundingSource=${fundingSource}&PayerID=PP-payer-122`,
|
|
155
|
+
search: "",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const params = getAppSwitchResumeParams();
|
|
159
|
+
|
|
160
|
+
expect(params).toEqual({
|
|
161
|
+
buttonSessionID,
|
|
162
|
+
checkoutState: "onApprove",
|
|
163
|
+
fundingSource,
|
|
164
|
+
orderID,
|
|
165
|
+
payerID: "PP-payer-122",
|
|
166
|
+
});
|
|
167
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("should extract resume params when merchant return_url has hash fragment without PayerID (cancel)", () => {
|
|
171
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
172
|
+
hash: `#payment?button_session_id=${buttonSessionID}&token=${orderID}&fundingSource=${fundingSource}`,
|
|
173
|
+
search: "",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const params = getAppSwitchResumeParams();
|
|
177
|
+
|
|
178
|
+
expect(params).toEqual({
|
|
179
|
+
buttonSessionID,
|
|
180
|
+
checkoutState: "onCancel",
|
|
181
|
+
fundingSource,
|
|
182
|
+
orderID,
|
|
183
|
+
});
|
|
184
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("should extract resume params when hash uses & delimiter instead of ?", () => {
|
|
188
|
+
// Real-world case: URL like /ppcp-js-sdk?clientSideDelay=0#payment&token=...&PayerID=...
|
|
189
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
190
|
+
hash: `#payment&token=${orderID}&PayerID=PP-payer-122&button_session_id=${buttonSessionID}`,
|
|
191
|
+
search: "?clientSideDelay=0&serverSideDelay=0",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const params = getAppSwitchResumeParams();
|
|
195
|
+
|
|
196
|
+
expect(params).toEqual({
|
|
197
|
+
buttonSessionID,
|
|
198
|
+
checkoutState: "onApprove",
|
|
199
|
+
orderID,
|
|
200
|
+
payerID: "PP-payer-122",
|
|
201
|
+
});
|
|
202
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("should extract vault resume params from hash fragment", () => {
|
|
206
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
207
|
+
hash: `#/step3?button_session_id=${buttonSessionID}&token=${orderID}&approval_token_id=VA-3`,
|
|
208
|
+
search: "",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const params = getAppSwitchResumeParams();
|
|
212
|
+
|
|
213
|
+
expect(params).toEqual({
|
|
214
|
+
buttonSessionID,
|
|
215
|
+
checkoutState: "onCancel",
|
|
216
|
+
orderID,
|
|
217
|
+
vaultSetupToken: "VA-3",
|
|
218
|
+
});
|
|
219
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("should prefer search params over hash params when both exist", () => {
|
|
223
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
224
|
+
hash: `#payment?token=WRONG-TOKEN`,
|
|
225
|
+
search: `?button_session_id=${buttonSessionID}&token=${orderID}&PayerID=PP-123`,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const params = getAppSwitchResumeParams();
|
|
229
|
+
|
|
230
|
+
expect(params).toEqual({
|
|
231
|
+
buttonSessionID,
|
|
232
|
+
checkoutState: "onApprove",
|
|
233
|
+
orderID,
|
|
234
|
+
payerID: "PP-123",
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("should return null when hash has merchant fragment but no PayPal params", () => {
|
|
239
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
240
|
+
hash: "#payment",
|
|
241
|
+
search: "",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const params = getAppSwitchResumeParams();
|
|
245
|
+
|
|
246
|
+
expect(params).toEqual(null);
|
|
247
|
+
expect(isAppSwitchResumeFlow()).toEqual(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("should return null when hash has merchant fragment with unrelated params", () => {
|
|
251
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
252
|
+
hash: "#/checkout?step=review&cart=abc123",
|
|
253
|
+
search: "",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const params = getAppSwitchResumeParams();
|
|
257
|
+
|
|
258
|
+
expect(params).toEqual(null);
|
|
259
|
+
expect(isAppSwitchResumeFlow()).toEqual(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("should handle vaultSetupToken in search params", () => {
|
|
263
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
264
|
+
hash: "",
|
|
265
|
+
search: `?button_session_id=${buttonSessionID}&vaultSetupToken=VA-123&PayerID=PP-456`,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const params = getAppSwitchResumeParams();
|
|
269
|
+
|
|
270
|
+
expect(params).toEqual({
|
|
271
|
+
buttonSessionID,
|
|
272
|
+
checkoutState: "onApprove",
|
|
273
|
+
payerID: "PP-456",
|
|
274
|
+
vaultSetupToken: "VA-123",
|
|
275
|
+
});
|
|
276
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("should handle approval_token_id in search params", () => {
|
|
280
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
281
|
+
hash: "",
|
|
282
|
+
search: `?button_session_id=${buttonSessionID}&approval_token_id=AT-789`,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const params = getAppSwitchResumeParams();
|
|
286
|
+
|
|
287
|
+
expect(params).toEqual({
|
|
288
|
+
buttonSessionID,
|
|
289
|
+
checkoutState: "onCancel",
|
|
290
|
+
vaultSetupToken: "AT-789",
|
|
291
|
+
});
|
|
292
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("should handle approval_session_id in search params", () => {
|
|
296
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
297
|
+
hash: "",
|
|
298
|
+
search: `?button_session_id=${buttonSessionID}&approval_session_id=AS-999`,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const params = getAppSwitchResumeParams();
|
|
302
|
+
|
|
303
|
+
expect(params).toEqual({
|
|
304
|
+
buttonSessionID,
|
|
305
|
+
checkoutState: "onCancel",
|
|
306
|
+
vaultSetupToken: "AS-999",
|
|
307
|
+
});
|
|
308
|
+
expect(isAppSwitchResumeFlow()).toEqual(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("should return null when web fallback throws error", () => {
|
|
312
|
+
// Mock location.search as a getter that throws an error
|
|
313
|
+
// eslint-disable-next-line compat/compat
|
|
314
|
+
const originalURLSearchParams = window.URLSearchParams;
|
|
315
|
+
// eslint-disable-next-line compat/compat
|
|
316
|
+
window.URLSearchParams = class {
|
|
317
|
+
constructor() {
|
|
318
|
+
throw new Error("Invalid URL");
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
323
|
+
hash: "",
|
|
324
|
+
search: "?invalid",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const params = getAppSwitchResumeParams();
|
|
328
|
+
|
|
329
|
+
expect(params).toEqual(null);
|
|
330
|
+
expect(isAppSwitchResumeFlow()).toEqual(false);
|
|
331
|
+
|
|
332
|
+
// Restore original URLSearchParams
|
|
333
|
+
// eslint-disable-next-line compat/compat
|
|
334
|
+
window.URLSearchParams = originalURLSearchParams;
|
|
335
|
+
});
|
|
143
336
|
});
|