@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.
- package/.medusa/server/src/admin/index.js +187 -287
- package/.medusa/server/src/admin/index.mjs +187 -287
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts +0 -1
- 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 +204 -332
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js.map +1 -1
- package/.medusa/server/src/api/admin/paypal/onboard-complete/route.d.ts +7 -0
- package/.medusa/server/src/api/admin/paypal/onboard-complete/route.d.ts.map +1 -1
- package/.medusa/server/src/api/admin/paypal/onboard-complete/route.js +7 -0
- package/.medusa/server/src/api/admin/paypal/onboard-complete/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/onboard-return/route.d.ts +25 -0
- package/.medusa/server/src/api/store/paypal/onboard-return/route.d.ts.map +1 -0
- package/.medusa/server/src/api/store/paypal/onboard-return/route.js +82 -0
- package/.medusa/server/src/api/store/paypal/onboard-return/route.js.map +1 -0
- package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/service.js +32 -2
- package/.medusa/server/src/modules/paypal/service.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/routes/settings/paypal/connection/page.tsx +230 -354
- package/src/api/admin/paypal/onboard-complete/route.ts +7 -0
- package/src/api/store/paypal/onboard-return/route.ts +88 -0
- package/src/modules/paypal/service.ts +34 -3
|
@@ -13,11 +13,16 @@ export const config = defineRouteConfig({
|
|
|
13
13
|
label: "PayPal Connection",
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Let partner.js own the entire onboarding flow: it opens PayPal's mini-browser
|
|
17
|
+
// itself (synchronously inside the click, so no new tab / no popup block), it
|
|
18
|
+
// receives PayPal's completion postMessage, and it invokes the callback named in
|
|
19
|
+
// `data-paypal-onboard-complete` with (authCode, sharedId). This is PayPal's
|
|
20
|
+
// documented integration and matches the official @easypayment WooCommerce
|
|
21
|
+
// plugin. Opening the popup ourselves (a previous attempt at fixing the "opens a
|
|
22
|
+
// new tab" issue) stopped partner.js from receiving completion, which left the
|
|
23
|
+
// popup stranded on PayPal's "isuDone" page and never saved credentials.
|
|
24
|
+
const PARTNER_JS_URL =
|
|
25
|
+
"https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js"
|
|
21
26
|
|
|
22
27
|
declare global {
|
|
23
28
|
interface Window {
|
|
@@ -26,7 +31,6 @@ declare global {
|
|
|
26
31
|
Signup?: {
|
|
27
32
|
miniBrowser?: { init?: () => void; closeFlow?: () => void }
|
|
28
33
|
MiniBrowser?: { init?: () => void; closeFlow?: () => void }
|
|
29
|
-
render?: () => void
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -34,13 +38,7 @@ declare global {
|
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
41
|
const SERVICE_URL = "/admin/paypal/onboarding-link"
|
|
39
|
-
// PayPal's conventional mini-browser window name (also the anchor target).
|
|
40
|
-
const POPUP_NAME = "PPFrame"
|
|
41
|
-
|
|
42
|
-
// The onboarding link is cached in the browser for 6 hours so it is always
|
|
43
|
-
// present for the mini-browser flow without regenerating on every load.
|
|
44
42
|
const CACHE_PREFIX = "pp_onboard_cache"
|
|
45
43
|
const CACHE_EXPIRY = 6 * 60 * 60 * 1000 // 6 hours
|
|
46
44
|
|
|
@@ -73,12 +71,11 @@ function readCachedUrl(env: Env): string | null {
|
|
|
73
71
|
return null
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
function writeCachedUrl(env: Env, url: string)
|
|
74
|
+
function writeCachedUrl(env: Env, url: string) {
|
|
77
75
|
try {
|
|
78
76
|
localStorage.setItem(cacheKeyFor(env), JSON.stringify({ url, ts: Date.now() }))
|
|
79
|
-
return readCachedUrl(env) === url
|
|
80
77
|
} catch {
|
|
81
|
-
|
|
78
|
+
/* ignore */
|
|
82
79
|
}
|
|
83
80
|
}
|
|
84
81
|
|
|
@@ -95,73 +92,6 @@ function clearCachedUrl(env?: Env) {
|
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
function isPayPalOrigin(origin: string): boolean {
|
|
99
|
-
try {
|
|
100
|
-
const host = new URL(origin).hostname.toLowerCase()
|
|
101
|
-
return (
|
|
102
|
-
host === "www.paypal.com" ||
|
|
103
|
-
host === "www.sandbox.paypal.com" ||
|
|
104
|
-
host.endsWith(".paypal.com") ||
|
|
105
|
-
host.endsWith(".paypalobjects.com")
|
|
106
|
-
)
|
|
107
|
-
} catch {
|
|
108
|
-
return false
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Defensively pull authCode/sharedId out of whatever shape PayPal posts back
|
|
113
|
-
// (object, JSON string, or query string; possibly nested). We only act when both
|
|
114
|
-
// are present, which ignores the many unrelated intermediate messages PayPal
|
|
115
|
-
// emits.
|
|
116
|
-
function extractAuth(data: unknown): { authCode?: string; sharedId?: string } {
|
|
117
|
-
if (!data) return {}
|
|
118
|
-
|
|
119
|
-
if (typeof data === "object") {
|
|
120
|
-
const obj = data as Record<string, any>
|
|
121
|
-
const authCode = obj.authCode ?? obj.auth_code ?? obj.authcode ?? obj.code
|
|
122
|
-
const sharedId = obj.sharedId ?? obj.shared_id ?? obj.sharedid
|
|
123
|
-
if (authCode && sharedId) {
|
|
124
|
-
return { authCode: String(authCode), sharedId: String(sharedId) }
|
|
125
|
-
}
|
|
126
|
-
for (const key of ["data", "payload", "message", "detail", "body", "params"]) {
|
|
127
|
-
if (obj[key] && typeof obj[key] === "object") {
|
|
128
|
-
const inner = extractAuth(obj[key])
|
|
129
|
-
if (inner.authCode && inner.sharedId) return inner
|
|
130
|
-
}
|
|
131
|
-
if (typeof obj[key] === "string") {
|
|
132
|
-
const inner = extractAuth(obj[key])
|
|
133
|
-
if (inner.authCode && inner.sharedId) return inner
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return {}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (typeof data === "string") {
|
|
140
|
-
const s = data.trim()
|
|
141
|
-
if (!s) return {}
|
|
142
|
-
if (s.startsWith("{") || s.startsWith("[")) {
|
|
143
|
-
try {
|
|
144
|
-
return extractAuth(JSON.parse(s))
|
|
145
|
-
} catch {
|
|
146
|
-
/* not JSON */
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (/auth_?code/i.test(s) && /shared_?id/i.test(s)) {
|
|
150
|
-
try {
|
|
151
|
-
const sp = new URLSearchParams(s.replace(/^[?#]/, ""))
|
|
152
|
-
const authCode = sp.get("authCode") || sp.get("auth_code") || undefined
|
|
153
|
-
const sharedId = sp.get("sharedId") || sp.get("shared_id") || undefined
|
|
154
|
-
if (authCode && sharedId) return { authCode, sharedId }
|
|
155
|
-
} catch {
|
|
156
|
-
/* not a query string */
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
95
|
export default function PayPalConnectionPage() {
|
|
166
96
|
const [env, setEnv] = useState<Env>("live")
|
|
167
97
|
const [envReady, setEnvReady] = useState(false)
|
|
@@ -170,7 +100,6 @@ export default function PayPalConnectionPage() {
|
|
|
170
100
|
"loading" | "ready" | "connected" | "error"
|
|
171
101
|
>("loading")
|
|
172
102
|
const [error, setError] = useState<string | null>(null)
|
|
173
|
-
const [popupBlocked, setPopupBlocked] = useState(false)
|
|
174
103
|
const [finalUrl, setFinalUrl] = useState<string>("")
|
|
175
104
|
const [showManual, setShowManual] = useState(false)
|
|
176
105
|
const [clientId, setClientId] = useState("")
|
|
@@ -188,19 +117,15 @@ export default function PayPalConnectionPage() {
|
|
|
188
117
|
const errorLogRef = useRef<HTMLDivElement>(null)
|
|
189
118
|
const runIdRef = useRef(0)
|
|
190
119
|
const currentRunId = useRef(0)
|
|
120
|
+
const completedRef = useRef(false)
|
|
191
121
|
|
|
122
|
+
// Long-lived listeners/timers read the current env through a ref so they never
|
|
123
|
+
// capture a stale value from the render that registered them.
|
|
192
124
|
const envRef = useRef<Env>(env)
|
|
193
125
|
envRef.current = env
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
connStateRef.current = connState
|
|
198
|
-
const inProgressRef = useRef(onboardingInProgress)
|
|
199
|
-
inProgressRef.current = onboardingInProgress
|
|
200
|
-
|
|
201
|
-
const popupRef = useRef<Window | null>(null)
|
|
202
|
-
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
203
|
-
const completedRef = useRef(false)
|
|
126
|
+
|
|
127
|
+
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
128
|
+
const pollAttemptsRef = useRef(0)
|
|
204
129
|
|
|
205
130
|
const ppBtnMeasureRef = useRef<HTMLAnchorElement | null>(null)
|
|
206
131
|
const [ppBtnWidth, setPpBtnWidth] = useState<number | null>(null)
|
|
@@ -214,57 +139,89 @@ export default function PayPalConnectionPage() {
|
|
|
214
139
|
setError(msg)
|
|
215
140
|
}, [])
|
|
216
141
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
142
|
+
// Close PayPal's mini-browser via partner.js's API (works under both the
|
|
143
|
+
// `miniBrowser` and `MiniBrowser` casings PayPal has shipped over time).
|
|
144
|
+
const closeMiniBrowser = useCallback(() => {
|
|
145
|
+
try {
|
|
146
|
+
const close1 = window.PAYPAL?.apps?.Signup?.MiniBrowser?.closeFlow
|
|
147
|
+
if (typeof close1 === "function") close1()
|
|
148
|
+
} catch {}
|
|
149
|
+
try {
|
|
150
|
+
const close2 = window.PAYPAL?.apps?.Signup?.miniBrowser?.closeFlow
|
|
151
|
+
if (typeof close2 === "function") close2()
|
|
152
|
+
} catch {}
|
|
222
153
|
}, [])
|
|
223
154
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const openOnboardingPopup = useCallback((url: string): Window | null => {
|
|
229
|
-
const w = 450
|
|
230
|
-
const h = 600
|
|
231
|
-
const baseLeft =
|
|
232
|
-
typeof window.screenX === "number" ? window.screenX : (window as any).screenLeft || 0
|
|
233
|
-
const baseTop =
|
|
234
|
-
typeof window.screenY === "number" ? window.screenY : (window as any).screenTop || 0
|
|
235
|
-
const outerW = window.outerWidth || document.documentElement.clientWidth || 1024
|
|
236
|
-
const outerH = window.outerHeight || document.documentElement.clientHeight || 768
|
|
237
|
-
const left = Math.round(baseLeft + Math.max(0, (outerW - w) / 2))
|
|
238
|
-
const top = Math.round(baseTop + Math.max(0, (outerH - h) / 2))
|
|
239
|
-
const features = `popup=yes,width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`
|
|
240
|
-
|
|
241
|
-
let popup: Window | null = null
|
|
242
|
-
try {
|
|
243
|
-
popup = window.open(url, POPUP_NAME, features)
|
|
244
|
-
} catch {
|
|
245
|
-
popup = null
|
|
155
|
+
const stopStatusPolling = useCallback(() => {
|
|
156
|
+
if (pollTimerRef.current) {
|
|
157
|
+
clearTimeout(pollTimerRef.current)
|
|
158
|
+
pollTimerRef.current = null
|
|
246
159
|
}
|
|
247
|
-
|
|
248
|
-
|
|
160
|
+
pollAttemptsRef.current = 0
|
|
161
|
+
}, [])
|
|
162
|
+
|
|
163
|
+
// Fetch status for an env and, if the seller credentials are present, flip the
|
|
164
|
+
// UI to "connected" and close the PayPal popup. Returns true once connected.
|
|
165
|
+
const refreshStatusAndMaybeConnect = useCallback(
|
|
166
|
+
async (activeEnv: Env): Promise<boolean> => {
|
|
249
167
|
try {
|
|
250
|
-
|
|
168
|
+
const res = await fetch(`${STATUS_ENDPOINT}?environment=${activeEnv}`, {
|
|
169
|
+
method: "GET",
|
|
170
|
+
credentials: "include",
|
|
171
|
+
})
|
|
172
|
+
const st = await res.json().catch(() => ({}))
|
|
173
|
+
const connected =
|
|
174
|
+
st?.status === "connected" && st?.seller_client_id_present === true
|
|
175
|
+
if (connected) {
|
|
176
|
+
completedRef.current = true
|
|
177
|
+
setStatusInfo(st)
|
|
178
|
+
setConnState("connected")
|
|
179
|
+
setShowManual(false)
|
|
180
|
+
setOnboardingInProgress(false)
|
|
181
|
+
clearCachedUrl(activeEnv)
|
|
182
|
+
closeMiniBrowser()
|
|
183
|
+
stopStatusPolling()
|
|
184
|
+
}
|
|
185
|
+
return connected
|
|
251
186
|
} catch {
|
|
252
|
-
|
|
187
|
+
return false
|
|
253
188
|
}
|
|
189
|
+
},
|
|
190
|
+
[closeMiniBrowser, stopStatusPolling]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// While a connect attempt is in flight, the seller may complete onboarding via
|
|
194
|
+
// a path that never reaches the opener (e.g. partner.js fails to fire its
|
|
195
|
+
// callback, or the popup's completion message is blocked). The return_url
|
|
196
|
+
// bridge still saves credentials server-side, so poll status as the reliable
|
|
197
|
+
// fallback: the moment credentials land, this flips to "connected" and closes
|
|
198
|
+
// the stranded popup.
|
|
199
|
+
const startStatusPolling = useCallback(() => {
|
|
200
|
+
stopStatusPolling()
|
|
201
|
+
const MAX_ATTEMPTS = 100 // ~5 min at 3s intervals
|
|
202
|
+
const tick = async () => {
|
|
203
|
+
pollAttemptsRef.current += 1
|
|
204
|
+
const connected = await refreshStatusAndMaybeConnect(envRef.current)
|
|
205
|
+
if (connected || completedRef.current) return
|
|
206
|
+
if (pollAttemptsRef.current >= MAX_ATTEMPTS) {
|
|
207
|
+
stopStatusPolling()
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
pollTimerRef.current = setTimeout(tick, 3000)
|
|
254
211
|
}
|
|
255
|
-
|
|
256
|
-
}, [])
|
|
212
|
+
pollTimerRef.current = setTimeout(tick, 3000)
|
|
213
|
+
}, [refreshStatusAndMaybeConnect, stopStatusPolling])
|
|
257
214
|
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
215
|
+
// Shared completion: exchange authCode/sharedId for seller credentials, then
|
|
216
|
+
// close the popup and refresh status. Invoked by partner.js's onboardingCallback
|
|
217
|
+
// and by the return_url bridge's postMessage. Guarded + server-idempotent so
|
|
218
|
+
// redundant invocations are harmless.
|
|
261
219
|
const completeOnboarding = useCallback(
|
|
262
220
|
async (authCode: string, sharedId: string) => {
|
|
263
221
|
if (!authCode || !sharedId) return
|
|
264
222
|
if (completedRef.current) return
|
|
265
223
|
completedRef.current = true
|
|
266
224
|
|
|
267
|
-
stopPoll()
|
|
268
225
|
try {
|
|
269
226
|
window.onbeforeunload = null
|
|
270
227
|
} catch {}
|
|
@@ -274,7 +231,6 @@ export default function PayPalConnectionPage() {
|
|
|
274
231
|
setOnboardingInProgress(true)
|
|
275
232
|
setConnState("loading")
|
|
276
233
|
setError(null)
|
|
277
|
-
setPopupBlocked(false)
|
|
278
234
|
|
|
279
235
|
try {
|
|
280
236
|
const res = await fetch(ONBOARDING_COMPLETE_ENDPOINT, {
|
|
@@ -289,82 +245,114 @@ export default function PayPalConnectionPage() {
|
|
|
289
245
|
throw new Error(txt || `Onboarding exchange failed (${res.status})`)
|
|
290
246
|
}
|
|
291
247
|
|
|
292
|
-
|
|
293
|
-
popupRef.current?.close()
|
|
294
|
-
} catch {}
|
|
295
|
-
try {
|
|
296
|
-
const close1 = window.PAYPAL?.apps?.Signup?.MiniBrowser?.closeFlow
|
|
297
|
-
if (typeof close1 === "function") close1()
|
|
298
|
-
} catch {}
|
|
299
|
-
try {
|
|
300
|
-
const close2 =
|
|
301
|
-
window.PAYPAL?.apps?.Signup?.miniBrowser &&
|
|
302
|
-
(window.PAYPAL.apps.Signup.miniBrowser as any).closeFlow
|
|
303
|
-
if (typeof close2 === "function") close2()
|
|
304
|
-
} catch {}
|
|
305
|
-
|
|
248
|
+
closeMiniBrowser()
|
|
306
249
|
clearCachedUrl(activeEnv)
|
|
307
250
|
|
|
308
251
|
try {
|
|
309
|
-
const statusRes = await fetch(
|
|
310
|
-
|
|
311
|
-
credentials: "include"
|
|
312
|
-
|
|
252
|
+
const statusRes = await fetch(
|
|
253
|
+
`${STATUS_ENDPOINT}?environment=${activeEnv}`,
|
|
254
|
+
{ method: "GET", credentials: "include" }
|
|
255
|
+
)
|
|
313
256
|
const refreshedStatus = await statusRes.json().catch(() => ({}))
|
|
314
257
|
setStatusInfo(refreshedStatus || null)
|
|
315
258
|
} catch {}
|
|
316
259
|
|
|
317
260
|
setConnState("connected")
|
|
318
261
|
setShowManual(false)
|
|
262
|
+
stopStatusPolling()
|
|
319
263
|
} catch (e: any) {
|
|
320
264
|
completedRef.current = false
|
|
321
265
|
console.error(e)
|
|
266
|
+
// The bridge may have already saved credentials server-side; if so the
|
|
267
|
+
// poll will surface "connected". Don't strand the user on an error.
|
|
322
268
|
setConnState("error")
|
|
323
269
|
setError(e?.message || "Exchange failed while saving credentials.")
|
|
324
270
|
} finally {
|
|
325
271
|
setOnboardingInProgress(false)
|
|
326
272
|
}
|
|
327
273
|
},
|
|
328
|
-
[
|
|
274
|
+
[closeMiniBrowser, stopStatusPolling]
|
|
329
275
|
)
|
|
330
276
|
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
ticks += 1
|
|
339
|
-
if (completedRef.current || ticks > 150 /* ~5 min */) {
|
|
340
|
-
stopPoll()
|
|
341
|
-
return
|
|
342
|
-
}
|
|
277
|
+
// Bind partner.js to the connect button once it has a real onboarding URL.
|
|
278
|
+
const initPartner = useCallback(() => {
|
|
279
|
+
const signup = window.PAYPAL?.apps?.Signup
|
|
280
|
+
const init =
|
|
281
|
+
(signup?.miniBrowser && signup.miniBrowser.init) ||
|
|
282
|
+
(signup?.MiniBrowser && signup.MiniBrowser.init)
|
|
283
|
+
if (typeof init === "function") {
|
|
343
284
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
285
|
+
init()
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.error("[paypal] partner.js init failed:", e)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}, [])
|
|
291
|
+
|
|
292
|
+
// Load partner.js once for the lifetime of the page.
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const existingScript = document.getElementById("paypal-partner-js")
|
|
295
|
+
if (existingScript) return
|
|
296
|
+
|
|
297
|
+
const preloadHref = PARTNER_JS_URL
|
|
298
|
+
let preloadLink: HTMLLinkElement | null = null
|
|
299
|
+
if (!document.head.querySelector(`link[rel="preload"][href="${preloadHref}"]`)) {
|
|
300
|
+
preloadLink = document.createElement("link")
|
|
301
|
+
preloadLink.rel = "preload"
|
|
302
|
+
preloadLink.href = preloadHref
|
|
303
|
+
preloadLink.as = "script"
|
|
304
|
+
document.head.appendChild(preloadLink)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const ppScript = document.createElement("script")
|
|
308
|
+
ppScript.id = "paypal-partner-js"
|
|
309
|
+
ppScript.src = preloadHref
|
|
310
|
+
ppScript.async = true
|
|
311
|
+
document.head.appendChild(ppScript)
|
|
312
|
+
|
|
313
|
+
return () => {
|
|
314
|
+
if (preloadLink?.parentNode) preloadLink.parentNode.removeChild(preloadLink)
|
|
315
|
+
if (ppScript.parentNode) ppScript.parentNode.removeChild(ppScript)
|
|
316
|
+
}
|
|
317
|
+
}, [])
|
|
318
|
+
|
|
319
|
+
// Put the (cached or freshly generated) onboarding URL on the button, then wait
|
|
320
|
+
// for partner.js to be present before showing the button + initializing it. The
|
|
321
|
+
// button is only revealed once partner.js is ready, which is what guarantees
|
|
322
|
+
// the first click opens the mini-browser popup (never a new tab).
|
|
323
|
+
const activatePayPal = useCallback(
|
|
324
|
+
(url: string, runId: number) => {
|
|
325
|
+
if (paypalButtonRef.current) {
|
|
326
|
+
paypalButtonRef.current.href = url
|
|
327
|
+
}
|
|
328
|
+
setFinalUrl(url)
|
|
329
|
+
|
|
330
|
+
let attempts = 0
|
|
331
|
+
const MAX_ATTEMPTS = 200 // ~10s
|
|
332
|
+
|
|
333
|
+
const tryInit = () => {
|
|
334
|
+
if (runId !== currentRunId.current) return
|
|
335
|
+
if (window.PAYPAL?.apps?.Signup) {
|
|
336
|
+
initPartner()
|
|
337
|
+
setConnState("ready")
|
|
338
|
+
return
|
|
360
339
|
}
|
|
361
|
-
|
|
362
|
-
|
|
340
|
+
attempts++
|
|
341
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
342
|
+
showError(
|
|
343
|
+
"PayPal partner script failed to load. Please refresh and try again."
|
|
344
|
+
)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
setTimeout(tryInit, 50)
|
|
363
348
|
}
|
|
364
|
-
}, 2000)
|
|
365
|
-
}, [stopPoll])
|
|
366
349
|
|
|
367
|
-
|
|
350
|
+
tryInit()
|
|
351
|
+
},
|
|
352
|
+
[initPartner, showError]
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
const fetchFreshLink = useCallback(
|
|
368
356
|
async (targetEnv: Env, runId: number) => {
|
|
369
357
|
if (initLoaderRef.current) {
|
|
370
358
|
const loaderText = initLoaderRef.current.querySelector("#loader-text")
|
|
@@ -393,20 +381,14 @@ export default function PayPalConnectionPage() {
|
|
|
393
381
|
const url =
|
|
394
382
|
href + (href.includes("?") ? "&" : "?") + "displayMode=minibrowser"
|
|
395
383
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
window.location.reload()
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
setFinalUrl(url)
|
|
403
|
-
setConnState("ready")
|
|
384
|
+
writeCachedUrl(targetEnv, url)
|
|
385
|
+
activatePayPal(url, runId)
|
|
404
386
|
} catch {
|
|
405
387
|
if (runId !== currentRunId.current) return
|
|
406
388
|
showError("Unable to connect to service.")
|
|
407
389
|
}
|
|
408
390
|
},
|
|
409
|
-
[showError]
|
|
391
|
+
[activatePayPal, showError]
|
|
410
392
|
)
|
|
411
393
|
|
|
412
394
|
// Resolve the server environment first.
|
|
@@ -427,7 +409,7 @@ export default function PayPalConnectionPage() {
|
|
|
427
409
|
}
|
|
428
410
|
}, [])
|
|
429
411
|
|
|
430
|
-
// Page-load flow: connected? -> cached link? -> else generate
|
|
412
|
+
// Page-load flow: connected? -> cached link? -> else generate a fresh link.
|
|
431
413
|
useEffect(() => {
|
|
432
414
|
if (!envReady) return
|
|
433
415
|
|
|
@@ -440,7 +422,6 @@ export default function PayPalConnectionPage() {
|
|
|
440
422
|
const run = async () => {
|
|
441
423
|
setConnState("loading")
|
|
442
424
|
setError(null)
|
|
443
|
-
setPopupBlocked(false)
|
|
444
425
|
setFinalUrl("")
|
|
445
426
|
|
|
446
427
|
try {
|
|
@@ -470,12 +451,11 @@ export default function PayPalConnectionPage() {
|
|
|
470
451
|
|
|
471
452
|
const cached = readCachedUrl(env)
|
|
472
453
|
if (cached) {
|
|
473
|
-
|
|
474
|
-
setConnState("ready")
|
|
454
|
+
activatePayPal(cached, runId)
|
|
475
455
|
return
|
|
476
456
|
}
|
|
477
457
|
|
|
478
|
-
await
|
|
458
|
+
await fetchFreshLink(env, runId)
|
|
479
459
|
}
|
|
480
460
|
|
|
481
461
|
run()
|
|
@@ -483,163 +463,51 @@ export default function PayPalConnectionPage() {
|
|
|
483
463
|
return () => {
|
|
484
464
|
cancelled = true
|
|
485
465
|
}
|
|
486
|
-
}, [env, envReady,
|
|
487
|
-
|
|
488
|
-
// Load partner.js + init for its postMessage->onboardingCallback bridge (a
|
|
489
|
-
// secondary completion path next to our own message listener). We open the
|
|
490
|
-
// popup ourselves, so partner.js is NOT relied on to open the window.
|
|
491
|
-
useEffect(() => {
|
|
492
|
-
if (connState !== "ready" || !finalUrl) return
|
|
493
|
-
|
|
494
|
-
const scriptUrl = PARTNER_JS_URLS[env]
|
|
495
|
-
let cancelled = false
|
|
496
|
-
let pollTimer: ReturnType<typeof setTimeout> | undefined
|
|
497
|
-
|
|
498
|
-
const initPartner = (attempt = 0) => {
|
|
499
|
-
if (cancelled) return
|
|
500
|
-
|
|
501
|
-
const signup = window.PAYPAL?.apps?.Signup
|
|
502
|
-
const target =
|
|
503
|
-
signup?.miniBrowser && typeof signup.miniBrowser.init === "function"
|
|
504
|
-
? signup.miniBrowser
|
|
505
|
-
: signup?.MiniBrowser && typeof signup.MiniBrowser.init === "function"
|
|
506
|
-
? signup.MiniBrowser
|
|
507
|
-
: null
|
|
508
|
-
|
|
509
|
-
if (target?.init) {
|
|
510
|
-
try {
|
|
511
|
-
target.init()
|
|
512
|
-
} catch (e) {
|
|
513
|
-
console.error("[paypal] partner.js init failed:", e)
|
|
514
|
-
}
|
|
515
|
-
return
|
|
516
|
-
}
|
|
517
|
-
if (typeof signup?.render === "function") {
|
|
518
|
-
try {
|
|
519
|
-
signup.render()
|
|
520
|
-
} catch (e) {
|
|
521
|
-
console.error("[paypal] partner.js render failed:", e)
|
|
522
|
-
}
|
|
523
|
-
return
|
|
524
|
-
}
|
|
525
|
-
if (attempt < 40) {
|
|
526
|
-
pollTimer = setTimeout(() => initPartner(attempt + 1), 50)
|
|
527
|
-
return
|
|
528
|
-
}
|
|
529
|
-
try {
|
|
530
|
-
document.dispatchEvent(new Event("DOMContentLoaded"))
|
|
531
|
-
} catch {}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const existingScript = document.getElementById("paypal-partner-js")
|
|
535
|
-
if (existingScript) {
|
|
536
|
-
existingScript.parentNode?.removeChild(existingScript)
|
|
537
|
-
}
|
|
538
|
-
if (window.PAYPAL?.apps?.Signup) {
|
|
539
|
-
try {
|
|
540
|
-
delete (window.PAYPAL.apps as any).Signup
|
|
541
|
-
} catch {}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const ppScript = document.createElement("script")
|
|
545
|
-
ppScript.id = "paypal-partner-js"
|
|
546
|
-
ppScript.src = scriptUrl
|
|
547
|
-
ppScript.async = true
|
|
548
|
-
ppScript.onload = () => initPartner()
|
|
549
|
-
document.body.appendChild(ppScript)
|
|
550
|
-
|
|
551
|
-
return () => {
|
|
552
|
-
cancelled = true
|
|
553
|
-
if (pollTimer) clearTimeout(pollTimer)
|
|
554
|
-
if (ppScript.parentNode) ppScript.parentNode.removeChild(ppScript)
|
|
555
|
-
}
|
|
556
|
-
}, [connState, finalUrl, env])
|
|
557
|
-
|
|
558
|
-
// PRIMARY completion path: receive PayPal's onboarding postMessage ourselves.
|
|
559
|
-
// The diagnostic log lets us confirm the exact origin/shape PayPal sends in a
|
|
560
|
-
// real run if a tweak to the parser is ever needed.
|
|
561
|
-
useEffect(() => {
|
|
562
|
-
const onMessage = (ev: MessageEvent) => {
|
|
563
|
-
let serialized = ""
|
|
564
|
-
try {
|
|
565
|
-
serialized =
|
|
566
|
-
typeof ev.data === "string" ? ev.data : JSON.stringify(ev.data)
|
|
567
|
-
} catch {
|
|
568
|
-
serialized = String(ev.data)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const looksRelevant =
|
|
572
|
-
isPayPalOrigin(ev.origin) ||
|
|
573
|
-
/auth_?code|shared_?id|onboard|merchantId/i.test(serialized || "")
|
|
574
|
-
|
|
575
|
-
if (looksRelevant) {
|
|
576
|
-
// eslint-disable-next-line no-console
|
|
577
|
-
console.info(
|
|
578
|
-
"[PayPal onboarding] message from",
|
|
579
|
-
ev.origin,
|
|
580
|
-
"·",
|
|
581
|
-
(serialized || "").slice(0, 500)
|
|
582
|
-
)
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (!isPayPalOrigin(ev.origin)) return
|
|
586
|
-
const { authCode, sharedId } = extractAuth(ev.data)
|
|
587
|
-
if (authCode && sharedId) {
|
|
588
|
-
completeOnboarding(authCode, sharedId)
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
window.addEventListener("message", onMessage)
|
|
592
|
-
return () => window.removeEventListener("message", onMessage)
|
|
593
|
-
}, [completeOnboarding])
|
|
466
|
+
}, [env, envReady, fetchFreshLink, activatePayPal])
|
|
594
467
|
|
|
595
|
-
//
|
|
468
|
+
// partner.js invokes this by name (data-paypal-onboard-complete) once PayPal
|
|
469
|
+
// returns the seller's authCode + sharedId.
|
|
596
470
|
useLayoutEffect(() => {
|
|
597
471
|
window.onboardingCallback = (authCode: string, sharedId: string) => {
|
|
598
|
-
completeOnboarding(authCode, sharedId)
|
|
472
|
+
void completeOnboarding(authCode, sharedId)
|
|
599
473
|
}
|
|
474
|
+
|
|
600
475
|
return () => {
|
|
601
476
|
window.onboardingCallback = undefined
|
|
602
477
|
}
|
|
603
478
|
}, [completeOnboarding])
|
|
604
479
|
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
480
|
+
// The return_url bridge (/store/paypal/onboard-return), which runs inside the
|
|
481
|
+
// popup after PayPal redirects to it, posts its params back here. The bridge has
|
|
482
|
+
// already exchanged credentials server-side, so we just refresh status to flip
|
|
483
|
+
// to "connected" and close the popup. If for some reason credentials aren't yet
|
|
484
|
+
// saved but PayPal forwarded an authCode/sharedId, complete it from here too.
|
|
608
485
|
useEffect(() => {
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (!
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
completedRef.current = false
|
|
627
|
-
const popup = openOnboardingPopup(finalUrlRef.current)
|
|
628
|
-
if (!popup) {
|
|
629
|
-
setPopupBlocked(true)
|
|
630
|
-
return
|
|
631
|
-
}
|
|
632
|
-
setPopupBlocked(false)
|
|
633
|
-
startStatusPoll()
|
|
486
|
+
const onMessage = (event: MessageEvent) => {
|
|
487
|
+
const data = event?.data as
|
|
488
|
+
| { source?: string; params?: Record<string, string> }
|
|
489
|
+
| undefined
|
|
490
|
+
if (!data || data.source !== "paypal-onboarding-return") return
|
|
491
|
+
|
|
492
|
+
const params = data.params || {}
|
|
493
|
+
const authCode = params.authCode || params.auth_code
|
|
494
|
+
const sharedId = params.sharedId || params.shared_id
|
|
495
|
+
const activeEnv = envRef.current
|
|
496
|
+
|
|
497
|
+
void (async () => {
|
|
498
|
+
const connected = await refreshStatusAndMaybeConnect(activeEnv)
|
|
499
|
+
if (!connected && authCode && sharedId) {
|
|
500
|
+
await completeOnboarding(authCode, sharedId)
|
|
501
|
+
}
|
|
502
|
+
})()
|
|
634
503
|
}
|
|
635
504
|
|
|
636
|
-
|
|
637
|
-
return () =>
|
|
638
|
-
}, [
|
|
505
|
+
window.addEventListener("message", onMessage)
|
|
506
|
+
return () => window.removeEventListener("message", onMessage)
|
|
507
|
+
}, [completeOnboarding, refreshStatusAndMaybeConnect])
|
|
639
508
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}, [stopPoll])
|
|
509
|
+
// Always stop polling when the component unmounts.
|
|
510
|
+
useEffect(() => stopStatusPolling, [stopStatusPolling])
|
|
643
511
|
|
|
644
512
|
useLayoutEffect(() => {
|
|
645
513
|
const el = ppBtnMeasureRef.current
|
|
@@ -666,10 +534,17 @@ export default function PayPalConnectionPage() {
|
|
|
666
534
|
}
|
|
667
535
|
}, [connState, env, finalUrl])
|
|
668
536
|
|
|
669
|
-
//
|
|
670
|
-
//
|
|
537
|
+
// Only block the click while we are not ready; when ready, let the native click
|
|
538
|
+
// through so partner.js can open the mini-browser. Start polling status so we
|
|
539
|
+
// detect completion (and close the popup) even if no completion message reaches
|
|
540
|
+
// this page.
|
|
671
541
|
const handleConnectClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
672
|
-
|
|
542
|
+
if (connState !== "ready" || !finalUrl || onboardingInProgress) {
|
|
543
|
+
e.preventDefault()
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
completedRef.current = false
|
|
547
|
+
startStatusPolling()
|
|
673
548
|
}
|
|
674
549
|
|
|
675
550
|
const handleSaveManual = async () => {
|
|
@@ -719,6 +594,7 @@ export default function PayPalConnectionPage() {
|
|
|
719
594
|
if (onboardingInProgress) return
|
|
720
595
|
if (!window.confirm("Disconnect PayPal for this environment?")) return
|
|
721
596
|
|
|
597
|
+
stopStatusPolling()
|
|
722
598
|
setOnboardingInProgress(true)
|
|
723
599
|
setConnState("loading")
|
|
724
600
|
setError(null)
|
|
@@ -743,7 +619,7 @@ export default function PayPalConnectionPage() {
|
|
|
743
619
|
completedRef.current = false
|
|
744
620
|
|
|
745
621
|
currentRunId.current = ++runIdRef.current
|
|
746
|
-
await
|
|
622
|
+
await fetchFreshLink(env, currentRunId.current)
|
|
747
623
|
} catch (e: any) {
|
|
748
624
|
console.error(e)
|
|
749
625
|
setConnState("error")
|
|
@@ -757,9 +633,9 @@ export default function PayPalConnectionPage() {
|
|
|
757
633
|
const next = e.target.value as Env
|
|
758
634
|
if (next === env || onboardingInProgress) return
|
|
759
635
|
|
|
636
|
+
stopStatusPolling()
|
|
760
637
|
completedRef.current = false
|
|
761
638
|
clearCachedUrl()
|
|
762
|
-
setPopupBlocked(false)
|
|
763
639
|
setConnState("loading")
|
|
764
640
|
|
|
765
641
|
try {
|
|
@@ -809,6 +685,14 @@ export default function PayPalConnectionPage() {
|
|
|
809
685
|
<div>
|
|
810
686
|
<div className="text-sm text-green-600 bg-green-50 p-3 rounded border border-green-200">
|
|
811
687
|
✅ Successfully connected to PayPal!
|
|
688
|
+
<a
|
|
689
|
+
data-paypal-button="true"
|
|
690
|
+
data-paypal-onboard-complete="onboardingCallback"
|
|
691
|
+
href="#"
|
|
692
|
+
style={{ display: "none" }}
|
|
693
|
+
>
|
|
694
|
+
PayPal
|
|
695
|
+
</a>
|
|
812
696
|
</div>
|
|
813
697
|
<div className="mt-3 rounded-md border border-ui-border-base bg-ui-bg-subtle p-3 text-xs text-ui-fg-subtle">
|
|
814
698
|
<div className="font-medium text-ui-fg-base">
|
|
@@ -856,7 +740,6 @@ export default function PayPalConnectionPage() {
|
|
|
856
740
|
ppBtnMeasureRef.current = node
|
|
857
741
|
}}
|
|
858
742
|
id="paypal-button"
|
|
859
|
-
target={POPUP_NAME}
|
|
860
743
|
data-paypal-button="true"
|
|
861
744
|
href={finalUrl || "#"}
|
|
862
745
|
data-paypal-onboard-complete="onboardingCallback"
|
|
@@ -871,13 +754,6 @@ export default function PayPalConnectionPage() {
|
|
|
871
754
|
Connect to PayPal
|
|
872
755
|
</a>
|
|
873
756
|
|
|
874
|
-
{popupBlocked && (
|
|
875
|
-
<div className="mt-3 text-left text-xs bg-amber-50 text-amber-700 p-3 border border-amber-200 rounded">
|
|
876
|
-
Your browser blocked the PayPal popup. Please allow popups
|
|
877
|
-
for this site, then click “Connect to PayPal” again.
|
|
878
|
-
</div>
|
|
879
|
-
)}
|
|
880
|
-
|
|
881
757
|
<div
|
|
882
758
|
className="mt-2"
|
|
883
759
|
style={{
|