@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paypal/checkout-components",
3
- "version": "5.0.404",
3
+ "version": "5.0.405-alpha-da545e5.0",
4
4
  "description": "PayPal Checkout components, for integrating checkout products.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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 params = Object.fromEntries(
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 test null fetching resume params with invalid callback passed", () => {
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(null);
82
- expect(isAppSwitchResumeFlow()).toEqual(false);
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
  });