@easypayment/medusa-payment-paypal 0.9.21 → 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 +258 -214
- package/.medusa/server/src/admin/index.mjs +258 -214
- 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 +210 -223
- 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 +273 -293
- 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,26 +17,17 @@ 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
|
-
//
|
|
21
|
-
// gesture (see openOnboardingPopup) — the only reliable way to get a popup
|
|
22
|
-
// rather than a blocked window or a new tab. The window name matches the
|
|
23
|
-
// anchor's `target` and PayPal's conventional "PPFrame": partner.js opens the
|
|
24
|
-
// flow with `window.open(url, target, ...)`, so when it runs right after us it
|
|
25
|
-
// *reuses this already-open window* (a named-window navigation needs no user
|
|
26
|
-
// activation) instead of trying — and failing, asynchronously — to spawn its
|
|
27
|
-
// own. That lets partner.js still wire its completion bridge without ever
|
|
28
|
-
// opening a second/late window.
|
|
20
|
+
// PayPal's conventional mini-browser window name (also the anchor target).
|
|
29
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.
|
|
30
24
|
const CACHE_PREFIX = "pp_onboard_cache";
|
|
31
|
-
const CACHE_EXPIRY =
|
|
25
|
+
const CACHE_EXPIRY = 6 * 60 * 60 * 1000; // 6 hours
|
|
32
26
|
const ONBOARDING_COMPLETE_ENDPOINT = "/admin/paypal/onboard-complete";
|
|
33
27
|
const STATUS_ENDPOINT = "/admin/paypal/status";
|
|
34
28
|
const SAVE_CREDENTIALS_ENDPOINT = "/admin/paypal/save-credentials";
|
|
35
29
|
const DISCONNECT_ENDPOINT = "/admin/paypal/disconnect";
|
|
36
30
|
const ENVIRONMENT_ENDPOINT = "/admin/paypal/environment";
|
|
37
|
-
// Onboarding URLs are environment-specific and short-lived, so the cache must be
|
|
38
|
-
// keyed per environment — a single shared key let a stale sandbox URL leak into
|
|
39
|
-
// the live flow (and vice-versa) after a reload.
|
|
40
31
|
const cacheKeyFor = (env) => `${CACHE_PREFIX}_${env}`;
|
|
41
32
|
function readCachedUrl(env) {
|
|
42
33
|
if (typeof window === "undefined")
|
|
@@ -60,9 +51,10 @@ function readCachedUrl(env) {
|
|
|
60
51
|
function writeCachedUrl(env, url) {
|
|
61
52
|
try {
|
|
62
53
|
localStorage.setItem(cacheKeyFor(env), JSON.stringify({ url, ts: Date.now() }));
|
|
54
|
+
return readCachedUrl(env) === url;
|
|
63
55
|
}
|
|
64
56
|
catch {
|
|
65
|
-
|
|
57
|
+
return false;
|
|
66
58
|
}
|
|
67
59
|
}
|
|
68
60
|
function clearCachedUrl(env) {
|
|
@@ -79,9 +71,6 @@ function clearCachedUrl(env) {
|
|
|
79
71
|
/* ignore */
|
|
80
72
|
}
|
|
81
73
|
}
|
|
82
|
-
// PayPal delivers the seamless-onboarding result by posting a message to
|
|
83
|
-
// window.opener. We validate the sender is actually a PayPal origin before
|
|
84
|
-
// trusting authCode/sharedId.
|
|
85
74
|
function isPayPalOrigin(origin) {
|
|
86
75
|
try {
|
|
87
76
|
const host = new URL(origin).hostname.toLowerCase();
|
|
@@ -94,26 +83,31 @@ function isPayPalOrigin(origin) {
|
|
|
94
83
|
return false;
|
|
95
84
|
}
|
|
96
85
|
}
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
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.
|
|
101
90
|
function extractAuth(data) {
|
|
102
91
|
if (!data)
|
|
103
92
|
return {};
|
|
104
93
|
if (typeof data === "object") {
|
|
105
94
|
const obj = data;
|
|
106
|
-
const authCode = obj.authCode ?? obj.auth_code ?? obj.authcode;
|
|
95
|
+
const authCode = obj.authCode ?? obj.auth_code ?? obj.authcode ?? obj.code;
|
|
107
96
|
const sharedId = obj.sharedId ?? obj.shared_id ?? obj.sharedid;
|
|
108
97
|
if (authCode && sharedId) {
|
|
109
98
|
return { authCode: String(authCode), sharedId: String(sharedId) };
|
|
110
99
|
}
|
|
111
|
-
for (const key of ["data", "payload", "message", "detail", "body"]) {
|
|
100
|
+
for (const key of ["data", "payload", "message", "detail", "body", "params"]) {
|
|
112
101
|
if (obj[key] && typeof obj[key] === "object") {
|
|
113
102
|
const inner = extractAuth(obj[key]);
|
|
114
103
|
if (inner.authCode && inner.sharedId)
|
|
115
104
|
return inner;
|
|
116
105
|
}
|
|
106
|
+
if (typeof obj[key] === "string") {
|
|
107
|
+
const inner = extractAuth(obj[key]);
|
|
108
|
+
if (inner.authCode && inner.sharedId)
|
|
109
|
+
return inner;
|
|
110
|
+
}
|
|
117
111
|
}
|
|
118
112
|
return {};
|
|
119
113
|
}
|
|
@@ -121,7 +115,7 @@ function extractAuth(data) {
|
|
|
121
115
|
const s = data.trim();
|
|
122
116
|
if (!s)
|
|
123
117
|
return {};
|
|
124
|
-
if (s.startsWith("{")) {
|
|
118
|
+
if (s.startsWith("{") || s.startsWith("[")) {
|
|
125
119
|
try {
|
|
126
120
|
return extractAuth(JSON.parse(s));
|
|
127
121
|
}
|
|
@@ -146,9 +140,6 @@ function extractAuth(data) {
|
|
|
146
140
|
}
|
|
147
141
|
function PayPalConnectionPage() {
|
|
148
142
|
const [env, setEnv] = (0, react_1.useState)("live");
|
|
149
|
-
// We must not generate/fetch an onboarding link until we know which
|
|
150
|
-
// environment the server is actually on, otherwise the first fetch races the
|
|
151
|
-
// GET /environment response and can target the wrong environment.
|
|
152
143
|
const [envReady, setEnvReady] = (0, react_1.useState)(false);
|
|
153
144
|
const [connState, setConnState] = (0, react_1.useState)("loading");
|
|
154
145
|
const [error, setError] = (0, react_1.useState)(null);
|
|
@@ -164,20 +155,16 @@ function PayPalConnectionPage() {
|
|
|
164
155
|
const errorLogRef = (0, react_1.useRef)(null);
|
|
165
156
|
const runIdRef = (0, react_1.useRef)(0);
|
|
166
157
|
const currentRunId = (0, react_1.useRef)(0);
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
const envRef = (0, react_1.useRef)(env);
|
|
159
|
+
envRef.current = env;
|
|
169
160
|
const finalUrlRef = (0, react_1.useRef)(finalUrl);
|
|
170
161
|
finalUrlRef.current = finalUrl;
|
|
171
162
|
const connStateRef = (0, react_1.useRef)(connState);
|
|
172
163
|
connStateRef.current = connState;
|
|
173
164
|
const inProgressRef = (0, react_1.useRef)(onboardingInProgress);
|
|
174
165
|
inProgressRef.current = onboardingInProgress;
|
|
175
|
-
const envRef = (0, react_1.useRef)(env);
|
|
176
|
-
envRef.current = env;
|
|
177
166
|
const popupRef = (0, react_1.useRef)(null);
|
|
178
|
-
const
|
|
179
|
-
// Guards against the two redundant completion paths (our own message listener
|
|
180
|
-
// and partner.js's onboardingCallback bridge) both POSTing the same result.
|
|
167
|
+
const pollRef = (0, react_1.useRef)(null);
|
|
181
168
|
const completedRef = (0, react_1.useRef)(false);
|
|
182
169
|
const ppBtnMeasureRef = (0, react_1.useRef)(null);
|
|
183
170
|
const [ppBtnWidth, setPpBtnWidth] = (0, react_1.useState)(null);
|
|
@@ -188,18 +175,16 @@ function PayPalConnectionPage() {
|
|
|
188
175
|
setConnState("error");
|
|
189
176
|
setError(msg);
|
|
190
177
|
}, []);
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
clearInterval(
|
|
194
|
-
|
|
178
|
+
const stopPoll = (0, react_1.useCallback)(() => {
|
|
179
|
+
if (pollRef.current) {
|
|
180
|
+
clearInterval(pollRef.current);
|
|
181
|
+
pollRef.current = null;
|
|
195
182
|
}
|
|
196
183
|
}, []);
|
|
197
|
-
// Open
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// never need an await before opening, and we never depend on winning a race
|
|
202
|
-
// with partner.js's asynchronous click binding.
|
|
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.
|
|
203
188
|
const openOnboardingPopup = (0, react_1.useCallback)((url) => {
|
|
204
189
|
const w = 450;
|
|
205
190
|
const h = 600;
|
|
@@ -225,30 +210,23 @@ function PayPalConnectionPage() {
|
|
|
225
210
|
catch {
|
|
226
211
|
/* ignore */
|
|
227
212
|
}
|
|
228
|
-
stopPopupPoll();
|
|
229
|
-
popupPollRef.current = setInterval(() => {
|
|
230
|
-
if (!popupRef.current || popupRef.current.closed) {
|
|
231
|
-
stopPopupPoll();
|
|
232
|
-
}
|
|
233
|
-
}, 1000);
|
|
234
213
|
}
|
|
235
214
|
return popup;
|
|
236
|
-
}, [
|
|
237
|
-
// Exchange
|
|
238
|
-
//
|
|
239
|
-
//
|
|
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.
|
|
240
219
|
const completeOnboarding = (0, react_1.useCallback)(async (authCode, sharedId) => {
|
|
241
220
|
if (!authCode || !sharedId)
|
|
242
221
|
return;
|
|
243
222
|
if (completedRef.current)
|
|
244
223
|
return;
|
|
245
224
|
completedRef.current = true;
|
|
225
|
+
stopPoll();
|
|
246
226
|
try {
|
|
247
227
|
window.onbeforeunload = null;
|
|
248
228
|
}
|
|
249
|
-
catch {
|
|
250
|
-
/* ignore */
|
|
251
|
-
}
|
|
229
|
+
catch { }
|
|
252
230
|
const activeEnv = envRef.current === "sandbox" ? "sandbox" : "live";
|
|
253
231
|
setOnboardingInProgress(true);
|
|
254
232
|
setConnState("loading");
|
|
@@ -265,45 +243,37 @@ function PayPalConnectionPage() {
|
|
|
265
243
|
const txt = await res.text().catch(() => "");
|
|
266
244
|
throw new Error(txt || `Onboarding exchange failed (${res.status})`);
|
|
267
245
|
}
|
|
268
|
-
// Close the popup we opened, plus any partner.js-managed flow.
|
|
269
246
|
try {
|
|
270
247
|
popupRef.current?.close();
|
|
271
248
|
}
|
|
272
|
-
catch {
|
|
273
|
-
/* ignore */
|
|
274
|
-
}
|
|
275
|
-
stopPopupPoll();
|
|
249
|
+
catch { }
|
|
276
250
|
try {
|
|
277
251
|
const close1 = window.PAYPAL?.apps?.Signup?.MiniBrowser?.closeFlow;
|
|
278
252
|
if (typeof close1 === "function")
|
|
279
253
|
close1();
|
|
280
254
|
}
|
|
281
|
-
catch {
|
|
282
|
-
/* ignore */
|
|
283
|
-
}
|
|
255
|
+
catch { }
|
|
284
256
|
try {
|
|
285
257
|
const close2 = window.PAYPAL?.apps?.Signup?.miniBrowser &&
|
|
286
258
|
window.PAYPAL.apps.Signup.miniBrowser.closeFlow;
|
|
287
259
|
if (typeof close2 === "function")
|
|
288
260
|
close2();
|
|
289
261
|
}
|
|
290
|
-
catch {
|
|
291
|
-
/* ignore */
|
|
292
|
-
}
|
|
262
|
+
catch { }
|
|
293
263
|
clearCachedUrl(activeEnv);
|
|
294
264
|
try {
|
|
295
|
-
const statusRes = await fetch(`${STATUS_ENDPOINT}?environment=${activeEnv}`, {
|
|
265
|
+
const statusRes = await fetch(`${STATUS_ENDPOINT}?environment=${activeEnv}`, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
credentials: "include",
|
|
268
|
+
});
|
|
296
269
|
const refreshedStatus = await statusRes.json().catch(() => ({}));
|
|
297
270
|
setStatusInfo(refreshedStatus || null);
|
|
298
271
|
}
|
|
299
|
-
catch {
|
|
300
|
-
/* ignore — still consider it connected below */
|
|
301
|
-
}
|
|
272
|
+
catch { }
|
|
302
273
|
setConnState("connected");
|
|
303
274
|
setShowManual(false);
|
|
304
275
|
}
|
|
305
276
|
catch (e) {
|
|
306
|
-
// Allow a retry after a failed exchange.
|
|
307
277
|
completedRef.current = false;
|
|
308
278
|
console.error(e);
|
|
309
279
|
setConnState("error");
|
|
@@ -312,30 +282,60 @@ function PayPalConnectionPage() {
|
|
|
312
282
|
finally {
|
|
313
283
|
setOnboardingInProgress(false);
|
|
314
284
|
}
|
|
315
|
-
}, [
|
|
316
|
-
|
|
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]);
|
|
323
|
+
const generateLinkAndReload = (0, react_1.useCallback)(async (targetEnv, runId) => {
|
|
317
324
|
if (initLoaderRef.current) {
|
|
318
325
|
const loaderText = initLoaderRef.current.querySelector("#loader-text");
|
|
319
326
|
if (loaderText)
|
|
320
327
|
loaderText.textContent = "Generating onboarding session...";
|
|
321
328
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
products: ["PPCP"],
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
})
|
|
333
|
-
.then((r) => {
|
|
334
|
-
if (!r.ok)
|
|
335
|
-
throw new Error(`Service returned ${r.status}`);
|
|
336
|
-
return r.json();
|
|
337
|
-
})
|
|
338
|
-
.then((data) => {
|
|
329
|
+
try {
|
|
330
|
+
const res = await fetch(SERVICE_URL, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: { "content-type": "application/json" },
|
|
333
|
+
credentials: "include",
|
|
334
|
+
body: JSON.stringify({ products: ["PPCP"], environment: targetEnv }),
|
|
335
|
+
});
|
|
336
|
+
if (!res.ok)
|
|
337
|
+
throw new Error(`Service returned ${res.status}`);
|
|
338
|
+
const data = await res.json();
|
|
339
339
|
if (runId !== currentRunId.current)
|
|
340
340
|
return;
|
|
341
341
|
const href = data?.onboarding_url;
|
|
@@ -344,17 +344,21 @@ function PayPalConnectionPage() {
|
|
|
344
344
|
return;
|
|
345
345
|
}
|
|
346
346
|
const url = href + (href.includes("?") ? "&" : "?") + "displayMode=minibrowser";
|
|
347
|
-
writeCachedUrl(targetEnv, url);
|
|
347
|
+
const persisted = writeCachedUrl(targetEnv, url);
|
|
348
|
+
if (persisted) {
|
|
349
|
+
window.location.reload();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
348
352
|
setFinalUrl(url);
|
|
349
353
|
setConnState("ready");
|
|
350
|
-
}
|
|
351
|
-
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
352
356
|
if (runId !== currentRunId.current)
|
|
353
357
|
return;
|
|
354
358
|
showError("Unable to connect to service.");
|
|
355
|
-
}
|
|
359
|
+
}
|
|
356
360
|
}, [showError]);
|
|
357
|
-
// Resolve the server
|
|
361
|
+
// Resolve the server environment first.
|
|
358
362
|
(0, react_1.useEffect)(() => {
|
|
359
363
|
let alive = true;
|
|
360
364
|
fetch(ENVIRONMENT_ENDPOINT, { method: "GET", credentials: "include" })
|
|
@@ -373,14 +377,56 @@ function PayPalConnectionPage() {
|
|
|
373
377
|
alive = false;
|
|
374
378
|
};
|
|
375
379
|
}, []);
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
380
|
+
// Page-load flow: connected? -> cached link? -> else generate + cache + reload.
|
|
381
|
+
(0, react_1.useEffect)(() => {
|
|
382
|
+
if (!envReady)
|
|
383
|
+
return;
|
|
384
|
+
currentRunId.current = ++runIdRef.current;
|
|
385
|
+
const runId = currentRunId.current;
|
|
386
|
+
let cancelled = false;
|
|
387
|
+
completedRef.current = false;
|
|
388
|
+
const run = async () => {
|
|
389
|
+
setConnState("loading");
|
|
390
|
+
setError(null);
|
|
391
|
+
setPopupBlocked(false);
|
|
392
|
+
setFinalUrl("");
|
|
393
|
+
try {
|
|
394
|
+
const r = await fetch(`${STATUS_ENDPOINT}?environment=${env}`, {
|
|
395
|
+
method: "GET",
|
|
396
|
+
credentials: "include",
|
|
397
|
+
});
|
|
398
|
+
const st = await r.json().catch(() => ({}));
|
|
399
|
+
if (cancelled || runId !== currentRunId.current)
|
|
400
|
+
return;
|
|
401
|
+
setStatusInfo(st);
|
|
402
|
+
const isConnected = st?.status === "connected" && st?.seller_client_id_present === true;
|
|
403
|
+
if (isConnected) {
|
|
404
|
+
setConnState("connected");
|
|
405
|
+
setShowManual(false);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
console.error(e);
|
|
411
|
+
}
|
|
412
|
+
if (cancelled || runId !== currentRunId.current)
|
|
413
|
+
return;
|
|
414
|
+
const cached = readCachedUrl(env);
|
|
415
|
+
if (cached) {
|
|
416
|
+
setFinalUrl(cached);
|
|
417
|
+
setConnState("ready");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
await generateLinkAndReload(env, runId);
|
|
421
|
+
};
|
|
422
|
+
run();
|
|
423
|
+
return () => {
|
|
424
|
+
cancelled = true;
|
|
425
|
+
};
|
|
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.
|
|
384
430
|
(0, react_1.useEffect)(() => {
|
|
385
431
|
if (connState !== "ready" || !finalUrl)
|
|
386
432
|
return;
|
|
@@ -398,9 +444,6 @@ function PayPalConnectionPage() {
|
|
|
398
444
|
: null;
|
|
399
445
|
if (target?.init) {
|
|
400
446
|
try {
|
|
401
|
-
// Wires partner.js's click interceptor + its postMessage->callback
|
|
402
|
-
// bridge. It will adopt the window we pre-open (same name) rather than
|
|
403
|
-
// open its own.
|
|
404
447
|
target.init();
|
|
405
448
|
}
|
|
406
449
|
catch (e) {
|
|
@@ -426,8 +469,6 @@ function PayPalConnectionPage() {
|
|
|
426
469
|
}
|
|
427
470
|
catch { }
|
|
428
471
|
};
|
|
429
|
-
// Remove any stale copy + namespace so the fresh load re-registers against
|
|
430
|
-
// the current environment (sandbox vs live partner.js differ).
|
|
431
472
|
const existingScript = document.getElementById("paypal-partner-js");
|
|
432
473
|
if (existingScript) {
|
|
433
474
|
existingScript.parentNode?.removeChild(existingScript);
|
|
@@ -446,19 +487,31 @@ function PayPalConnectionPage() {
|
|
|
446
487
|
document.body.appendChild(ppScript);
|
|
447
488
|
return () => {
|
|
448
489
|
cancelled = true;
|
|
449
|
-
if (pollTimer)
|
|
490
|
+
if (pollTimer)
|
|
450
491
|
clearTimeout(pollTimer);
|
|
451
|
-
|
|
452
|
-
if (ppScript.parentNode) {
|
|
492
|
+
if (ppScript.parentNode)
|
|
453
493
|
ppScript.parentNode.removeChild(ppScript);
|
|
454
|
-
}
|
|
455
494
|
};
|
|
456
495
|
}, [connState, finalUrl, env]);
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
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.
|
|
460
499
|
(0, react_1.useEffect)(() => {
|
|
461
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
|
+
}
|
|
462
515
|
if (!isPayPalOrigin(ev.origin))
|
|
463
516
|
return;
|
|
464
517
|
const { authCode, sharedId } = extractAuth(ev.data);
|
|
@@ -469,57 +522,7 @@ function PayPalConnectionPage() {
|
|
|
469
522
|
window.addEventListener("message", onMessage);
|
|
470
523
|
return () => window.removeEventListener("message", onMessage);
|
|
471
524
|
}, [completeOnboarding]);
|
|
472
|
-
//
|
|
473
|
-
// correct environment. runId guards against a stale response from a previous
|
|
474
|
-
// environment overwriting the current one.
|
|
475
|
-
(0, react_1.useEffect)(() => {
|
|
476
|
-
if (!envReady)
|
|
477
|
-
return;
|
|
478
|
-
currentRunId.current = ++runIdRef.current;
|
|
479
|
-
const runId = currentRunId.current;
|
|
480
|
-
let cancelled = false;
|
|
481
|
-
completedRef.current = false;
|
|
482
|
-
const run = async () => {
|
|
483
|
-
setConnState("loading");
|
|
484
|
-
setError(null);
|
|
485
|
-
setPopupBlocked(false);
|
|
486
|
-
setFinalUrl("");
|
|
487
|
-
try {
|
|
488
|
-
const r = await fetch(`${STATUS_ENDPOINT}?environment=${env}`, {
|
|
489
|
-
method: "GET",
|
|
490
|
-
credentials: "include",
|
|
491
|
-
});
|
|
492
|
-
const st = await r.json().catch(() => ({}));
|
|
493
|
-
if (cancelled || runId !== currentRunId.current)
|
|
494
|
-
return;
|
|
495
|
-
setStatusInfo(st);
|
|
496
|
-
const isConnected = st?.status === "connected" && st?.seller_client_id_present === true;
|
|
497
|
-
if (isConnected) {
|
|
498
|
-
setConnState("connected");
|
|
499
|
-
setShowManual(false);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
catch (e) {
|
|
504
|
-
console.error(e);
|
|
505
|
-
}
|
|
506
|
-
if (cancelled || runId !== currentRunId.current)
|
|
507
|
-
return;
|
|
508
|
-
const cached = readCachedUrl(env);
|
|
509
|
-
if (cached) {
|
|
510
|
-
setFinalUrl(cached);
|
|
511
|
-
setConnState("ready");
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
fetchFreshLink(runId, env);
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
run();
|
|
518
|
-
return () => {
|
|
519
|
-
cancelled = true;
|
|
520
|
-
};
|
|
521
|
-
}, [env, envReady, fetchFreshLink]);
|
|
522
|
-
// Bridge partner.js's callback into our single idempotent completion handler.
|
|
525
|
+
// SECONDARY completion path: partner.js's bridge calls this by name.
|
|
523
526
|
(0, react_1.useLayoutEffect)(() => {
|
|
524
527
|
window.onboardingCallback = (authCode, sharedId) => {
|
|
525
528
|
completeOnboarding(authCode, sharedId);
|
|
@@ -528,12 +531,39 @@ function PayPalConnectionPage() {
|
|
|
528
531
|
window.onboardingCallback = undefined;
|
|
529
532
|
};
|
|
530
533
|
}, [completeOnboarding]);
|
|
531
|
-
//
|
|
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.
|
|
532
537
|
(0, react_1.useEffect)(() => {
|
|
533
|
-
|
|
534
|
-
|
|
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();
|
|
535
560
|
};
|
|
536
|
-
|
|
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]);
|
|
537
567
|
(0, react_1.useLayoutEffect)(() => {
|
|
538
568
|
const el = ppBtnMeasureRef.current;
|
|
539
569
|
if (!el)
|
|
@@ -559,48 +589,8 @@ function PayPalConnectionPage() {
|
|
|
559
589
|
window.removeEventListener("resize", update);
|
|
560
590
|
};
|
|
561
591
|
}, [connState, env, finalUrl]);
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
// It runs on `document` in the CAPTURE phase, i.e. BEFORE the event reaches
|
|
565
|
-
// the anchor where partner.js binds its own (target-phase) handler. That
|
|
566
|
-
// ordering is the whole point:
|
|
567
|
-
// 1. We synchronously open the mini-browser popup here, inside the user
|
|
568
|
-
// gesture, so the browser always renders a real popup (never a blocked
|
|
569
|
-
// window, never a new tab).
|
|
570
|
-
// 2. We do NOT stop propagation, so partner.js's handler still runs a beat
|
|
571
|
-
// later. Since it opens with the same window name (POPUP_NAME), it adopts
|
|
572
|
-
// the window we just opened instead of asynchronously spawning its own
|
|
573
|
-
// (which the browser was blocking — "multiple popups ... lack of user
|
|
574
|
-
// activation") — and it still wires its postMessage->onboardingCallback
|
|
575
|
-
// bridge that auto-closes the popup and saves credentials.
|
|
576
|
-
// 3. We always preventDefault so the anchor's href can never open a tab.
|
|
577
|
-
//
|
|
578
|
-
// Completion is handled by partner.js's bridge AND by our own postMessage
|
|
579
|
-
// listener (see below), funnelling through one idempotent completeOnboarding.
|
|
580
|
-
(0, react_1.useEffect)(() => {
|
|
581
|
-
const onCaptureClick = (e) => {
|
|
582
|
-
const btn = paypalButtonRef.current;
|
|
583
|
-
if (!btn)
|
|
584
|
-
return;
|
|
585
|
-
const target = e.target;
|
|
586
|
-
if (!target || !btn.contains(target))
|
|
587
|
-
return;
|
|
588
|
-
e.preventDefault();
|
|
589
|
-
if (connStateRef.current !== "ready" ||
|
|
590
|
-
!finalUrlRef.current ||
|
|
591
|
-
inProgressRef.current) {
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
completedRef.current = false;
|
|
595
|
-
const popup = openOnboardingPopup(finalUrlRef.current);
|
|
596
|
-
setPopupBlocked(!popup);
|
|
597
|
-
};
|
|
598
|
-
document.addEventListener("click", onCaptureClick, true);
|
|
599
|
-
return () => document.removeEventListener("click", onCaptureClick, true);
|
|
600
|
-
}, [openOnboardingPopup]);
|
|
601
|
-
// Defensive only: the capture-phase listener above already opened the popup
|
|
602
|
-
// and prevented the default. If for some reason it didn't run, make sure the
|
|
603
|
-
// native target still can't open a tab.
|
|
592
|
+
// Defensive: the capture listener handles the click; this just guarantees the
|
|
593
|
+
// native href can never navigate.
|
|
604
594
|
const handleConnectClick = (e) => {
|
|
605
595
|
e.preventDefault();
|
|
606
596
|
};
|
|
@@ -669,8 +659,7 @@ function PayPalConnectionPage() {
|
|
|
669
659
|
clearCachedUrl(env);
|
|
670
660
|
completedRef.current = false;
|
|
671
661
|
currentRunId.current = ++runIdRef.current;
|
|
672
|
-
|
|
673
|
-
fetchFreshLink(runId, env);
|
|
662
|
+
await generateLinkAndReload(env, currentRunId.current);
|
|
674
663
|
}
|
|
675
664
|
catch (e) {
|
|
676
665
|
console.error(e);
|
|
@@ -683,15 +672,12 @@ function PayPalConnectionPage() {
|
|
|
683
672
|
};
|
|
684
673
|
const handleEnvChange = async (e) => {
|
|
685
674
|
const next = e.target.value;
|
|
675
|
+
if (next === env || onboardingInProgress)
|
|
676
|
+
return;
|
|
686
677
|
completedRef.current = false;
|
|
687
|
-
setPopupBlocked(false);
|
|
688
|
-
// Drop both cached URLs so neither environment can serve a stale link.
|
|
689
678
|
clearCachedUrl();
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
// fetches are environment-explicit, correctness no longer depends on this
|
|
693
|
-
// POST winning a race.
|
|
694
|
-
setEnv(next);
|
|
679
|
+
setPopupBlocked(false);
|
|
680
|
+
setConnState("loading");
|
|
695
681
|
try {
|
|
696
682
|
await fetch(ENVIRONMENT_ENDPOINT, {
|
|
697
683
|
method: "POST",
|
|
@@ -701,6 +687,7 @@ function PayPalConnectionPage() {
|
|
|
701
687
|
});
|
|
702
688
|
}
|
|
703
689
|
catch { }
|
|
690
|
+
setEnv(next);
|
|
704
691
|
};
|
|
705
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
|
|
706
693
|
? "Configuring connection to PayPal…"
|