@easypayment/medusa-payment-paypal 0.9.22 → 0.9.24
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/.medusa/server/src/admin/index.js +262 -92
- package/.medusa/server/src/admin/index.mjs +262 -92
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts.map +1 -1
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js +304 -146
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js.map +1 -1
- package/.medusa/server/src/api/admin/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
- package/.medusa/server/src/api/admin/payment-collections/[id]/payment-sessions/route.js +6 -8
- package/.medusa/server/src/api/admin/payment-collections/[id]/payment-sessions/route.js.map +1 -1
- package/.medusa/server/src/api/admin/paypal/environment/route.d.ts.map +1 -1
- package/.medusa/server/src/api/admin/paypal/environment/route.js +11 -3
- package/.medusa/server/src/api/admin/paypal/environment/route.js.map +1 -1
- package/.medusa/server/src/api/admin/paypal/onboarding-status/route.d.ts.map +1 -1
- package/.medusa/server/src/api/admin/paypal/onboarding-status/route.js +4 -1
- package/.medusa/server/src/api/admin/paypal/onboarding-status/route.js.map +1 -1
- package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js +6 -8
- package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/capture-order/route.js +11 -0
- package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.js +2 -4
- package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +55 -34
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/core-workflow.d.ts +32 -0
- package/.medusa/server/src/modules/paypal/utils/core-workflow.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/core-workflow.js +40 -0
- package/.medusa/server/src/modules/paypal/utils/core-workflow.js.map +1 -0
- package/package.json +1 -1
- package/src/admin/routes/settings/paypal/connection/page.tsx +330 -161
- package/src/api/admin/payment-collections/[id]/payment-sessions/route.ts +6 -8
- package/src/api/admin/paypal/environment/route.ts +10 -3
- package/src/api/admin/paypal/onboarding-status/route.ts +4 -1
- package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +6 -8
- package/src/api/store/paypal/capture-order/route.ts +15 -0
- package/src/api/store/paypal-complete/route.ts +6 -4
- package/src/modules/paypal/payment-provider/card-service.ts +72 -38
- package/src/modules/paypal/utils/core-workflow.ts +46 -0
|
@@ -17,15 +17,10 @@ const PARTNER_JS_URLS = {
|
|
|
17
17
|
live: "https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js",
|
|
18
18
|
};
|
|
19
19
|
const SERVICE_URL = "/admin/paypal/onboarding-link";
|
|
20
|
-
// PayPal's conventional mini-browser window name
|
|
21
|
-
// `target` so partner.js opens the flow in a popup.
|
|
20
|
+
// PayPal's conventional mini-browser window name (also the anchor target).
|
|
22
21
|
const POPUP_NAME = "PPFrame";
|
|
23
|
-
// The onboarding link is cached in the browser for 6 hours
|
|
24
|
-
//
|
|
25
|
-
// callback if the anchor already carries a valid onboarding href when partner.js
|
|
26
|
-
// initializes. By caching the link (and reloading once to populate the cache on
|
|
27
|
-
// a cold start) the link is always present on page load, so partner.js can take
|
|
28
|
-
// over the click and run its flow exactly as documented.
|
|
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.
|
|
29
24
|
const CACHE_PREFIX = "pp_onboard_cache";
|
|
30
25
|
const CACHE_EXPIRY = 6 * 60 * 60 * 1000; // 6 hours
|
|
31
26
|
const ONBOARDING_COMPLETE_ENDPOINT = "/admin/paypal/onboard-complete";
|
|
@@ -33,9 +28,6 @@ const STATUS_ENDPOINT = "/admin/paypal/status";
|
|
|
33
28
|
const SAVE_CREDENTIALS_ENDPOINT = "/admin/paypal/save-credentials";
|
|
34
29
|
const DISCONNECT_ENDPOINT = "/admin/paypal/disconnect";
|
|
35
30
|
const ENVIRONMENT_ENDPOINT = "/admin/paypal/environment";
|
|
36
|
-
// Onboarding URLs are environment-specific, so the cache is keyed per
|
|
37
|
-
// environment — a single shared key let a stale sandbox URL leak into the live
|
|
38
|
-
// flow (and vice-versa).
|
|
39
31
|
const cacheKeyFor = (env) => `${CACHE_PREFIX}_${env}`;
|
|
40
32
|
function readCachedUrl(env) {
|
|
41
33
|
if (typeof window === "undefined")
|
|
@@ -56,8 +48,6 @@ function readCachedUrl(env) {
|
|
|
56
48
|
}
|
|
57
49
|
return null;
|
|
58
50
|
}
|
|
59
|
-
// Returns true only if the value was actually persisted (storage enabled), which
|
|
60
|
-
// lets the caller decide whether reloading to pick up the cache is safe.
|
|
61
51
|
function writeCachedUrl(env, url) {
|
|
62
52
|
try {
|
|
63
53
|
localStorage.setItem(cacheKeyFor(env), JSON.stringify({ url, ts: Date.now() }));
|
|
@@ -81,18 +71,79 @@ function clearCachedUrl(env) {
|
|
|
81
71
|
/* ignore */
|
|
82
72
|
}
|
|
83
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
|
+
}
|
|
84
141
|
function PayPalConnectionPage() {
|
|
85
142
|
const [env, setEnv] = (0, react_1.useState)("live");
|
|
86
|
-
// We must know the server's environment before generating/looking up a link so
|
|
87
|
-
// the per-environment cache and the generated link always match.
|
|
88
143
|
const [envReady, setEnvReady] = (0, react_1.useState)(false);
|
|
89
144
|
const [connState, setConnState] = (0, react_1.useState)("loading");
|
|
90
|
-
// True once partner.js has loaded and bound its click interceptor to the
|
|
91
|
-
// button. The button stays in the DOM but is not clickable until then, so a
|
|
92
|
-
// click can never fall through to the native anchor (which would open a tab
|
|
93
|
-
// without partner.js's onboarding flow / completion callback).
|
|
94
|
-
const [partnerReady, setPartnerReady] = (0, react_1.useState)(false);
|
|
95
145
|
const [error, setError] = (0, react_1.useState)(null);
|
|
146
|
+
const [popupBlocked, setPopupBlocked] = (0, react_1.useState)(false);
|
|
96
147
|
const [finalUrl, setFinalUrl] = (0, react_1.useState)("");
|
|
97
148
|
const [showManual, setShowManual] = (0, react_1.useState)(false);
|
|
98
149
|
const [clientId, setClientId] = (0, react_1.useState)("");
|
|
@@ -100,13 +151,20 @@ function PayPalConnectionPage() {
|
|
|
100
151
|
const [statusInfo, setStatusInfo] = (0, react_1.useState)(null);
|
|
101
152
|
const [onboardingInProgress, setOnboardingInProgress] = (0, react_1.useState)(false);
|
|
102
153
|
const initLoaderRef = (0, react_1.useRef)(null);
|
|
154
|
+
const paypalButtonRef = (0, react_1.useRef)(null);
|
|
103
155
|
const errorLogRef = (0, react_1.useRef)(null);
|
|
104
156
|
const runIdRef = (0, react_1.useRef)(0);
|
|
105
157
|
const currentRunId = (0, react_1.useRef)(0);
|
|
106
158
|
const envRef = (0, react_1.useRef)(env);
|
|
107
159
|
envRef.current = env;
|
|
108
|
-
|
|
109
|
-
|
|
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);
|
|
110
168
|
const completedRef = (0, react_1.useRef)(false);
|
|
111
169
|
const ppBtnMeasureRef = (0, react_1.useRef)(null);
|
|
112
170
|
const [ppBtnWidth, setPpBtnWidth] = (0, react_1.useState)(null);
|
|
@@ -117,16 +175,156 @@ function PayPalConnectionPage() {
|
|
|
117
175
|
setConnState("error");
|
|
118
176
|
setError(msg);
|
|
119
177
|
}, []);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
178
|
+
const stopPoll = (0, react_1.useCallback)(() => {
|
|
179
|
+
if (pollRef.current) {
|
|
180
|
+
clearInterval(pollRef.current);
|
|
181
|
+
pollRef.current = null;
|
|
182
|
+
}
|
|
183
|
+
}, []);
|
|
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;
|
|
199
|
+
try {
|
|
200
|
+
popup = window.open(url, POPUP_NAME, features);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
popup = null;
|
|
204
|
+
}
|
|
205
|
+
if (popup) {
|
|
206
|
+
popupRef.current = popup;
|
|
207
|
+
try {
|
|
208
|
+
popup.focus();
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
/* ignore */
|
|
212
|
+
}
|
|
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.
|
|
219
|
+
const completeOnboarding = (0, react_1.useCallback)(async (authCode, sharedId) => {
|
|
220
|
+
if (!authCode || !sharedId)
|
|
221
|
+
return;
|
|
222
|
+
if (completedRef.current)
|
|
223
|
+
return;
|
|
224
|
+
completedRef.current = true;
|
|
225
|
+
stopPoll();
|
|
226
|
+
try {
|
|
227
|
+
window.onbeforeunload = null;
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
const activeEnv = envRef.current === "sandbox" ? "sandbox" : "live";
|
|
231
|
+
setOnboardingInProgress(true);
|
|
232
|
+
setConnState("loading");
|
|
233
|
+
setError(null);
|
|
234
|
+
setPopupBlocked(false);
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetch(ONBOARDING_COMPLETE_ENDPOINT, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "content-type": "application/json" },
|
|
239
|
+
credentials: "include",
|
|
240
|
+
body: JSON.stringify({ authCode, sharedId, env: activeEnv }),
|
|
241
|
+
});
|
|
242
|
+
if (!res.ok) {
|
|
243
|
+
const txt = await res.text().catch(() => "");
|
|
244
|
+
throw new Error(txt || `Onboarding exchange failed (${res.status})`);
|
|
245
|
+
}
|
|
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 { }
|
|
263
|
+
clearCachedUrl(activeEnv);
|
|
264
|
+
try {
|
|
265
|
+
const statusRes = await fetch(`${STATUS_ENDPOINT}?environment=${activeEnv}`, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
credentials: "include",
|
|
268
|
+
});
|
|
269
|
+
const refreshedStatus = await statusRes.json().catch(() => ({}));
|
|
270
|
+
setStatusInfo(refreshedStatus || null);
|
|
271
|
+
}
|
|
272
|
+
catch { }
|
|
273
|
+
setConnState("connected");
|
|
274
|
+
setShowManual(false);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
completedRef.current = false;
|
|
278
|
+
console.error(e);
|
|
279
|
+
setConnState("error");
|
|
280
|
+
setError(e?.message || "Exchange failed while saving credentials.");
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
setOnboardingInProgress(false);
|
|
284
|
+
}
|
|
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
|
+
}
|
|
298
|
+
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
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
/* keep polling */
|
|
320
|
+
}
|
|
321
|
+
}, 2000);
|
|
322
|
+
}, [stopPoll]);
|
|
124
323
|
const generateLinkAndReload = (0, react_1.useCallback)(async (targetEnv, runId) => {
|
|
125
324
|
if (initLoaderRef.current) {
|
|
126
325
|
const loaderText = initLoaderRef.current.querySelector("#loader-text");
|
|
127
|
-
if (loaderText)
|
|
326
|
+
if (loaderText)
|
|
128
327
|
loaderText.textContent = "Generating onboarding session...";
|
|
129
|
-
}
|
|
130
328
|
}
|
|
131
329
|
try {
|
|
132
330
|
const res = await fetch(SERVICE_URL, {
|
|
@@ -148,13 +346,9 @@ function PayPalConnectionPage() {
|
|
|
148
346
|
const url = href + (href.includes("?") ? "&" : "?") + "displayMode=minibrowser";
|
|
149
347
|
const persisted = writeCachedUrl(targetEnv, url);
|
|
150
348
|
if (persisted) {
|
|
151
|
-
// The cache survived; reload so the link is present at first render and
|
|
152
|
-
// partner.js initializes against it. The post-reload load finds the
|
|
153
|
-
// cache (no regeneration), so this can't loop.
|
|
154
349
|
window.location.reload();
|
|
155
350
|
return;
|
|
156
351
|
}
|
|
157
|
-
// Storage disabled — use the link directly without reloading.
|
|
158
352
|
setFinalUrl(url);
|
|
159
353
|
setConnState("ready");
|
|
160
354
|
}
|
|
@@ -183,11 +377,7 @@ function PayPalConnectionPage() {
|
|
|
183
377
|
alive = false;
|
|
184
378
|
};
|
|
185
379
|
}, []);
|
|
186
|
-
// Page-load flow:
|
|
187
|
-
// 1. If already connected, show the connected state.
|
|
188
|
-
// 2. Else if a valid cached link exists, use it (partner.js will bind it).
|
|
189
|
-
// 3. Else generate a link, cache it for 6h, and reload so the cached link is
|
|
190
|
-
// present for partner.js on the next load.
|
|
380
|
+
// Page-load flow: connected? -> cached link? -> else generate + cache + reload.
|
|
191
381
|
(0, react_1.useEffect)(() => {
|
|
192
382
|
if (!envReady)
|
|
193
383
|
return;
|
|
@@ -198,6 +388,7 @@ function PayPalConnectionPage() {
|
|
|
198
388
|
const run = async () => {
|
|
199
389
|
setConnState("loading");
|
|
200
390
|
setError(null);
|
|
391
|
+
setPopupBlocked(false);
|
|
201
392
|
setFinalUrl("");
|
|
202
393
|
try {
|
|
203
394
|
const r = await fetch(`${STATUS_ENDPOINT}?environment=${env}`, {
|
|
@@ -233,30 +424,15 @@ function PayPalConnectionPage() {
|
|
|
233
424
|
cancelled = true;
|
|
234
425
|
};
|
|
235
426
|
}, [env, envReady, generateLinkAndReload]);
|
|
236
|
-
// Load partner.js
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// ourselves for it to discover the button and take over the click into the
|
|
240
|
-
// mini-browser — opening the popup, and wiring its postMessage bridge that
|
|
241
|
-
// invokes window.onboardingCallback and closes the popup on completion.
|
|
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.
|
|
242
430
|
(0, react_1.useEffect)(() => {
|
|
243
431
|
if (connState !== "ready" || !finalUrl)
|
|
244
432
|
return;
|
|
245
433
|
const scriptUrl = PARTNER_JS_URLS[env];
|
|
246
434
|
let cancelled = false;
|
|
247
435
|
let pollTimer;
|
|
248
|
-
// The button is gated on partnerReady; reset it for this (re)load.
|
|
249
|
-
setPartnerReady(false);
|
|
250
|
-
// Safety net: if partner.js can't load (e.g. blocked), enable the button
|
|
251
|
-
// anyway after a few seconds rather than leaving it permanently disabled.
|
|
252
|
-
const readyFallback = setTimeout(() => {
|
|
253
|
-
if (!cancelled)
|
|
254
|
-
setPartnerReady(true);
|
|
255
|
-
}, 6000);
|
|
256
|
-
const markReady = () => {
|
|
257
|
-
if (!cancelled)
|
|
258
|
-
setPartnerReady(true);
|
|
259
|
-
};
|
|
260
436
|
const initPartner = (attempt = 0) => {
|
|
261
437
|
if (cancelled)
|
|
262
438
|
return;
|
|
@@ -273,7 +449,6 @@ function PayPalConnectionPage() {
|
|
|
273
449
|
catch (e) {
|
|
274
450
|
console.error("[paypal] partner.js init failed:", e);
|
|
275
451
|
}
|
|
276
|
-
markReady();
|
|
277
452
|
return;
|
|
278
453
|
}
|
|
279
454
|
if (typeof signup?.render === "function") {
|
|
@@ -283,11 +458,8 @@ function PayPalConnectionPage() {
|
|
|
283
458
|
catch (e) {
|
|
284
459
|
console.error("[paypal] partner.js render failed:", e);
|
|
285
460
|
}
|
|
286
|
-
markReady();
|
|
287
461
|
return;
|
|
288
462
|
}
|
|
289
|
-
// The namespace may not be populated the instant onload fires; retry
|
|
290
|
-
// briefly before giving up.
|
|
291
463
|
if (attempt < 40) {
|
|
292
464
|
pollTimer = setTimeout(() => initPartner(attempt + 1), 50);
|
|
293
465
|
return;
|
|
@@ -296,10 +468,7 @@ function PayPalConnectionPage() {
|
|
|
296
468
|
document.dispatchEvent(new Event("DOMContentLoaded"));
|
|
297
469
|
}
|
|
298
470
|
catch { }
|
|
299
|
-
markReady();
|
|
300
471
|
};
|
|
301
|
-
// Remove any stale copy + namespace so the fresh load re-registers against
|
|
302
|
-
// the current environment (sandbox vs live partner.js differ).
|
|
303
472
|
const existingScript = document.getElementById("paypal-partner-js");
|
|
304
473
|
if (existingScript) {
|
|
305
474
|
existingScript.parentNode?.removeChild(existingScript);
|
|
@@ -315,85 +484,45 @@ function PayPalConnectionPage() {
|
|
|
315
484
|
ppScript.src = scriptUrl;
|
|
316
485
|
ppScript.async = true;
|
|
317
486
|
ppScript.onload = () => initPartner();
|
|
318
|
-
ppScript.onerror = () => markReady();
|
|
319
487
|
document.body.appendChild(ppScript);
|
|
320
488
|
return () => {
|
|
321
489
|
cancelled = true;
|
|
322
|
-
|
|
323
|
-
if (pollTimer) {
|
|
490
|
+
if (pollTimer)
|
|
324
491
|
clearTimeout(pollTimer);
|
|
325
|
-
|
|
326
|
-
if (ppScript.parentNode) {
|
|
492
|
+
if (ppScript.parentNode)
|
|
327
493
|
ppScript.parentNode.removeChild(ppScript);
|
|
328
|
-
}
|
|
329
494
|
};
|
|
330
495
|
}, [connState, finalUrl, env]);
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (completedRef.current)
|
|
338
|
-
return;
|
|
339
|
-
completedRef.current = true;
|
|
340
|
-
try {
|
|
341
|
-
window.onbeforeunload = null;
|
|
342
|
-
}
|
|
343
|
-
catch { }
|
|
344
|
-
const activeEnv = envRef.current === "sandbox" ? "sandbox" : "live";
|
|
345
|
-
setOnboardingInProgress(true);
|
|
346
|
-
setConnState("loading");
|
|
347
|
-
setError(null);
|
|
348
|
-
try {
|
|
349
|
-
const res = await fetch(ONBOARDING_COMPLETE_ENDPOINT, {
|
|
350
|
-
method: "POST",
|
|
351
|
-
headers: { "content-type": "application/json" },
|
|
352
|
-
credentials: "include",
|
|
353
|
-
body: JSON.stringify({ authCode, sharedId, env: activeEnv }),
|
|
354
|
-
});
|
|
355
|
-
if (!res.ok) {
|
|
356
|
-
const txt = await res.text().catch(() => "");
|
|
357
|
-
throw new Error(txt || `Onboarding exchange failed (${res.status})`);
|
|
358
|
-
}
|
|
359
|
-
// Close the partner.js mini-browser.
|
|
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 = "";
|
|
360
502
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
close1();
|
|
503
|
+
serialized =
|
|
504
|
+
typeof ev.data === "string" ? ev.data : JSON.stringify(ev.data);
|
|
364
505
|
}
|
|
365
|
-
catch {
|
|
366
|
-
|
|
367
|
-
const close2 = window.PAYPAL?.apps?.Signup?.miniBrowser &&
|
|
368
|
-
window.PAYPAL.apps.Signup.miniBrowser.closeFlow;
|
|
369
|
-
if (typeof close2 === "function")
|
|
370
|
-
close2();
|
|
506
|
+
catch {
|
|
507
|
+
serialized = String(ev.data);
|
|
371
508
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
method: "GET",
|
|
378
|
-
credentials: "include",
|
|
379
|
-
});
|
|
380
|
-
const refreshedStatus = await statusRes.json().catch(() => ({}));
|
|
381
|
-
setStatusInfo(refreshedStatus || null);
|
|
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));
|
|
382
514
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
setOnboardingInProgress(false);
|
|
395
|
-
}
|
|
396
|
-
}, []);
|
|
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.
|
|
397
526
|
(0, react_1.useLayoutEffect)(() => {
|
|
398
527
|
window.onboardingCallback = (authCode, sharedId) => {
|
|
399
528
|
completeOnboarding(authCode, sharedId);
|
|
@@ -402,6 +531,39 @@ function PayPalConnectionPage() {
|
|
|
402
531
|
window.onboardingCallback = undefined;
|
|
403
532
|
};
|
|
404
533
|
}, [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.
|
|
537
|
+
(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);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
setPopupBlocked(false);
|
|
559
|
+
startStatusPoll();
|
|
560
|
+
};
|
|
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]);
|
|
405
567
|
(0, react_1.useLayoutEffect)(() => {
|
|
406
568
|
const el = ppBtnMeasureRef.current;
|
|
407
569
|
if (!el)
|
|
@@ -427,6 +589,11 @@ function PayPalConnectionPage() {
|
|
|
427
589
|
window.removeEventListener("resize", update);
|
|
428
590
|
};
|
|
429
591
|
}, [connState, env, finalUrl]);
|
|
592
|
+
// Defensive: the capture listener handles the click; this just guarantees the
|
|
593
|
+
// native href can never navigate.
|
|
594
|
+
const handleConnectClick = (e) => {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
};
|
|
430
597
|
const handleSaveManual = async () => {
|
|
431
598
|
if (!canSaveManual || onboardingInProgress)
|
|
432
599
|
return;
|
|
@@ -491,8 +658,6 @@ function PayPalConnectionPage() {
|
|
|
491
658
|
}
|
|
492
659
|
clearCachedUrl(env);
|
|
493
660
|
completedRef.current = false;
|
|
494
|
-
// Generate a fresh link for the now-disconnected environment and reload so
|
|
495
|
-
// the onboarding button is ready again.
|
|
496
661
|
currentRunId.current = ++runIdRef.current;
|
|
497
662
|
await generateLinkAndReload(env, currentRunId.current);
|
|
498
663
|
}
|
|
@@ -511,9 +676,8 @@ function PayPalConnectionPage() {
|
|
|
511
676
|
return;
|
|
512
677
|
completedRef.current = false;
|
|
513
678
|
clearCachedUrl();
|
|
679
|
+
setPopupBlocked(false);
|
|
514
680
|
setConnState("loading");
|
|
515
|
-
// Persist the environment BEFORE switching local state so the post-reload
|
|
516
|
-
// GET /environment returns the same value the cache was keyed under.
|
|
517
681
|
try {
|
|
518
682
|
await fetch(ENVIRONMENT_ENDPOINT, {
|
|
519
683
|
method: "POST",
|
|
@@ -528,19 +692,13 @@ function PayPalConnectionPage() {
|
|
|
528
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
|
|
529
693
|
? "Configuring connection to PayPal…"
|
|
530
694
|
: "Checking connection..." })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${connState === "ready" ? "block" : "hidden"}`, children: [(0, jsx_runtime_1.jsx)("a", { ref: (node) => {
|
|
695
|
+
paypalButtonRef.current = node;
|
|
531
696
|
ppBtnMeasureRef.current = node;
|
|
532
|
-
}, id: "paypal-button", target: POPUP_NAME, "data-paypal-button": "true", href: finalUrl || "#", "data-paypal-onboard-complete": "onboardingCallback",
|
|
533
|
-
cursor: onboardingInProgress
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// Keep the anchor mounted (partner.js must find it to bind)
|
|
538
|
-
// but un-clickable until partner.js has taken over the
|
|
539
|
-
// click — so it can never open a tab via the native href.
|
|
540
|
-
pointerEvents: onboardingInProgress || !partnerReady ? "none" : "auto",
|
|
541
|
-
}, children: partnerReady || onboardingInProgress
|
|
542
|
-
? "Connect to PayPal"
|
|
543
|
-
: "Preparing PayPal…" }), (0, jsx_runtime_1.jsx)("div", { className: "mt-2", style: {
|
|
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: {
|
|
698
|
+
cursor: onboardingInProgress ? "not-allowed" : "pointer",
|
|
699
|
+
opacity: onboardingInProgress ? 0.6 : 1,
|
|
700
|
+
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: {
|
|
544
702
|
width: ppBtnWidth ? `${ppBtnWidth}px` : "auto",
|
|
545
703
|
marginTop: "20px",
|
|
546
704
|
marginBottom: "10px",
|