@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
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
2
|
import type PayPalModuleService from "../../../../modules/paypal/service"
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The PayPal onboarding `return_url` (the page the mini-browser popup lands on)
|
|
6
|
+
* is the PUBLIC route `GET /store/paypal/onboard-return` — `/admin/*` routes
|
|
7
|
+
* require an auth token that a top-level popup navigation cannot carry, which
|
|
8
|
+
* would leave the popup stuck on a 401 page. This admin GET is therefore not part
|
|
9
|
+
* of the browser flow; it only documents the contract for the POST below.
|
|
10
|
+
*/
|
|
4
11
|
export async function GET(_req: MedusaRequest, res: MedusaResponse) {
|
|
5
12
|
return res.status(405).json({
|
|
6
13
|
message:
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import type PayPalModuleService from "../../../../modules/paypal/service"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public PayPal onboarding `return_url`.
|
|
6
|
+
*
|
|
7
|
+
* When a seller finishes onboarding, PayPal redirects the mini-browser popup to
|
|
8
|
+
* this URL as a plain top-level navigation (no auth header / no publishable key),
|
|
9
|
+
* which is why it must live under the public `/store/paypal/*` namespace rather
|
|
10
|
+
* than `/admin/*`. Leaving the popup on a raw JSON / 401 / 405 page is exactly
|
|
11
|
+
* what made the popup appear to "never close".
|
|
12
|
+
*
|
|
13
|
+
* This route is the authoritative, partner.js-independent completion path. It:
|
|
14
|
+
* 1. exchanges PayPal's `authCode` + `sharedId` for seller credentials
|
|
15
|
+
* server-side (idempotent — safe even if partner.js's onboardingCallback or
|
|
16
|
+
* admin status polling also completes the exchange),
|
|
17
|
+
* 2. relays whatever query params PayPal appended back to the opener (the
|
|
18
|
+
* Medusa Admin connection page) via postMessage so it can refresh
|
|
19
|
+
* immediately, and
|
|
20
|
+
* 3. closes the popup.
|
|
21
|
+
*
|
|
22
|
+
* Because the exchange no longer depends on the opener receiving an in-flight
|
|
23
|
+
* postMessage, onboarding completes (and the admin page flips to "connected" via
|
|
24
|
+
* its status poll) even when partner.js never fires its callback.
|
|
25
|
+
*/
|
|
26
|
+
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
27
|
+
const query = (req.query || {}) as Record<string, unknown>
|
|
28
|
+
const params: Record<string, string> = {}
|
|
29
|
+
for (const [k, v] of Object.entries(query)) {
|
|
30
|
+
if (v == null) continue
|
|
31
|
+
params[k] = Array.isArray(v) ? String(v[0]) : String(v)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// PayPal appends the seller's authorization code + shared id to the return_url
|
|
35
|
+
// (plus the env we pinned when generating the link). When present, complete the
|
|
36
|
+
// exchange right here so credentials are saved even if no other path runs.
|
|
37
|
+
const authCode = params.authCode || params.auth_code
|
|
38
|
+
const sharedId = params.sharedId || params.shared_id
|
|
39
|
+
const env =
|
|
40
|
+
params.env === "sandbox" || params.env === "live" ? params.env : undefined
|
|
41
|
+
|
|
42
|
+
if (authCode && sharedId) {
|
|
43
|
+
try {
|
|
44
|
+
const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
|
|
45
|
+
await paypal.exchangeAndSaveSellerCredentials({ authCode, sharedId, env })
|
|
46
|
+
} catch (e: unknown) {
|
|
47
|
+
// Don't break the popup close on failure — relay the params so the opener
|
|
48
|
+
// (partner.js / admin polling) can still attempt to complete.
|
|
49
|
+
console.error(
|
|
50
|
+
"[PayPal] onboard-return exchange failed:",
|
|
51
|
+
e instanceof Error ? e.message : e
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// JSON-encode then escape the characters that could break out of the inline
|
|
57
|
+
// <script> string/tag (including the U+2028/U+2029 separators that are legal in
|
|
58
|
+
// JSON but illegal in a JS string literal), so the relayed params embed safely.
|
|
59
|
+
const safePayload = JSON.stringify(params)
|
|
60
|
+
.replace(/</g, "\\u003c")
|
|
61
|
+
.replace(/>/g, "\\u003e")
|
|
62
|
+
.replace(/&/g, "\\u0026")
|
|
63
|
+
.replace(/[\u2028\u2029]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"))
|
|
64
|
+
|
|
65
|
+
const html = `<!doctype html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head><meta charset="utf-8" /><title>PayPal onboarding complete</title></head>
|
|
68
|
+
<body style="font-family: system-ui, sans-serif; padding: 24px; text-align: center; color: #1f2937;">
|
|
69
|
+
<p>PayPal onboarding complete. You can close this window.</p>
|
|
70
|
+
<script>
|
|
71
|
+
(function () {
|
|
72
|
+
try {
|
|
73
|
+
var message = { source: "paypal-onboarding-return", params: ${safePayload} };
|
|
74
|
+
if (window.opener && !window.opener.closed) {
|
|
75
|
+
try { window.opener.postMessage(message, "*"); } catch (e) {}
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
// Give the opener a moment to react, then close the popup.
|
|
79
|
+
setTimeout(function () { try { window.close(); } catch (e) {} }, 600);
|
|
80
|
+
})();
|
|
81
|
+
</script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>`
|
|
84
|
+
|
|
85
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8")
|
|
86
|
+
res.setHeader("Cache-Control", "no-store")
|
|
87
|
+
return res.status(200).send(html)
|
|
88
|
+
}
|
|
@@ -495,13 +495,20 @@ class PayPalModuleService extends MedusaService({
|
|
|
495
495
|
|
|
496
496
|
async createOnboardingLink(input?: { email?: string; products?: string[]; env?: Environment }) {
|
|
497
497
|
const { onboarding } = await this.ensureSettingsDefaults()
|
|
498
|
-
const return_url = `${String(onboarding.backend_url || "").replace(/\/$/, "")}/admin/paypal/onboard-complete`
|
|
499
498
|
// Honor an explicit environment from the caller so the generated link can't
|
|
500
499
|
// depend on a racing "set environment" request having landed first.
|
|
501
500
|
const env =
|
|
502
501
|
input?.env === "sandbox" || input?.env === "live"
|
|
503
502
|
? input.env
|
|
504
503
|
: await this.getCurrentEnvironment()
|
|
504
|
+
// The popup lands here after onboarding, as a plain top-level navigation with
|
|
505
|
+
// no auth token — so it must be the PUBLIC store bridge route, not an
|
|
506
|
+
// /admin/* route (which would 401 and leave the popup stuck). The bridge
|
|
507
|
+
// exchanges the auth code for seller credentials server-side, relays the
|
|
508
|
+
// result to the opener, and closes the popup. We pin the environment in the
|
|
509
|
+
// URL so the bridge saves credentials under the correct env even though
|
|
510
|
+
// PayPal's redirect carries no auth/session.
|
|
511
|
+
const return_url = `${String(onboarding.backend_url || "").replace(/\/$/, "")}/store/paypal/onboard-return?env=${env}`
|
|
505
512
|
const partner_merchant_id = await this.getPartnerMerchantId(env)
|
|
506
513
|
|
|
507
514
|
const email = (input?.email || "").trim()
|
|
@@ -649,9 +656,23 @@ class PayPalModuleService extends MedusaService({
|
|
|
649
656
|
sharedId: string
|
|
650
657
|
env?: "sandbox" | "live"
|
|
651
658
|
}) {
|
|
652
|
-
await this.saveOnboardCallback({ authCode: input.authCode, sharedId: input.sharedId })
|
|
653
|
-
|
|
654
659
|
const env = (input.env || (await this.getCurrentEnvironment())) as Environment
|
|
660
|
+
|
|
661
|
+
// Onboarding completion can now arrive via several redundant paths
|
|
662
|
+
// (partner.js's onboardingCallback in the opener, the return_url bridge that
|
|
663
|
+
// runs inside the popup, and admin status polling). PayPal's authorization
|
|
664
|
+
// code is single-use, so a second exchange of the same code would fail with
|
|
665
|
+
// invalid_grant. If we already exchanged this exact code and hold seller
|
|
666
|
+
// credentials for this environment, treat the duplicate as a success no-op.
|
|
667
|
+
const existingRow = await this.getCurrentRow()
|
|
668
|
+
if (existingRow && existingRow.auth_code === input.authCode) {
|
|
669
|
+
const existingCreds = this.getEnvCreds(existingRow, env)
|
|
670
|
+
if (existingCreds.clientId && existingCreds.clientSecret) {
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
await this.saveOnboardCallback({ authCode: input.authCode, sharedId: input.sharedId })
|
|
655
676
|
const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
|
|
656
677
|
|
|
657
678
|
const { onboarding } = await this.ensureSettingsDefaults()
|
|
@@ -686,6 +707,16 @@ class PayPalModuleService extends MedusaService({
|
|
|
686
707
|
}
|
|
687
708
|
|
|
688
709
|
if (!tokenRes.ok) {
|
|
710
|
+
// Two redundant completion paths can race to exchange the same single-use
|
|
711
|
+
// code; the loser gets invalid_grant. If credentials were saved in the
|
|
712
|
+
// meantime (the other path won), the onboarding actually succeeded.
|
|
713
|
+
const racedRow = await this.getCurrentRow()
|
|
714
|
+
if (racedRow) {
|
|
715
|
+
const racedCreds = this.getEnvCreds(racedRow, env)
|
|
716
|
+
if (racedCreds.clientId && racedCreds.clientSecret) {
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
}
|
|
689
720
|
throw new Error(
|
|
690
721
|
`PayPal authorization_code token exchange failed (${tokenRes.status}): ${tokenText || JSON.stringify(tokenJson)}`
|
|
691
722
|
)
|