@easypayment/medusa-payment-paypal 0.9.17 → 0.9.19

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.
Files changed (44) hide show
  1. package/.medusa/server/src/admin/index.js +326 -130
  2. package/.medusa/server/src/admin/index.mjs +326 -130
  3. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts +4 -1
  4. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts.map +1 -1
  5. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js +434 -113
  6. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js.map +1 -1
  7. package/.medusa/server/src/api/admin/paypal/onboarding-link/route.d.ts.map +1 -1
  8. package/.medusa/server/src/api/admin/paypal/onboarding-link/route.js +4 -0
  9. package/.medusa/server/src/api/admin/paypal/onboarding-link/route.js.map +1 -1
  10. package/.medusa/server/src/api/middlewares.d.ts.map +1 -1
  11. package/.medusa/server/src/api/middlewares.js +9 -0
  12. package/.medusa/server/src/api/middlewares.js.map +1 -1
  13. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
  14. package/.medusa/server/src/api/store/paypal/webhook/route.js +13 -8
  15. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
  16. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
  17. package/.medusa/server/src/modules/paypal/payment-provider/service.js +54 -31
  18. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
  19. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.d.ts +32 -0
  20. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.d.ts.map +1 -0
  21. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.js +65 -0
  22. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.js.map +1 -0
  23. package/.medusa/server/src/modules/paypal/service.d.ts +13 -1
  24. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  25. package/.medusa/server/src/modules/paypal/service.js +19 -4
  26. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  27. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts +2 -1
  28. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts.map +1 -1
  29. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js +82 -26
  30. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js.map +1 -1
  31. package/.medusa/server/src/modules/paypal/utils/webhook-verify.d.ts +28 -0
  32. package/.medusa/server/src/modules/paypal/utils/webhook-verify.d.ts.map +1 -0
  33. package/.medusa/server/src/modules/paypal/utils/webhook-verify.js +58 -0
  34. package/.medusa/server/src/modules/paypal/utils/webhook-verify.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/admin/routes/settings/paypal/connection/page.tsx +471 -127
  37. package/src/api/admin/paypal/onboarding-link/route.ts +6 -0
  38. package/src/api/middlewares.ts +9 -0
  39. package/src/api/store/paypal/webhook/route.ts +22 -8
  40. package/src/modules/paypal/payment-provider/service.ts +73 -29
  41. package/src/modules/paypal/payment-provider/status-utils.ts +59 -0
  42. package/src/modules/paypal/service.ts +24 -5
  43. package/src/modules/paypal/utils/secret-crypto.ts +90 -27
  44. package/src/modules/paypal/utils/webhook-verify.ts +61 -0
@@ -311,6 +311,9 @@ function AdvancedCardPaymentsTab() {
311
311
  function PayPalApplePayPage() {
312
312
  return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: "/settings/paypal/connection", replace: true });
313
313
  }
314
+ function PayPalGooglePayPage() {
315
+ return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: "/settings/paypal/connection", replace: true });
316
+ }
314
317
  const config = adminSdk.defineRouteConfig({
315
318
  label: "PayPal Connection"
316
319
  });
@@ -319,37 +322,97 @@ const PARTNER_JS_URLS = {
319
322
  live: "https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js"
320
323
  };
321
324
  const SERVICE_URL = "/admin/paypal/onboarding-link";
322
- const CACHE_KEY = "pp_onboard_cache";
325
+ const POPUP_NAME = "PPMiniBrowser";
326
+ const CACHE_PREFIX = "pp_onboard_cache";
323
327
  const CACHE_EXPIRY = 10 * 60 * 1e3;
324
328
  const ONBOARDING_COMPLETE_ENDPOINT = "/admin/paypal/onboard-complete";
325
329
  const STATUS_ENDPOINT = "/admin/paypal/status";
326
330
  const SAVE_CREDENTIALS_ENDPOINT = "/admin/paypal/save-credentials";
327
331
  const DISCONNECT_ENDPOINT = "/admin/paypal/disconnect";
328
- let cachedUrl = null;
329
- if (typeof window !== "undefined") {
332
+ const ENVIRONMENT_ENDPOINT = "/admin/paypal/environment";
333
+ const cacheKeyFor = (env) => `${CACHE_PREFIX}_${env}`;
334
+ function readCachedUrl(env) {
335
+ if (typeof window === "undefined") return null;
330
336
  try {
331
- const cached = localStorage.getItem(CACHE_KEY);
332
- if (cached) {
333
- const data = JSON.parse(cached);
334
- if ((/* @__PURE__ */ new Date()).getTime() - data.ts < CACHE_EXPIRY) {
335
- cachedUrl = data.url;
337
+ const raw = localStorage.getItem(cacheKeyFor(env));
338
+ if (!raw) return null;
339
+ const data = JSON.parse(raw);
340
+ if (data && typeof data.url === "string" && Date.now() - (Number(data.ts) || 0) < CACHE_EXPIRY) {
341
+ return data.url;
342
+ }
343
+ } catch {
344
+ }
345
+ return null;
346
+ }
347
+ function writeCachedUrl(env, url) {
348
+ try {
349
+ localStorage.setItem(cacheKeyFor(env), JSON.stringify({ url, ts: Date.now() }));
350
+ } catch {
351
+ }
352
+ }
353
+ function clearCachedUrl(env) {
354
+ try {
355
+ if (env) {
356
+ localStorage.removeItem(cacheKeyFor(env));
357
+ } else {
358
+ localStorage.removeItem(cacheKeyFor("sandbox"));
359
+ localStorage.removeItem(cacheKeyFor("live"));
360
+ }
361
+ } catch {
362
+ }
363
+ }
364
+ function isPayPalOrigin(origin) {
365
+ try {
366
+ const host = new URL(origin).hostname.toLowerCase();
367
+ return host === "www.paypal.com" || host === "www.sandbox.paypal.com" || host.endsWith(".paypal.com") || host.endsWith(".paypalobjects.com");
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+ function extractAuth(data) {
373
+ if (!data) return {};
374
+ if (typeof data === "object") {
375
+ const obj = data;
376
+ const authCode = obj.authCode ?? obj.auth_code ?? obj.authcode;
377
+ const sharedId = obj.sharedId ?? obj.shared_id ?? obj.sharedid;
378
+ if (authCode && sharedId) {
379
+ return { authCode: String(authCode), sharedId: String(sharedId) };
380
+ }
381
+ for (const key of ["data", "payload", "message", "detail", "body"]) {
382
+ if (obj[key] && typeof obj[key] === "object") {
383
+ const inner = extractAuth(obj[key]);
384
+ if (inner.authCode && inner.sharedId) return inner;
385
+ }
386
+ }
387
+ return {};
388
+ }
389
+ if (typeof data === "string") {
390
+ const s = data.trim();
391
+ if (!s) return {};
392
+ if (s.startsWith("{")) {
393
+ try {
394
+ return extractAuth(JSON.parse(s));
395
+ } catch {
396
+ }
397
+ }
398
+ if (/auth_?code/i.test(s) && /shared_?id/i.test(s)) {
399
+ try {
400
+ const sp = new URLSearchParams(s.replace(/^[?#]/, ""));
401
+ const authCode = sp.get("authCode") || sp.get("auth_code") || void 0;
402
+ const sharedId = sp.get("sharedId") || sp.get("shared_id") || void 0;
403
+ if (authCode && sharedId) return { authCode, sharedId };
404
+ } catch {
336
405
  }
337
406
  }
338
- } catch (e) {
339
- console.error("Cache read error:", e);
340
407
  }
408
+ return {};
341
409
  }
342
410
  function PayPalConnectionPage() {
343
411
  const [env, setEnv] = react.useState("live");
344
- react.useEffect(() => {
345
- fetch("/admin/paypal/environment", { method: "GET", credentials: "include" }).then((r) => r.json()).then((d) => {
346
- const v = (d == null ? void 0 : d.environment) === "sandbox" ? "sandbox" : "live";
347
- setEnv(v);
348
- }).catch(() => {
349
- });
350
- }, []);
412
+ const [envReady, setEnvReady] = react.useState(false);
351
413
  const [connState, setConnState] = react.useState("loading");
352
414
  const [error, setError] = react.useState(null);
415
+ const [popupBlocked, setPopupBlocked] = react.useState(false);
353
416
  const [finalUrl, setFinalUrl] = react.useState("");
354
417
  const [showManual, setShowManual] = react.useState(false);
355
418
  const [clientId, setClientId] = react.useState("");
@@ -361,6 +424,17 @@ function PayPalConnectionPage() {
361
424
  const errorLogRef = react.useRef(null);
362
425
  const runIdRef = react.useRef(0);
363
426
  const currentRunId = react.useRef(0);
427
+ const finalUrlRef = react.useRef(finalUrl);
428
+ finalUrlRef.current = finalUrl;
429
+ const connStateRef = react.useRef(connState);
430
+ connStateRef.current = connState;
431
+ const inProgressRef = react.useRef(onboardingInProgress);
432
+ inProgressRef.current = onboardingInProgress;
433
+ const envRef = react.useRef(env);
434
+ envRef.current = env;
435
+ const popupRef = react.useRef(null);
436
+ const popupPollRef = react.useRef(null);
437
+ const completedRef = react.useRef(false);
364
438
  const ppBtnMeasureRef = react.useRef(null);
365
439
  const [ppBtnWidth, setPpBtnWidth] = react.useState(null);
366
440
  const canSaveManual = react.useMemo(() => {
@@ -370,8 +444,124 @@ function PayPalConnectionPage() {
370
444
  setConnState("error");
371
445
  setError(msg);
372
446
  }, []);
447
+ const stopPopupPoll = react.useCallback(() => {
448
+ if (popupPollRef.current) {
449
+ clearInterval(popupPollRef.current);
450
+ popupPollRef.current = null;
451
+ }
452
+ }, []);
453
+ const openOnboardingPopup = react.useCallback(
454
+ (url) => {
455
+ const w = 450;
456
+ const h = 600;
457
+ const baseLeft = typeof window.screenX === "number" ? window.screenX : window.screenLeft || 0;
458
+ const baseTop = typeof window.screenY === "number" ? window.screenY : window.screenTop || 0;
459
+ const outerW = window.outerWidth || document.documentElement.clientWidth || 1024;
460
+ const outerH = window.outerHeight || document.documentElement.clientHeight || 768;
461
+ const left = Math.round(baseLeft + Math.max(0, (outerW - w) / 2));
462
+ const top = Math.round(baseTop + Math.max(0, (outerH - h) / 2));
463
+ const features = `popup=yes,width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`;
464
+ let popup = null;
465
+ try {
466
+ popup = window.open(url, POPUP_NAME, features);
467
+ } catch {
468
+ popup = null;
469
+ }
470
+ if (popup) {
471
+ popupRef.current = popup;
472
+ try {
473
+ popup.focus();
474
+ } catch {
475
+ }
476
+ stopPopupPoll();
477
+ popupPollRef.current = setInterval(() => {
478
+ if (!popupRef.current || popupRef.current.closed) {
479
+ stopPopupPoll();
480
+ }
481
+ }, 1e3);
482
+ }
483
+ return popup;
484
+ },
485
+ [stopPopupPoll]
486
+ );
487
+ const openConnect = react.useCallback(() => {
488
+ if (connStateRef.current !== "ready" || !finalUrlRef.current || inProgressRef.current) {
489
+ return;
490
+ }
491
+ completedRef.current = false;
492
+ const popup = openOnboardingPopup(finalUrlRef.current);
493
+ if (!popup) {
494
+ setPopupBlocked(true);
495
+ return;
496
+ }
497
+ setPopupBlocked(false);
498
+ }, [openOnboardingPopup]);
499
+ const completeOnboarding = react.useCallback(
500
+ async (authCode, sharedId) => {
501
+ var _a, _b, _c, _d, _e, _f, _g, _h;
502
+ if (!authCode || !sharedId) return;
503
+ if (completedRef.current) return;
504
+ completedRef.current = true;
505
+ try {
506
+ window.onbeforeunload = null;
507
+ } catch {
508
+ }
509
+ const activeEnv = envRef.current === "sandbox" ? "sandbox" : "live";
510
+ setOnboardingInProgress(true);
511
+ setConnState("loading");
512
+ setError(null);
513
+ setPopupBlocked(false);
514
+ try {
515
+ const res = await fetch(ONBOARDING_COMPLETE_ENDPOINT, {
516
+ method: "POST",
517
+ headers: { "content-type": "application/json" },
518
+ credentials: "include",
519
+ body: JSON.stringify({ authCode, sharedId, env: activeEnv })
520
+ });
521
+ if (!res.ok) {
522
+ const txt = await res.text().catch(() => "");
523
+ throw new Error(txt || `Onboarding exchange failed (${res.status})`);
524
+ }
525
+ try {
526
+ (_a = popupRef.current) == null ? void 0 : _a.close();
527
+ } catch {
528
+ }
529
+ stopPopupPoll();
530
+ try {
531
+ const close1 = (_e = (_d = (_c = (_b = window.PAYPAL) == null ? void 0 : _b.apps) == null ? void 0 : _c.Signup) == null ? void 0 : _d.MiniBrowser) == null ? void 0 : _e.closeFlow;
532
+ if (typeof close1 === "function") close1();
533
+ } catch {
534
+ }
535
+ try {
536
+ const close2 = ((_h = (_g = (_f = window.PAYPAL) == null ? void 0 : _f.apps) == null ? void 0 : _g.Signup) == null ? void 0 : _h.miniBrowser) && window.PAYPAL.apps.Signup.miniBrowser.closeFlow;
537
+ if (typeof close2 === "function") close2();
538
+ } catch {
539
+ }
540
+ clearCachedUrl(activeEnv);
541
+ try {
542
+ const statusRes = await fetch(
543
+ `${STATUS_ENDPOINT}?environment=${activeEnv}`,
544
+ { method: "GET", credentials: "include" }
545
+ );
546
+ const refreshedStatus = await statusRes.json().catch(() => ({}));
547
+ setStatusInfo(refreshedStatus || null);
548
+ } catch {
549
+ }
550
+ setConnState("connected");
551
+ setShowManual(false);
552
+ } catch (e) {
553
+ completedRef.current = false;
554
+ console.error(e);
555
+ setConnState("error");
556
+ setError((e == null ? void 0 : e.message) || "Exchange failed while saving credentials.");
557
+ } finally {
558
+ setOnboardingInProgress(false);
559
+ }
560
+ },
561
+ [stopPopupPoll]
562
+ );
373
563
  const fetchFreshLink = react.useCallback(
374
- (runId) => {
564
+ (runId, targetEnv) => {
375
565
  if (initLoaderRef.current) {
376
566
  const loaderText = initLoaderRef.current.querySelector("#loader-text");
377
567
  if (loaderText)
@@ -382,7 +572,10 @@ function PayPalConnectionPage() {
382
572
  headers: { "content-type": "application/json" },
383
573
  credentials: "include",
384
574
  body: JSON.stringify({
385
- products: ["PPCP"]
575
+ products: ["PPCP"],
576
+ // Generate the link for the explicitly selected environment so it can
577
+ // never depend on a racing POST /environment having landed yet.
578
+ environment: targetEnv
386
579
  })
387
580
  }).then((r) => {
388
581
  if (!r.ok) throw new Error(`Service returned ${r.status}`);
@@ -395,10 +588,7 @@ function PayPalConnectionPage() {
395
588
  return;
396
589
  }
397
590
  const url = href + (href.includes("?") ? "&" : "?") + "displayMode=minibrowser";
398
- localStorage.setItem(
399
- CACHE_KEY,
400
- JSON.stringify({ url, ts: Date.now() })
401
- );
591
+ writeCachedUrl(targetEnv, url);
402
592
  setFinalUrl(url);
403
593
  setConnState("ready");
404
594
  }).catch(() => {
@@ -406,37 +596,104 @@ function PayPalConnectionPage() {
406
596
  showError("Unable to connect to service.");
407
597
  });
408
598
  },
409
- [env, showError]
599
+ [showError]
410
600
  );
601
+ react.useEffect(() => {
602
+ let alive = true;
603
+ fetch(ENVIRONMENT_ENDPOINT, { method: "GET", credentials: "include" }).then((r) => r.json()).then((d) => {
604
+ if (!alive) return;
605
+ setEnv((d == null ? void 0 : d.environment) === "sandbox" ? "sandbox" : "live");
606
+ }).catch(() => {
607
+ }).finally(() => {
608
+ if (alive) setEnvReady(true);
609
+ });
610
+ return () => {
611
+ alive = false;
612
+ };
613
+ }, []);
411
614
  react.useEffect(() => {
412
615
  var _a, _b, _c;
413
616
  if (connState !== "ready" || !finalUrl) return;
414
617
  const scriptUrl = PARTNER_JS_URLS[env];
618
+ let cancelled = false;
619
+ let pollTimer;
620
+ const initPartner = (attempt = 0) => {
621
+ var _a2, _b2;
622
+ if (cancelled) return;
623
+ const signup = (_b2 = (_a2 = window.PAYPAL) == null ? void 0 : _a2.apps) == null ? void 0 : _b2.Signup;
624
+ const target = (signup == null ? void 0 : signup.miniBrowser) && typeof signup.miniBrowser.init === "function" ? signup.miniBrowser : (signup == null ? void 0 : signup.MiniBrowser) && typeof signup.MiniBrowser.init === "function" ? signup.MiniBrowser : null;
625
+ if (target == null ? void 0 : target.init) {
626
+ try {
627
+ target.init();
628
+ } catch (e) {
629
+ console.error("[paypal] partner.js init failed:", e);
630
+ }
631
+ return;
632
+ }
633
+ if (typeof (signup == null ? void 0 : signup.render) === "function") {
634
+ try {
635
+ signup.render();
636
+ } catch (e) {
637
+ console.error("[paypal] partner.js render failed:", e);
638
+ }
639
+ return;
640
+ }
641
+ if (attempt < 40) {
642
+ pollTimer = setTimeout(() => initPartner(attempt + 1), 50);
643
+ return;
644
+ }
645
+ try {
646
+ document.dispatchEvent(new Event("DOMContentLoaded"));
647
+ } catch {
648
+ }
649
+ };
415
650
  const existingScript = document.getElementById("paypal-partner-js");
416
651
  if (existingScript) {
417
652
  (_a = existingScript.parentNode) == null ? void 0 : _a.removeChild(existingScript);
418
653
  }
419
654
  if ((_c = (_b = window.PAYPAL) == null ? void 0 : _b.apps) == null ? void 0 : _c.Signup) {
420
- delete window.PAYPAL.apps.Signup;
655
+ try {
656
+ delete window.PAYPAL.apps.Signup;
657
+ } catch {
658
+ }
421
659
  }
422
660
  const ppScript = document.createElement("script");
423
661
  ppScript.id = "paypal-partner-js";
424
662
  ppScript.src = scriptUrl;
425
663
  ppScript.async = true;
664
+ ppScript.onload = () => initPartner();
426
665
  document.body.appendChild(ppScript);
427
666
  return () => {
667
+ cancelled = true;
668
+ if (pollTimer) {
669
+ clearTimeout(pollTimer);
670
+ }
428
671
  if (ppScript.parentNode) {
429
672
  ppScript.parentNode.removeChild(ppScript);
430
673
  }
431
674
  };
432
675
  }, [connState, finalUrl, env]);
433
676
  react.useEffect(() => {
677
+ const onMessage = (ev) => {
678
+ if (!isPayPalOrigin(ev.origin)) return;
679
+ const { authCode, sharedId } = extractAuth(ev.data);
680
+ if (authCode && sharedId) {
681
+ completeOnboarding(authCode, sharedId);
682
+ }
683
+ };
684
+ window.addEventListener("message", onMessage);
685
+ return () => window.removeEventListener("message", onMessage);
686
+ }, [completeOnboarding]);
687
+ react.useEffect(() => {
688
+ if (!envReady) return;
434
689
  currentRunId.current = ++runIdRef.current;
435
690
  const runId = currentRunId.current;
436
691
  let cancelled = false;
692
+ completedRef.current = false;
437
693
  const run = async () => {
438
694
  setConnState("loading");
439
695
  setError(null);
696
+ setPopupBlocked(false);
440
697
  setFinalUrl("");
441
698
  try {
442
699
  const r = await fetch(`${STATUS_ENDPOINT}?environment=${env}`, {
@@ -455,84 +712,46 @@ function PayPalConnectionPage() {
455
712
  } catch (e) {
456
713
  console.error(e);
457
714
  }
458
- if (cachedUrl) {
459
- setFinalUrl(cachedUrl);
715
+ if (cancelled || runId !== currentRunId.current) return;
716
+ const cached = readCachedUrl(env);
717
+ if (cached) {
718
+ setFinalUrl(cached);
460
719
  setConnState("ready");
461
720
  } else {
462
- fetchFreshLink(runId);
721
+ fetchFreshLink(runId, env);
463
722
  }
464
723
  };
465
724
  run();
466
725
  return () => {
467
726
  cancelled = true;
468
- currentRunId.current = 0;
469
727
  };
470
- }, [env, fetchFreshLink]);
728
+ }, [env, envReady, fetchFreshLink]);
471
729
  react.useLayoutEffect(() => {
472
- window.onboardingCallback = async function(authCode, sharedId) {
473
- var _a, _b, _c, _d, _e, _f, _g;
474
- try {
475
- window.onbeforeunload = null;
476
- } catch {
477
- }
478
- setOnboardingInProgress(true);
479
- setConnState("loading");
480
- setError(null);
481
- const payload = {
482
- authCode,
483
- sharedId,
484
- env: env === "sandbox" ? "sandbox" : "live"
485
- };
486
- try {
487
- const res = await fetch(ONBOARDING_COMPLETE_ENDPOINT, {
488
- method: "POST",
489
- headers: { "content-type": "application/json" },
490
- credentials: "include",
491
- body: JSON.stringify(payload)
492
- });
493
- if (!res.ok) {
494
- const txt = await res.text().catch(() => "");
495
- throw new Error(txt || `Onboarding exchange failed (${res.status})`);
496
- }
497
- try {
498
- const close1 = (_d = (_c = (_b = (_a = window.PAYPAL) == null ? void 0 : _a.apps) == null ? void 0 : _b.Signup) == null ? void 0 : _c.MiniBrowser) == null ? void 0 : _d.closeFlow;
499
- if (typeof close1 === "function") close1();
500
- } catch {
501
- }
502
- try {
503
- const close2 = ((_g = (_f = (_e = window.PAYPAL) == null ? void 0 : _e.apps) == null ? void 0 : _f.Signup) == null ? void 0 : _g.miniBrowser) && window.PAYPAL.apps.Signup.miniBrowser.closeFlow;
504
- if (typeof close2 === "function") close2();
505
- } catch {
506
- }
507
- try {
508
- localStorage.removeItem(CACHE_KEY);
509
- } catch {
510
- }
511
- try {
512
- const statusRes = await fetch(`${STATUS_ENDPOINT}?environment=${env}`, {
513
- method: "GET",
514
- credentials: "include"
515
- });
516
- const refreshedStatus = await statusRes.json().catch(() => ({}));
517
- setStatusInfo(refreshedStatus || null);
518
- setConnState("connected");
519
- setShowManual(false);
520
- } catch {
521
- setConnState("connected");
522
- setShowManual(false);
523
- }
524
- setOnboardingInProgress(false);
525
- } catch (e) {
526
- console.error(e);
527
- setConnState("error");
528
- setError((e == null ? void 0 : e.message) || "Exchange failed while saving credentials.");
529
- setOnboardingInProgress(false);
530
- }
730
+ window.onboardingCallback = (authCode, sharedId) => {
731
+ completeOnboarding(authCode, sharedId);
531
732
  };
532
733
  return () => {
533
734
  window.onboardingCallback = void 0;
534
735
  };
535
- }, [env]);
736
+ }, [completeOnboarding]);
737
+ react.useEffect(() => {
738
+ return () => {
739
+ stopPopupPoll();
740
+ };
741
+ }, [stopPopupPoll]);
742
+ react.useEffect(() => {
743
+ const onCaptureClick = (e) => {
744
+ const btn = paypalButtonRef.current;
745
+ if (!btn) return;
746
+ const target = e.target;
747
+ if (!target || !btn.contains(target)) return;
748
+ e.preventDefault();
749
+ e.stopImmediatePropagation();
750
+ openConnect();
751
+ };
752
+ document.addEventListener("click", onCaptureClick, true);
753
+ return () => document.removeEventListener("click", onCaptureClick, true);
754
+ }, [openConnect]);
536
755
  react.useLayoutEffect(() => {
537
756
  const el = ppBtnMeasureRef.current;
538
757
  if (!el) return;
@@ -554,9 +773,8 @@ function PayPalConnectionPage() {
554
773
  };
555
774
  }, [connState, env, finalUrl]);
556
775
  const handleConnectClick = (e) => {
557
- if (connState !== "ready" || !finalUrl || onboardingInProgress) {
558
- e.preventDefault();
559
- }
776
+ e.preventDefault();
777
+ openConnect();
560
778
  };
561
779
  const handleSaveManual = async () => {
562
780
  if (!canSaveManual || onboardingInProgress) return;
@@ -586,10 +804,7 @@ function PayPalConnectionPage() {
586
804
  setConnState("connected");
587
805
  setStatusInfo(refreshedStatus || null);
588
806
  setShowManual(false);
589
- try {
590
- localStorage.removeItem(CACHE_KEY);
591
- } catch {
592
- }
807
+ clearCachedUrl(env);
593
808
  } catch (e) {
594
809
  console.error(e);
595
810
  setConnState("error");
@@ -618,13 +833,11 @@ function PayPalConnectionPage() {
618
833
  const t = await res.text().catch(() => "");
619
834
  throw new Error(t || `Disconnect failed (${res.status})`);
620
835
  }
621
- try {
622
- localStorage.removeItem(CACHE_KEY);
623
- } catch {
624
- }
836
+ clearCachedUrl(env);
837
+ completedRef.current = false;
625
838
  currentRunId.current = ++runIdRef.current;
626
839
  const runId = currentRunId.current;
627
- fetchFreshLink(runId);
840
+ fetchFreshLink(runId, env);
628
841
  } catch (e) {
629
842
  console.error(e);
630
843
  setConnState("error");
@@ -635,10 +848,12 @@ function PayPalConnectionPage() {
635
848
  };
636
849
  const handleEnvChange = async (e) => {
637
850
  const next = e.target.value;
851
+ completedRef.current = false;
852
+ setPopupBlocked(false);
853
+ clearCachedUrl();
638
854
  setEnv(next);
639
- cachedUrl = null;
640
855
  try {
641
- await fetch("/admin/paypal/environment", {
856
+ await fetch(ENVIRONMENT_ENDPOINT, {
642
857
  method: "POST",
643
858
  headers: { "content-type": "application/json" },
644
859
  credentials: "include",
@@ -646,10 +861,6 @@ function PayPalConnectionPage() {
646
861
  });
647
862
  } catch {
648
863
  }
649
- try {
650
- localStorage.removeItem(CACHE_KEY);
651
- } catch {
652
- }
653
864
  };
654
865
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6", children: [
655
866
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-6", children: [
@@ -672,20 +883,7 @@ function PayPalConnectionPage() {
672
883
  ) }),
673
884
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium pt-2", children: env === "sandbox" ? "Connect to PayPal (Sandbox)" : "Connect to PayPal" }),
674
885
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-xl", children: connState === "connected" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
675
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-green-600 bg-green-50 p-3 rounded border border-green-200", children: [
676
- "✅ Successfully connected to PayPal!",
677
- /* @__PURE__ */ jsxRuntime.jsx(
678
- "a",
679
- {
680
- target: "_blank",
681
- "data-paypal-button": "true",
682
- "data-paypal-onboard-complete": "onboardingCallback",
683
- href: "#",
684
- style: { display: "none" },
685
- children: "PayPal"
686
- }
687
- )
688
- ] }),
886
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-green-600 bg-green-50 p-3 rounded border border-green-200", children: "✅ Successfully connected to PayPal!" }),
689
887
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 rounded-md border border-ui-border-base bg-ui-bg-subtle p-3 text-xs text-ui-fg-subtle", children: [
690
888
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-ui-fg-base", children: "Connected PayPal account" }),
691
889
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1", children: [
@@ -726,7 +924,7 @@ function PayPalConnectionPage() {
726
924
  ppBtnMeasureRef.current = node;
727
925
  },
728
926
  id: "paypal-button",
729
- target: "_blank",
927
+ target: POPUP_NAME,
730
928
  "data-paypal-button": "true",
731
929
  href: finalUrl || "#",
732
930
  "data-paypal-onboard-complete": "onboardingCallback",
@@ -740,6 +938,7 @@ function PayPalConnectionPage() {
740
938
  children: "Connect to PayPal"
741
939
  }
742
940
  ),
941
+ popupBlocked && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 text-left text-xs bg-amber-50 text-amber-700 p-3 border border-amber-200 rounded", children: "Your browser blocked the PayPal popup. Please allow popups for this site, then click “Connect to PayPal” again." }),
743
942
  /* @__PURE__ */ jsxRuntime.jsx(
744
943
  "div",
745
944
  {
@@ -895,9 +1094,6 @@ function PayPalConnectionPage() {
895
1094
  ` })
896
1095
  ] });
897
1096
  }
898
- function PayPalGooglePayPage() {
899
- return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: "/settings/paypal/connection", replace: true });
900
- }
901
1097
  function PayPalPayLaterMessagingPage() {
902
1098
  return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: "/settings/paypal/connection", replace: true });
903
1099
  }
@@ -1127,14 +1323,14 @@ const routeModule = {
1127
1323
  Component: PayPalApplePayPage,
1128
1324
  path: "/settings/paypal/apple-pay"
1129
1325
  },
1130
- {
1131
- Component: PayPalConnectionPage,
1132
- path: "/settings/paypal/connection"
1133
- },
1134
1326
  {
1135
1327
  Component: PayPalGooglePayPage,
1136
1328
  path: "/settings/paypal/google-pay"
1137
1329
  },
1330
+ {
1331
+ Component: PayPalConnectionPage,
1332
+ path: "/settings/paypal/connection"
1333
+ },
1138
1334
  {
1139
1335
  Component: PayPalPayLaterMessagingPage,
1140
1336
  path: "/settings/paypal/pay-later-messaging"