@easypayment/medusa-payment-paypal 0.9.24 → 0.9.26

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 (22) hide show
  1. package/.medusa/server/src/admin/index.js +187 -287
  2. package/.medusa/server/src/admin/index.mjs +187 -287
  3. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts +0 -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 +204 -332
  6. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js.map +1 -1
  7. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.d.ts +7 -0
  8. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.d.ts.map +1 -1
  9. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.js +7 -0
  10. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.js.map +1 -1
  11. package/.medusa/server/src/api/store/paypal/onboard-return/route.d.ts +25 -0
  12. package/.medusa/server/src/api/store/paypal/onboard-return/route.d.ts.map +1 -0
  13. package/.medusa/server/src/api/store/paypal/onboard-return/route.js +82 -0
  14. package/.medusa/server/src/api/store/paypal/onboard-return/route.js.map +1 -0
  15. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  16. package/.medusa/server/src/modules/paypal/service.js +32 -2
  17. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/admin/routes/settings/paypal/connection/page.tsx +230 -354
  20. package/src/api/admin/paypal/onboard-complete/route.ts +7 -0
  21. package/src/api/store/paypal/onboard-return/route.ts +88 -0
  22. package/src/modules/paypal/service.ts +34 -3
@@ -12,15 +12,16 @@ const Tabs_1 = __importDefault(require("../_components/Tabs"));
12
12
  exports.config = (0, admin_sdk_1.defineRouteConfig)({
13
13
  label: "PayPal Connection",
14
14
  });
15
- const PARTNER_JS_URLS = {
16
- sandbox: "https://www.sandbox.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js",
17
- live: "https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js",
18
- };
15
+ // Let partner.js own the entire onboarding flow: it opens PayPal's mini-browser
16
+ // itself (synchronously inside the click, so no new tab / no popup block), it
17
+ // receives PayPal's completion postMessage, and it invokes the callback named in
18
+ // `data-paypal-onboard-complete` with (authCode, sharedId). This is PayPal's
19
+ // documented integration and matches the official @easypayment WooCommerce
20
+ // plugin. Opening the popup ourselves (a previous attempt at fixing the "opens a
21
+ // new tab" issue) stopped partner.js from receiving completion, which left the
22
+ // popup stranded on PayPal's "isuDone" page and never saved credentials.
23
+ const PARTNER_JS_URL = "https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js";
19
24
  const SERVICE_URL = "/admin/paypal/onboarding-link";
20
- // PayPal's conventional mini-browser window name (also the anchor target).
21
- const POPUP_NAME = "PPFrame";
22
- // The onboarding link is cached in the browser for 6 hours so it is always
23
- // present for the mini-browser flow without regenerating on every load.
24
25
  const CACHE_PREFIX = "pp_onboard_cache";
25
26
  const CACHE_EXPIRY = 6 * 60 * 60 * 1000; // 6 hours
26
27
  const ONBOARDING_COMPLETE_ENDPOINT = "/admin/paypal/onboard-complete";
@@ -51,10 +52,9 @@ function readCachedUrl(env) {
51
52
  function writeCachedUrl(env, url) {
52
53
  try {
53
54
  localStorage.setItem(cacheKeyFor(env), JSON.stringify({ url, ts: Date.now() }));
54
- return readCachedUrl(env) === url;
55
55
  }
56
56
  catch {
57
- return false;
57
+ /* ignore */
58
58
  }
59
59
  }
60
60
  function clearCachedUrl(env) {
@@ -71,79 +71,11 @@ function clearCachedUrl(env) {
71
71
  /* ignore */
72
72
  }
73
73
  }
74
- function isPayPalOrigin(origin) {
75
- try {
76
- const host = new URL(origin).hostname.toLowerCase();
77
- return (host === "www.paypal.com" ||
78
- host === "www.sandbox.paypal.com" ||
79
- host.endsWith(".paypal.com") ||
80
- host.endsWith(".paypalobjects.com"));
81
- }
82
- catch {
83
- return false;
84
- }
85
- }
86
- // Defensively pull authCode/sharedId out of whatever shape PayPal posts back
87
- // (object, JSON string, or query string; possibly nested). We only act when both
88
- // are present, which ignores the many unrelated intermediate messages PayPal
89
- // emits.
90
- function extractAuth(data) {
91
- if (!data)
92
- return {};
93
- if (typeof data === "object") {
94
- const obj = data;
95
- const authCode = obj.authCode ?? obj.auth_code ?? obj.authcode ?? obj.code;
96
- const sharedId = obj.sharedId ?? obj.shared_id ?? obj.sharedid;
97
- if (authCode && sharedId) {
98
- return { authCode: String(authCode), sharedId: String(sharedId) };
99
- }
100
- for (const key of ["data", "payload", "message", "detail", "body", "params"]) {
101
- if (obj[key] && typeof obj[key] === "object") {
102
- const inner = extractAuth(obj[key]);
103
- if (inner.authCode && inner.sharedId)
104
- return inner;
105
- }
106
- if (typeof obj[key] === "string") {
107
- const inner = extractAuth(obj[key]);
108
- if (inner.authCode && inner.sharedId)
109
- return inner;
110
- }
111
- }
112
- return {};
113
- }
114
- if (typeof data === "string") {
115
- const s = data.trim();
116
- if (!s)
117
- return {};
118
- if (s.startsWith("{") || s.startsWith("[")) {
119
- try {
120
- return extractAuth(JSON.parse(s));
121
- }
122
- catch {
123
- /* not JSON */
124
- }
125
- }
126
- if (/auth_?code/i.test(s) && /shared_?id/i.test(s)) {
127
- try {
128
- const sp = new URLSearchParams(s.replace(/^[?#]/, ""));
129
- const authCode = sp.get("authCode") || sp.get("auth_code") || undefined;
130
- const sharedId = sp.get("sharedId") || sp.get("shared_id") || undefined;
131
- if (authCode && sharedId)
132
- return { authCode, sharedId };
133
- }
134
- catch {
135
- /* not a query string */
136
- }
137
- }
138
- }
139
- return {};
140
- }
141
74
  function PayPalConnectionPage() {
142
75
  const [env, setEnv] = (0, react_1.useState)("live");
143
76
  const [envReady, setEnvReady] = (0, react_1.useState)(false);
144
77
  const [connState, setConnState] = (0, react_1.useState)("loading");
145
78
  const [error, setError] = (0, react_1.useState)(null);
146
- const [popupBlocked, setPopupBlocked] = (0, react_1.useState)(false);
147
79
  const [finalUrl, setFinalUrl] = (0, react_1.useState)("");
148
80
  const [showManual, setShowManual] = (0, react_1.useState)(false);
149
81
  const [clientId, setClientId] = (0, react_1.useState)("");
@@ -155,17 +87,13 @@ function PayPalConnectionPage() {
155
87
  const errorLogRef = (0, react_1.useRef)(null);
156
88
  const runIdRef = (0, react_1.useRef)(0);
157
89
  const currentRunId = (0, react_1.useRef)(0);
90
+ const completedRef = (0, react_1.useRef)(false);
91
+ // Long-lived listeners/timers read the current env through a ref so they never
92
+ // capture a stale value from the render that registered them.
158
93
  const envRef = (0, react_1.useRef)(env);
159
94
  envRef.current = env;
160
- const finalUrlRef = (0, react_1.useRef)(finalUrl);
161
- finalUrlRef.current = finalUrl;
162
- const connStateRef = (0, react_1.useRef)(connState);
163
- connStateRef.current = connState;
164
- const inProgressRef = (0, react_1.useRef)(onboardingInProgress);
165
- inProgressRef.current = onboardingInProgress;
166
- const popupRef = (0, react_1.useRef)(null);
167
- const pollRef = (0, react_1.useRef)(null);
168
- const completedRef = (0, react_1.useRef)(false);
95
+ const pollTimerRef = (0, react_1.useRef)(null);
96
+ const pollAttemptsRef = (0, react_1.useRef)(0);
169
97
  const ppBtnMeasureRef = (0, react_1.useRef)(null);
170
98
  const [ppBtnWidth, setPpBtnWidth] = (0, react_1.useState)(null);
171
99
  const canSaveManual = (0, react_1.useMemo)(() => {
@@ -175,54 +103,87 @@ function PayPalConnectionPage() {
175
103
  setConnState("error");
176
104
  setError(msg);
177
105
  }, []);
178
- const stopPoll = (0, react_1.useCallback)(() => {
179
- if (pollRef.current) {
180
- clearInterval(pollRef.current);
181
- pollRef.current = null;
106
+ // Close PayPal's mini-browser via partner.js's API (works under both the
107
+ // `miniBrowser` and `MiniBrowser` casings PayPal has shipped over time).
108
+ const closeMiniBrowser = (0, react_1.useCallback)(() => {
109
+ try {
110
+ const close1 = window.PAYPAL?.apps?.Signup?.MiniBrowser?.closeFlow;
111
+ if (typeof close1 === "function")
112
+ close1();
113
+ }
114
+ catch { }
115
+ try {
116
+ const close2 = window.PAYPAL?.apps?.Signup?.miniBrowser?.closeFlow;
117
+ if (typeof close2 === "function")
118
+ close2();
119
+ }
120
+ catch { }
121
+ }, []);
122
+ const stopStatusPolling = (0, react_1.useCallback)(() => {
123
+ if (pollTimerRef.current) {
124
+ clearTimeout(pollTimerRef.current);
125
+ pollTimerRef.current = null;
182
126
  }
127
+ pollAttemptsRef.current = 0;
183
128
  }, []);
184
- // Open PayPal's mini-browser ourselves, synchronously inside the click gesture.
185
- // This is the only reliable way to get a popup (rather than a new tab, which is
186
- // what happens when partner.js's asynchronous open misses the user-activation
187
- // window). Named POPUP_NAME and sized like PayPal's mini-browser.
188
- const openOnboardingPopup = (0, react_1.useCallback)((url) => {
189
- const w = 450;
190
- const h = 600;
191
- const baseLeft = typeof window.screenX === "number" ? window.screenX : window.screenLeft || 0;
192
- const baseTop = typeof window.screenY === "number" ? window.screenY : window.screenTop || 0;
193
- const outerW = window.outerWidth || document.documentElement.clientWidth || 1024;
194
- const outerH = window.outerHeight || document.documentElement.clientHeight || 768;
195
- const left = Math.round(baseLeft + Math.max(0, (outerW - w) / 2));
196
- const top = Math.round(baseTop + Math.max(0, (outerH - h) / 2));
197
- const features = `popup=yes,width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`;
198
- let popup = null;
129
+ // Fetch status for an env and, if the seller credentials are present, flip the
130
+ // UI to "connected" and close the PayPal popup. Returns true once connected.
131
+ const refreshStatusAndMaybeConnect = (0, react_1.useCallback)(async (activeEnv) => {
199
132
  try {
200
- popup = window.open(url, POPUP_NAME, features);
133
+ const res = await fetch(`${STATUS_ENDPOINT}?environment=${activeEnv}`, {
134
+ method: "GET",
135
+ credentials: "include",
136
+ });
137
+ const st = await res.json().catch(() => ({}));
138
+ const connected = st?.status === "connected" && st?.seller_client_id_present === true;
139
+ if (connected) {
140
+ completedRef.current = true;
141
+ setStatusInfo(st);
142
+ setConnState("connected");
143
+ setShowManual(false);
144
+ setOnboardingInProgress(false);
145
+ clearCachedUrl(activeEnv);
146
+ closeMiniBrowser();
147
+ stopStatusPolling();
148
+ }
149
+ return connected;
201
150
  }
202
151
  catch {
203
- popup = null;
152
+ return false;
204
153
  }
205
- if (popup) {
206
- popupRef.current = popup;
207
- try {
208
- popup.focus();
209
- }
210
- catch {
211
- /* ignore */
154
+ }, [closeMiniBrowser, stopStatusPolling]);
155
+ // While a connect attempt is in flight, the seller may complete onboarding via
156
+ // a path that never reaches the opener (e.g. partner.js fails to fire its
157
+ // callback, or the popup's completion message is blocked). The return_url
158
+ // bridge still saves credentials server-side, so poll status as the reliable
159
+ // fallback: the moment credentials land, this flips to "connected" and closes
160
+ // the stranded popup.
161
+ const startStatusPolling = (0, react_1.useCallback)(() => {
162
+ stopStatusPolling();
163
+ const MAX_ATTEMPTS = 100; // ~5 min at 3s intervals
164
+ const tick = async () => {
165
+ pollAttemptsRef.current += 1;
166
+ const connected = await refreshStatusAndMaybeConnect(envRef.current);
167
+ if (connected || completedRef.current)
168
+ return;
169
+ if (pollAttemptsRef.current >= MAX_ATTEMPTS) {
170
+ stopStatusPolling();
171
+ return;
212
172
  }
213
- }
214
- return popup;
215
- }, []);
216
- // Exchange authCode/sharedId for seller credentials. Idempotent so the multiple
217
- // completion signals (our message listener, partner.js's bridge, the status
218
- // poll) only run it once.
173
+ pollTimerRef.current = setTimeout(tick, 3000);
174
+ };
175
+ pollTimerRef.current = setTimeout(tick, 3000);
176
+ }, [refreshStatusAndMaybeConnect, stopStatusPolling]);
177
+ // Shared completion: exchange authCode/sharedId for seller credentials, then
178
+ // close the popup and refresh status. Invoked by partner.js's onboardingCallback
179
+ // and by the return_url bridge's postMessage. Guarded + server-idempotent so
180
+ // redundant invocations are harmless.
219
181
  const completeOnboarding = (0, react_1.useCallback)(async (authCode, sharedId) => {
220
182
  if (!authCode || !sharedId)
221
183
  return;
222
184
  if (completedRef.current)
223
185
  return;
224
186
  completedRef.current = true;
225
- stopPoll();
226
187
  try {
227
188
  window.onbeforeunload = null;
228
189
  }
@@ -231,7 +192,6 @@ function PayPalConnectionPage() {
231
192
  setOnboardingInProgress(true);
232
193
  setConnState("loading");
233
194
  setError(null);
234
- setPopupBlocked(false);
235
195
  try {
236
196
  const res = await fetch(ONBOARDING_COMPLETE_ENDPOINT, {
237
197
  method: "POST",
@@ -243,84 +203,99 @@ function PayPalConnectionPage() {
243
203
  const txt = await res.text().catch(() => "");
244
204
  throw new Error(txt || `Onboarding exchange failed (${res.status})`);
245
205
  }
246
- try {
247
- popupRef.current?.close();
248
- }
249
- catch { }
250
- try {
251
- const close1 = window.PAYPAL?.apps?.Signup?.MiniBrowser?.closeFlow;
252
- if (typeof close1 === "function")
253
- close1();
254
- }
255
- catch { }
256
- try {
257
- const close2 = window.PAYPAL?.apps?.Signup?.miniBrowser &&
258
- window.PAYPAL.apps.Signup.miniBrowser.closeFlow;
259
- if (typeof close2 === "function")
260
- close2();
261
- }
262
- catch { }
206
+ closeMiniBrowser();
263
207
  clearCachedUrl(activeEnv);
264
208
  try {
265
- const statusRes = await fetch(`${STATUS_ENDPOINT}?environment=${activeEnv}`, {
266
- method: "GET",
267
- credentials: "include",
268
- });
209
+ const statusRes = await fetch(`${STATUS_ENDPOINT}?environment=${activeEnv}`, { method: "GET", credentials: "include" });
269
210
  const refreshedStatus = await statusRes.json().catch(() => ({}));
270
211
  setStatusInfo(refreshedStatus || null);
271
212
  }
272
213
  catch { }
273
214
  setConnState("connected");
274
215
  setShowManual(false);
216
+ stopStatusPolling();
275
217
  }
276
218
  catch (e) {
277
219
  completedRef.current = false;
278
220
  console.error(e);
221
+ // The bridge may have already saved credentials server-side; if so the
222
+ // poll will surface "connected". Don't strand the user on an error.
279
223
  setConnState("error");
280
224
  setError(e?.message || "Exchange failed while saving credentials.");
281
225
  }
282
226
  finally {
283
227
  setOnboardingInProgress(false);
284
228
  }
285
- }, [stopPoll]);
286
- // After the popup is opened, poll the server. If the credentials get saved by
287
- // any completion path, the status flips to "connected" and we sync the UI even
288
- // if a message was missed.
289
- const startStatusPoll = (0, react_1.useCallback)(() => {
290
- stopPoll();
291
- let ticks = 0;
292
- pollRef.current = setInterval(async () => {
293
- ticks += 1;
294
- if (completedRef.current || ticks > 150 /* ~5 min */) {
295
- stopPoll();
296
- return;
297
- }
229
+ }, [closeMiniBrowser, stopStatusPolling]);
230
+ // Bind partner.js to the connect button once it has a real onboarding URL.
231
+ const initPartner = (0, react_1.useCallback)(() => {
232
+ const signup = window.PAYPAL?.apps?.Signup;
233
+ const init = (signup?.miniBrowser && signup.miniBrowser.init) ||
234
+ (signup?.MiniBrowser && signup.MiniBrowser.init);
235
+ if (typeof init === "function") {
298
236
  try {
299
- const r = await fetch(`${STATUS_ENDPOINT}?environment=${envRef.current}`, {
300
- method: "GET",
301
- credentials: "include",
302
- });
303
- const st = await r.json().catch(() => ({}));
304
- if (st?.status === "connected" && st?.seller_client_id_present === true) {
305
- completedRef.current = true;
306
- stopPoll();
307
- try {
308
- popupRef.current?.close();
309
- }
310
- catch { }
311
- clearCachedUrl(envRef.current);
312
- setStatusInfo(st);
313
- setConnState("connected");
314
- setShowManual(false);
315
- setOnboardingInProgress(false);
316
- }
237
+ init();
238
+ }
239
+ catch (e) {
240
+ console.error("[paypal] partner.js init failed:", e);
241
+ }
242
+ }
243
+ }, []);
244
+ // Load partner.js once for the lifetime of the page.
245
+ (0, react_1.useEffect)(() => {
246
+ const existingScript = document.getElementById("paypal-partner-js");
247
+ if (existingScript)
248
+ return;
249
+ const preloadHref = PARTNER_JS_URL;
250
+ let preloadLink = null;
251
+ if (!document.head.querySelector(`link[rel="preload"][href="${preloadHref}"]`)) {
252
+ preloadLink = document.createElement("link");
253
+ preloadLink.rel = "preload";
254
+ preloadLink.href = preloadHref;
255
+ preloadLink.as = "script";
256
+ document.head.appendChild(preloadLink);
257
+ }
258
+ const ppScript = document.createElement("script");
259
+ ppScript.id = "paypal-partner-js";
260
+ ppScript.src = preloadHref;
261
+ ppScript.async = true;
262
+ document.head.appendChild(ppScript);
263
+ return () => {
264
+ if (preloadLink?.parentNode)
265
+ preloadLink.parentNode.removeChild(preloadLink);
266
+ if (ppScript.parentNode)
267
+ ppScript.parentNode.removeChild(ppScript);
268
+ };
269
+ }, []);
270
+ // Put the (cached or freshly generated) onboarding URL on the button, then wait
271
+ // for partner.js to be present before showing the button + initializing it. The
272
+ // button is only revealed once partner.js is ready, which is what guarantees
273
+ // the first click opens the mini-browser popup (never a new tab).
274
+ const activatePayPal = (0, react_1.useCallback)((url, runId) => {
275
+ if (paypalButtonRef.current) {
276
+ paypalButtonRef.current.href = url;
277
+ }
278
+ setFinalUrl(url);
279
+ let attempts = 0;
280
+ const MAX_ATTEMPTS = 200; // ~10s
281
+ const tryInit = () => {
282
+ if (runId !== currentRunId.current)
283
+ return;
284
+ if (window.PAYPAL?.apps?.Signup) {
285
+ initPartner();
286
+ setConnState("ready");
287
+ return;
317
288
  }
318
- catch {
319
- /* keep polling */
289
+ attempts++;
290
+ if (attempts >= MAX_ATTEMPTS) {
291
+ showError("PayPal partner script failed to load. Please refresh and try again.");
292
+ return;
320
293
  }
321
- }, 2000);
322
- }, [stopPoll]);
323
- const generateLinkAndReload = (0, react_1.useCallback)(async (targetEnv, runId) => {
294
+ setTimeout(tryInit, 50);
295
+ };
296
+ tryInit();
297
+ }, [initPartner, showError]);
298
+ const fetchFreshLink = (0, react_1.useCallback)(async (targetEnv, runId) => {
324
299
  if (initLoaderRef.current) {
325
300
  const loaderText = initLoaderRef.current.querySelector("#loader-text");
326
301
  if (loaderText)
@@ -344,20 +319,15 @@ function PayPalConnectionPage() {
344
319
  return;
345
320
  }
346
321
  const url = href + (href.includes("?") ? "&" : "?") + "displayMode=minibrowser";
347
- const persisted = writeCachedUrl(targetEnv, url);
348
- if (persisted) {
349
- window.location.reload();
350
- return;
351
- }
352
- setFinalUrl(url);
353
- setConnState("ready");
322
+ writeCachedUrl(targetEnv, url);
323
+ activatePayPal(url, runId);
354
324
  }
355
325
  catch {
356
326
  if (runId !== currentRunId.current)
357
327
  return;
358
328
  showError("Unable to connect to service.");
359
329
  }
360
- }, [showError]);
330
+ }, [activatePayPal, showError]);
361
331
  // Resolve the server environment first.
362
332
  (0, react_1.useEffect)(() => {
363
333
  let alive = true;
@@ -377,7 +347,7 @@ function PayPalConnectionPage() {
377
347
  alive = false;
378
348
  };
379
349
  }, []);
380
- // Page-load flow: connected? -> cached link? -> else generate + cache + reload.
350
+ // Page-load flow: connected? -> cached link? -> else generate a fresh link.
381
351
  (0, react_1.useEffect)(() => {
382
352
  if (!envReady)
383
353
  return;
@@ -388,7 +358,6 @@ function PayPalConnectionPage() {
388
358
  const run = async () => {
389
359
  setConnState("loading");
390
360
  setError(null);
391
- setPopupBlocked(false);
392
361
  setFinalUrl("");
393
362
  try {
394
363
  const r = await fetch(`${STATUS_ENDPOINT}?environment=${env}`, {
@@ -413,157 +382,52 @@ function PayPalConnectionPage() {
413
382
  return;
414
383
  const cached = readCachedUrl(env);
415
384
  if (cached) {
416
- setFinalUrl(cached);
417
- setConnState("ready");
385
+ activatePayPal(cached, runId);
418
386
  return;
419
387
  }
420
- await generateLinkAndReload(env, runId);
388
+ await fetchFreshLink(env, runId);
421
389
  };
422
390
  run();
423
391
  return () => {
424
392
  cancelled = true;
425
393
  };
426
- }, [env, envReady, generateLinkAndReload]);
427
- // Load partner.js + init for its postMessage->onboardingCallback bridge (a
428
- // secondary completion path next to our own message listener). We open the
429
- // popup ourselves, so partner.js is NOT relied on to open the window.
430
- (0, react_1.useEffect)(() => {
431
- if (connState !== "ready" || !finalUrl)
432
- return;
433
- const scriptUrl = PARTNER_JS_URLS[env];
434
- let cancelled = false;
435
- let pollTimer;
436
- const initPartner = (attempt = 0) => {
437
- if (cancelled)
438
- return;
439
- const signup = window.PAYPAL?.apps?.Signup;
440
- const target = signup?.miniBrowser && typeof signup.miniBrowser.init === "function"
441
- ? signup.miniBrowser
442
- : signup?.MiniBrowser && typeof signup.MiniBrowser.init === "function"
443
- ? signup.MiniBrowser
444
- : null;
445
- if (target?.init) {
446
- try {
447
- target.init();
448
- }
449
- catch (e) {
450
- console.error("[paypal] partner.js init failed:", e);
451
- }
452
- return;
453
- }
454
- if (typeof signup?.render === "function") {
455
- try {
456
- signup.render();
457
- }
458
- catch (e) {
459
- console.error("[paypal] partner.js render failed:", e);
460
- }
461
- return;
462
- }
463
- if (attempt < 40) {
464
- pollTimer = setTimeout(() => initPartner(attempt + 1), 50);
465
- return;
466
- }
467
- try {
468
- document.dispatchEvent(new Event("DOMContentLoaded"));
469
- }
470
- catch { }
471
- };
472
- const existingScript = document.getElementById("paypal-partner-js");
473
- if (existingScript) {
474
- existingScript.parentNode?.removeChild(existingScript);
475
- }
476
- if (window.PAYPAL?.apps?.Signup) {
477
- try {
478
- delete window.PAYPAL.apps.Signup;
479
- }
480
- catch { }
481
- }
482
- const ppScript = document.createElement("script");
483
- ppScript.id = "paypal-partner-js";
484
- ppScript.src = scriptUrl;
485
- ppScript.async = true;
486
- ppScript.onload = () => initPartner();
487
- document.body.appendChild(ppScript);
488
- return () => {
489
- cancelled = true;
490
- if (pollTimer)
491
- clearTimeout(pollTimer);
492
- if (ppScript.parentNode)
493
- ppScript.parentNode.removeChild(ppScript);
494
- };
495
- }, [connState, finalUrl, env]);
496
- // PRIMARY completion path: receive PayPal's onboarding postMessage ourselves.
497
- // The diagnostic log lets us confirm the exact origin/shape PayPal sends in a
498
- // real run if a tweak to the parser is ever needed.
499
- (0, react_1.useEffect)(() => {
500
- const onMessage = (ev) => {
501
- let serialized = "";
502
- try {
503
- serialized =
504
- typeof ev.data === "string" ? ev.data : JSON.stringify(ev.data);
505
- }
506
- catch {
507
- serialized = String(ev.data);
508
- }
509
- const looksRelevant = isPayPalOrigin(ev.origin) ||
510
- /auth_?code|shared_?id|onboard|merchantId/i.test(serialized || "");
511
- if (looksRelevant) {
512
- // eslint-disable-next-line no-console
513
- console.info("[PayPal onboarding] message from", ev.origin, "·", (serialized || "").slice(0, 500));
514
- }
515
- if (!isPayPalOrigin(ev.origin))
516
- return;
517
- const { authCode, sharedId } = extractAuth(ev.data);
518
- if (authCode && sharedId) {
519
- completeOnboarding(authCode, sharedId);
520
- }
521
- };
522
- window.addEventListener("message", onMessage);
523
- return () => window.removeEventListener("message", onMessage);
524
- }, [completeOnboarding]);
525
- // SECONDARY completion path: partner.js's bridge calls this by name.
394
+ }, [env, envReady, fetchFreshLink, activatePayPal]);
395
+ // partner.js invokes this by name (data-paypal-onboard-complete) once PayPal
396
+ // returns the seller's authCode + sharedId.
526
397
  (0, react_1.useLayoutEffect)(() => {
527
398
  window.onboardingCallback = (authCode, sharedId) => {
528
- completeOnboarding(authCode, sharedId);
399
+ void completeOnboarding(authCode, sharedId);
529
400
  };
530
401
  return () => {
531
402
  window.onboardingCallback = undefined;
532
403
  };
533
404
  }, [completeOnboarding]);
534
- // Capture-phase opener: runs before partner.js, opens the popup synchronously,
535
- // and stops partner.js's (unreliable, async) open + the native anchor so we get
536
- // exactly one popup and never a new tab.
405
+ // The return_url bridge (/store/paypal/onboard-return), which runs inside the
406
+ // popup after PayPal redirects to it, posts its params back here. The bridge has
407
+ // already exchanged credentials server-side, so we just refresh status to flip
408
+ // to "connected" and close the popup. If for some reason credentials aren't yet
409
+ // saved but PayPal forwarded an authCode/sharedId, complete it from here too.
537
410
  (0, react_1.useEffect)(() => {
538
- const onCaptureClick = (e) => {
539
- const btn = paypalButtonRef.current;
540
- if (!btn)
541
- return;
542
- const target = e.target;
543
- if (!target || !btn.contains(target))
544
- return;
545
- e.preventDefault();
546
- e.stopImmediatePropagation();
547
- if (connStateRef.current !== "ready" ||
548
- !finalUrlRef.current ||
549
- inProgressRef.current) {
550
- return;
551
- }
552
- completedRef.current = false;
553
- const popup = openOnboardingPopup(finalUrlRef.current);
554
- if (!popup) {
555
- setPopupBlocked(true);
411
+ const onMessage = (event) => {
412
+ const data = event?.data;
413
+ if (!data || data.source !== "paypal-onboarding-return")
556
414
  return;
557
- }
558
- setPopupBlocked(false);
559
- startStatusPoll();
415
+ const params = data.params || {};
416
+ const authCode = params.authCode || params.auth_code;
417
+ const sharedId = params.sharedId || params.shared_id;
418
+ const activeEnv = envRef.current;
419
+ void (async () => {
420
+ const connected = await refreshStatusAndMaybeConnect(activeEnv);
421
+ if (!connected && authCode && sharedId) {
422
+ await completeOnboarding(authCode, sharedId);
423
+ }
424
+ })();
560
425
  };
561
- document.addEventListener("click", onCaptureClick, true);
562
- return () => document.removeEventListener("click", onCaptureClick, true);
563
- }, [openOnboardingPopup, startStatusPoll]);
564
- (0, react_1.useEffect)(() => {
565
- return () => stopPoll();
566
- }, [stopPoll]);
426
+ window.addEventListener("message", onMessage);
427
+ return () => window.removeEventListener("message", onMessage);
428
+ }, [completeOnboarding, refreshStatusAndMaybeConnect]);
429
+ // Always stop polling when the component unmounts.
430
+ (0, react_1.useEffect)(() => stopStatusPolling, [stopStatusPolling]);
567
431
  (0, react_1.useLayoutEffect)(() => {
568
432
  const el = ppBtnMeasureRef.current;
569
433
  if (!el)
@@ -589,10 +453,17 @@ function PayPalConnectionPage() {
589
453
  window.removeEventListener("resize", update);
590
454
  };
591
455
  }, [connState, env, finalUrl]);
592
- // Defensive: the capture listener handles the click; this just guarantees the
593
- // native href can never navigate.
456
+ // Only block the click while we are not ready; when ready, let the native click
457
+ // through so partner.js can open the mini-browser. Start polling status so we
458
+ // detect completion (and close the popup) even if no completion message reaches
459
+ // this page.
594
460
  const handleConnectClick = (e) => {
595
- e.preventDefault();
461
+ if (connState !== "ready" || !finalUrl || onboardingInProgress) {
462
+ e.preventDefault();
463
+ return;
464
+ }
465
+ completedRef.current = false;
466
+ startStatusPolling();
596
467
  };
597
468
  const handleSaveManual = async () => {
598
469
  if (!canSaveManual || onboardingInProgress)
@@ -639,6 +510,7 @@ function PayPalConnectionPage() {
639
510
  return;
640
511
  if (!window.confirm("Disconnect PayPal for this environment?"))
641
512
  return;
513
+ stopStatusPolling();
642
514
  setOnboardingInProgress(true);
643
515
  setConnState("loading");
644
516
  setError(null);
@@ -659,7 +531,7 @@ function PayPalConnectionPage() {
659
531
  clearCachedUrl(env);
660
532
  completedRef.current = false;
661
533
  currentRunId.current = ++runIdRef.current;
662
- await generateLinkAndReload(env, currentRunId.current);
534
+ await fetchFreshLink(env, currentRunId.current);
663
535
  }
664
536
  catch (e) {
665
537
  console.error(e);
@@ -674,9 +546,9 @@ function PayPalConnectionPage() {
674
546
  const next = e.target.value;
675
547
  if (next === env || onboardingInProgress)
676
548
  return;
549
+ stopStatusPolling();
677
550
  completedRef.current = false;
678
551
  clearCachedUrl();
679
- setPopupBlocked(false);
680
552
  setConnState("loading");
681
553
  try {
682
554
  await fetch(ENVIRONMENT_ENDPOINT, {
@@ -689,16 +561,16 @@ function PayPalConnectionPage() {
689
561
  catch { }
690
562
  setEnv(next);
691
563
  };
692
- return ((0, jsx_runtime_1.jsxs)("div", { className: "p-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-6", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-start justify-between gap-4", children: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("h1", { className: "text-xl font-semibold text-ui-fg-base", children: "PayPal Gateway By Easy Payment" }) }) }), (0, jsx_runtime_1.jsx)(Tabs_1.default, {}), (0, jsx_runtime_1.jsx)("div", { className: "rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm", children: (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 gap-y-6 p-4 md:grid-cols-[260px_1fr] md:items-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-sm font-medium pt-2", children: "Environment" }), (0, jsx_runtime_1.jsx)("div", { className: "max-w-xl", children: (0, jsx_runtime_1.jsxs)("select", { value: env, onChange: handleEnvChange, disabled: onboardingInProgress, className: "w-full rounded-md border border-ui-border-base bg-transparent px-3 py-2 text-sm", children: [(0, jsx_runtime_1.jsx)("option", { value: "sandbox", children: "Sandbox (Test Mode)" }), (0, jsx_runtime_1.jsx)("option", { value: "live", children: "Live (Production)" })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "text-sm font-medium pt-2", children: env === "sandbox" ? "Connect to PayPal (Sandbox)" : "Connect to PayPal" }), (0, jsx_runtime_1.jsx)("div", { className: "max-w-xl", children: connState === "connected" ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "text-sm text-green-600 bg-green-50 p-3 rounded border border-green-200", children: "\u2705 Successfully connected to PayPal!" }), (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)("div", { className: "font-medium text-ui-fg-base", children: "Connected PayPal account" }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-1", children: ["Email:", " ", (0, jsx_runtime_1.jsx)("span", { className: "font-mono text-ui-fg-base", children: statusInfo?.seller_email || "Unavailable" })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "mt-3 flex items-center gap-2", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleDisconnect, disabled: onboardingInProgress, className: "transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none shadow-buttons-neutral text-ui-fg-base bg-ui-button-neutral after:transition-fg after:absolute after:inset-0 after:content-[''] after:button-neutral-gradient hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient focus-visible:shadow-buttons-neutral-focus disabled:bg-ui-bg-disabled disabled:border-ui-border-base disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral disabled:after:hidden txt-compact-small-plus px-3 py-1.5", children: "Disconnect" }) })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { ref: initLoaderRef, id: "init-loader", className: `status-msg mb-4 ${connState !== "loading" ? "hidden" : "block"}`, children: [(0, jsx_runtime_1.jsx)("div", { className: "loader inline-block align-middle mr-2" }), (0, jsx_runtime_1.jsx)("span", { id: "loader-text", className: "text-sm", children: onboardingInProgress
564
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "p-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-6", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-start justify-between gap-4", children: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("h1", { className: "text-xl font-semibold text-ui-fg-base", children: "PayPal Gateway By Easy Payment" }) }) }), (0, jsx_runtime_1.jsx)(Tabs_1.default, {}), (0, jsx_runtime_1.jsx)("div", { className: "rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm", children: (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 gap-y-6 p-4 md:grid-cols-[260px_1fr] md:items-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-sm font-medium pt-2", children: "Environment" }), (0, jsx_runtime_1.jsx)("div", { className: "max-w-xl", children: (0, jsx_runtime_1.jsxs)("select", { value: env, onChange: handleEnvChange, disabled: onboardingInProgress, className: "w-full rounded-md border border-ui-border-base bg-transparent px-3 py-2 text-sm", children: [(0, jsx_runtime_1.jsx)("option", { value: "sandbox", children: "Sandbox (Test Mode)" }), (0, jsx_runtime_1.jsx)("option", { value: "live", children: "Live (Production)" })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "text-sm font-medium pt-2", children: env === "sandbox" ? "Connect to PayPal (Sandbox)" : "Connect to PayPal" }), (0, jsx_runtime_1.jsx)("div", { className: "max-w-xl", children: connState === "connected" ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-sm text-green-600 bg-green-50 p-3 rounded border border-green-200", children: ["\u2705 Successfully connected to PayPal!", (0, jsx_runtime_1.jsx)("a", { "data-paypal-button": "true", "data-paypal-onboard-complete": "onboardingCallback", href: "#", style: { display: "none" }, children: "PayPal" })] }), (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)("div", { className: "font-medium text-ui-fg-base", children: "Connected PayPal account" }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-1", children: ["Email:", " ", (0, jsx_runtime_1.jsx)("span", { className: "font-mono text-ui-fg-base", children: statusInfo?.seller_email || "Unavailable" })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "mt-3 flex items-center gap-2", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleDisconnect, disabled: onboardingInProgress, className: "transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none shadow-buttons-neutral text-ui-fg-base bg-ui-button-neutral after:transition-fg after:absolute after:inset-0 after:content-[''] after:button-neutral-gradient hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient focus-visible:shadow-buttons-neutral-focus disabled:bg-ui-bg-disabled disabled:border-ui-border-base disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral disabled:after:hidden txt-compact-small-plus px-3 py-1.5", children: "Disconnect" }) })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { ref: initLoaderRef, id: "init-loader", className: `status-msg mb-4 ${connState !== "loading" ? "hidden" : "block"}`, children: [(0, jsx_runtime_1.jsx)("div", { className: "loader inline-block align-middle mr-2" }), (0, jsx_runtime_1.jsx)("span", { id: "loader-text", className: "text-sm", children: onboardingInProgress
693
565
  ? "Configuring connection to PayPal…"
694
566
  : "Checking connection..." })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${connState === "ready" ? "block" : "hidden"}`, children: [(0, jsx_runtime_1.jsx)("a", { ref: (node) => {
695
567
  paypalButtonRef.current = node;
696
568
  ppBtnMeasureRef.current = node;
697
- }, id: "paypal-button", target: POPUP_NAME, "data-paypal-button": "true", href: finalUrl || "#", "data-paypal-onboard-complete": "onboardingCallback", onClick: handleConnectClick, className: "transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none no-underline shadow-buttons-neutral text-ui-fg-base bg-ui-button-neutral after:transition-fg after:absolute after:inset-0 after:content-[''] after:button-neutral-gradient hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient focus-visible:shadow-buttons-neutral-focus disabled:bg-ui-bg-disabled disabled:border-ui-border-base disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral disabled:after:hidden txt-compact-small-plus px-3 py-1.5", style: {
569
+ }, id: "paypal-button", "data-paypal-button": "true", href: finalUrl || "#", "data-paypal-onboard-complete": "onboardingCallback", onClick: handleConnectClick, className: "transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none no-underline shadow-buttons-neutral text-ui-fg-base bg-ui-button-neutral after:transition-fg after:absolute after:inset-0 after:content-[''] after:button-neutral-gradient hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient focus-visible:shadow-buttons-neutral-focus disabled:bg-ui-bg-disabled disabled:border-ui-border-base disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral disabled:after:hidden txt-compact-small-plus px-3 py-1.5", style: {
698
570
  cursor: onboardingInProgress ? "not-allowed" : "pointer",
699
571
  opacity: onboardingInProgress ? 0.6 : 1,
700
572
  pointerEvents: onboardingInProgress ? "none" : "auto",
701
- }, children: "Connect to PayPal" }), popupBlocked && ((0, jsx_runtime_1.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 \u201CConnect to PayPal\u201D again." })), (0, jsx_runtime_1.jsx)("div", { className: "mt-2", style: {
573
+ }, children: "Connect to PayPal" }), (0, jsx_runtime_1.jsx)("div", { className: "mt-2", style: {
702
574
  width: ppBtnWidth ? `${ppBtnWidth}px` : "auto",
703
575
  marginTop: "20px",
704
576
  marginBottom: "10px",