@paytree/medusa-payment-paytree 0.1.0
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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +55 -0
- package/dist/modules/paytree/index.d.ts +36 -0
- package/dist/modules/paytree/index.js +13 -0
- package/dist/modules/paytree/migrations/Migration20260617000000.d.ts +5 -0
- package/dist/modules/paytree/migrations/Migration20260617000000.js +54 -0
- package/dist/modules/paytree/migrations/Migration20260618000000.d.ts +5 -0
- package/dist/modules/paytree/migrations/Migration20260618000000.js +19 -0
- package/dist/modules/paytree/models/paytree-event-log.d.ts +25 -0
- package/dist/modules/paytree/models/paytree-event-log.js +29 -0
- package/dist/modules/paytree/models/paytree-setting.d.ts +23 -0
- package/dist/modules/paytree/models/paytree-setting.js +27 -0
- package/dist/modules/paytree/service.d.ts +60 -0
- package/dist/modules/paytree/service.js +47 -0
- package/dist/modules/paytree-payment/index.d.ts +8 -0
- package/dist/modules/paytree-payment/index.js +16 -0
- package/dist/modules/paytree-payment/lib/amount.d.ts +23 -0
- package/dist/modules/paytree-payment/lib/amount.js +40 -0
- package/dist/modules/paytree-payment/lib/audit.d.ts +26 -0
- package/dist/modules/paytree-payment/lib/audit.js +51 -0
- package/dist/modules/paytree-payment/lib/client.d.ts +28 -0
- package/dist/modules/paytree-payment/lib/client.js +81 -0
- package/dist/modules/paytree-payment/lib/constants.d.ts +54 -0
- package/dist/modules/paytree-payment/lib/constants.js +55 -0
- package/dist/modules/paytree-payment/lib/logger.d.ts +27 -0
- package/dist/modules/paytree-payment/lib/logger.js +73 -0
- package/dist/modules/paytree-payment/lib/methods.d.ts +69 -0
- package/dist/modules/paytree-payment/lib/methods.js +68 -0
- package/dist/modules/paytree-payment/lib/redact.d.ts +16 -0
- package/dist/modules/paytree-payment/lib/redact.js +60 -0
- package/dist/modules/paytree-payment/lib/settings-reader.d.ts +24 -0
- package/dist/modules/paytree-payment/lib/settings-reader.js +74 -0
- package/dist/modules/paytree-payment/lib/status-map.d.ts +26 -0
- package/dist/modules/paytree-payment/lib/status-map.js +66 -0
- package/dist/modules/paytree-payment/lib/types.d.ts +144 -0
- package/dist/modules/paytree-payment/lib/types.js +2 -0
- package/dist/modules/paytree-payment/service.d.ts +36 -0
- package/dist/modules/paytree-payment/service.js +369 -0
- package/docs/api-reference.md +121 -0
- package/docs/architecture.md +97 -0
- package/docs/configuration.md +62 -0
- package/docs/getting-started.md +111 -0
- package/docs/storefront-integration.md +85 -0
- package/integration/README.md +46 -0
- package/integration/admin/routes/paytree/api.ts +16 -0
- package/integration/admin/routes/paytree/method-icons.tsx +106 -0
- package/integration/admin/routes/paytree/page.tsx +18 -0
- package/integration/admin/routes/paytree/payments/page.tsx +240 -0
- package/integration/admin/routes/paytree/settings/page.tsx +214 -0
- package/integration/api/admin/paytree/events/route.ts +35 -0
- package/integration/api/admin/paytree/settings/route.ts +86 -0
- package/integration/api/hooks/paytree/route.ts +195 -0
- package/integration/api/store/paytree/methods/route.ts +28 -0
- package/package.json +57 -0
- package/storefront/README.md +25 -0
- package/storefront/components/payment-button.tsx +361 -0
- package/storefront/components/payment.tsx +356 -0
- package/storefront/components/paytree-method-icon.tsx +106 -0
- package/storefront/lib/paytree.ts +23 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
This guide takes you from install to a first successful Paytree payment.
|
|
4
|
+
|
|
5
|
+
## 1. Prerequisites
|
|
6
|
+
|
|
7
|
+
- A running Medusa `>= 2.13` backend (and, for the full flow, a Next.js storefront).
|
|
8
|
+
- A Paytree account with an API key.
|
|
9
|
+
- The public URL of your backend (for local dev, a tunnel such as ngrok/cloudflared
|
|
10
|
+
so Paytree can reach `/hooks/paytree`).
|
|
11
|
+
|
|
12
|
+
## 2. Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @paytree/medusa-payment-paytree
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 3. Environment
|
|
19
|
+
|
|
20
|
+
In `backend/.env`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
PAYTREE_API_KEY=sk_live_or_test_key
|
|
24
|
+
PAYTREE_BASE_URL=https://api.paytree.tech
|
|
25
|
+
BACKEND_URL=https://<your-public-backend-url>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 4. Register in `medusa-config.js`
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
modules: [
|
|
32
|
+
// Data module: settings + audit-log tables (owns the migrations).
|
|
33
|
+
{ resolve: "@paytree/medusa-payment-paytree/dist/modules/paytree" },
|
|
34
|
+
// Payment module with the Paytree provider (the package's default export).
|
|
35
|
+
{
|
|
36
|
+
key: Modules.PAYMENT,
|
|
37
|
+
resolve: "@medusajs/payment",
|
|
38
|
+
options: {
|
|
39
|
+
providers: [
|
|
40
|
+
{
|
|
41
|
+
resolve: "@paytree/medusa-payment-paytree",
|
|
42
|
+
id: "paytree",
|
|
43
|
+
options: {
|
|
44
|
+
apiKey: process.env.PAYTREE_API_KEY,
|
|
45
|
+
backendUrl: process.env.BACKEND_URL,
|
|
46
|
+
baseUrl: process.env.PAYTREE_BASE_URL,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 5. Copy in the API routes and admin UI
|
|
56
|
+
|
|
57
|
+
Medusa loads API routes and admin extensions from *your app's* `src/`, so copy
|
|
58
|
+
them from the package's `integration/` folder into your backend:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cp -R node_modules/@paytree/medusa-payment-paytree/integration/api/* backend/src/api/
|
|
62
|
+
cp -R node_modules/@paytree/medusa-payment-paytree/integration/admin/* backend/src/admin/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See [`../integration/README.md`](../integration/README.md) for the file map.
|
|
66
|
+
|
|
67
|
+
## 6. Migrate the database
|
|
68
|
+
|
|
69
|
+
The data module owns two tables (`paytree_setting`, `paytree_event_log`):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx medusa db:migrate
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 7. Enable Paytree for a region
|
|
76
|
+
|
|
77
|
+
In the admin: **Settings → Regions → (a region) → Payment providers**, enable
|
|
78
|
+
**Paytree**. This is what makes it appear as an option at checkout.
|
|
79
|
+
|
|
80
|
+
## 8. Curate methods (optional)
|
|
81
|
+
|
|
82
|
+
Open the admin **Paytree → Settings** page. `card` is enabled by default. Toggle
|
|
83
|
+
the methods you want to offer and optionally override their labels. Only enabled
|
|
84
|
+
methods are returned by `GET /store/paytree/methods` and shown at checkout.
|
|
85
|
+
|
|
86
|
+
## 9. Storefront
|
|
87
|
+
|
|
88
|
+
Wire up the checkout UI using the reference components in
|
|
89
|
+
[`../storefront`](../storefront) — see
|
|
90
|
+
[storefront-integration.md](./storefront-integration.md).
|
|
91
|
+
|
|
92
|
+
## 10. Make a test payment
|
|
93
|
+
|
|
94
|
+
1. Add a product to the cart and reach the payment step.
|
|
95
|
+
2. Select **Paytree**, pick a method, and continue.
|
|
96
|
+
3. Click **Pay with Paytree** — you're redirected to the hosted page.
|
|
97
|
+
4. Complete payment; you're redirected back to the return URL and the order is
|
|
98
|
+
placed.
|
|
99
|
+
5. Check **Paytree → Payments** in the admin: you should see `initiate_payment`,
|
|
100
|
+
`webhook_received`, and `webhook_processed` events for the session.
|
|
101
|
+
|
|
102
|
+
## Troubleshooting
|
|
103
|
+
|
|
104
|
+
- **"Paytree requires the storefront to pass these fields…"** — the payment
|
|
105
|
+
session data is missing `customer`, `address.country`, or `session`
|
|
106
|
+
(IP/user-agent). Make sure your storefront assembles these server-side (the
|
|
107
|
+
reference `initiatePaymentSession` does this).
|
|
108
|
+
- **Callback never arrives** — `BACKEND_URL` must be publicly reachable by
|
|
109
|
+
Paytree; for local dev use a tunnel and set `BACKEND_URL` to the tunnel URL.
|
|
110
|
+
- **401/403 from Paytree** — check `PAYTREE_API_KEY`. The base URL must be
|
|
111
|
+
`https` (enforced) so the key is never sent in cleartext.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Storefront integration
|
|
2
|
+
|
|
3
|
+
Paytree is a **hosted-redirect** provider: the storefront collects the method
|
|
4
|
+
choice, the backend creates a payment intent and returns a `payment_link`, and
|
|
5
|
+
the customer is sent to Paytree's hosted page. These reference components target
|
|
6
|
+
the official [Medusa Next.js starter](https://github.com/medusajs/nextjs-starter-medusa)
|
|
7
|
+
layout.
|
|
8
|
+
|
|
9
|
+
The files live under [`../storefront`](../storefront). Copy each to the matching
|
|
10
|
+
path in your storefront and apply the shared-file edits below.
|
|
11
|
+
|
|
12
|
+
## Files to add
|
|
13
|
+
|
|
14
|
+
| Reference file | Destination in the Next.js starter |
|
|
15
|
+
| -------------- | ---------------------------------- |
|
|
16
|
+
| `storefront/lib/paytree.ts` | `src/lib/data/paytree.ts` |
|
|
17
|
+
| `storefront/components/paytree-method-icon.tsx` | `src/modules/checkout/components/paytree-method-icon/index.tsx` |
|
|
18
|
+
| `storefront/components/payment.tsx` | `src/modules/checkout/components/payment/index.tsx` (replaces the starter's) |
|
|
19
|
+
| `storefront/components/payment-button.tsx` | `src/modules/checkout/components/payment-button/index.tsx` (replaces the starter's) |
|
|
20
|
+
|
|
21
|
+
## Shared files to edit
|
|
22
|
+
|
|
23
|
+
### 1. `src/lib/constants.tsx`
|
|
24
|
+
|
|
25
|
+
Register the provider so the picker shows a title/icon and helpers can detect it:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
export const paymentInfoMap = {
|
|
29
|
+
// ...existing entries
|
|
30
|
+
pp_paytree_paytree: { title: "Paytree", icon: <CreditCard /> },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const isPaytree = (providerId?: string) =>
|
|
34
|
+
providerId?.startsWith("pp_paytree")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. `src/lib/data/cart.ts`
|
|
38
|
+
|
|
39
|
+
Paytree needs the customer, address, and request session (IP + user-agent) in
|
|
40
|
+
the payment session `data`. Assemble it **server-side** in
|
|
41
|
+
`initiatePaymentSession` before calling the SDK:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export async function initiatePaymentSession(cart, data) {
|
|
45
|
+
let payload = data
|
|
46
|
+
if (data.provider_id.includes("paytree")) {
|
|
47
|
+
payload = { ...data, data: await buildPaytreeSessionData(cart, data.data) }
|
|
48
|
+
}
|
|
49
|
+
return sdk.store.payment.initiatePaymentSession(
|
|
50
|
+
cart, payload, {}, await getAuthHeaders()
|
|
51
|
+
).then((resp) => { revalidateTag("cart"); return resp })
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`buildPaytreeSessionData` reads `next/headers` for `x-forwarded-for` /
|
|
56
|
+
`user-agent`, pulls the address/email from the cart, and sets `return_url` /
|
|
57
|
+
`cancel_url`. See the full implementation in the reference `payment.tsx` notes
|
|
58
|
+
and `storefront/lib/paytree.ts`, or copy from the demo's `cart.ts`.
|
|
59
|
+
|
|
60
|
+
## How the checkout flows
|
|
61
|
+
|
|
62
|
+
1. **Method step** (`payment.tsx`) — when Paytree is selected it calls
|
|
63
|
+
`listPaytreeMethods()` (`GET /store/paytree/methods`) and renders a radio list
|
|
64
|
+
using `PaytreeMethodIcon`. Paytree re-initiates the session on every submit so
|
|
65
|
+
the freshest method choice produces a fresh `payment_link`.
|
|
66
|
+
2. **Review / place order** (`payment-button.tsx`) — `PaytreePaymentButton` reads
|
|
67
|
+
`session.data.payment_link` and redirects the browser to it.
|
|
68
|
+
3. **Return** — Paytree redirects to your `return_url`. You need a return page at
|
|
69
|
+
the path you configured (the reference `buildPaytreeSessionData` uses
|
|
70
|
+
`/{countryCode}/checkout/paytree-return`). That page should finalize the cart
|
|
71
|
+
(`placeOrder()`) and show confirmation. Meanwhile the backend `/hooks/paytree`
|
|
72
|
+
callback reconciles the authoritative status independently, so the order is
|
|
73
|
+
captured even if the shopper closes the tab before the redirect completes.
|
|
74
|
+
|
|
75
|
+
## Data loader
|
|
76
|
+
|
|
77
|
+
`storefront/lib/paytree.ts` exports `listPaytreeMethods()`, a server action that
|
|
78
|
+
fetches enabled methods and returns `[]` on failure so checkout degrades
|
|
79
|
+
gracefully if Paytree isn't configured.
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
- The reference components keep the starter's import aliases (`@lib/...`,
|
|
84
|
+
`@modules/...`). If your storefront uses different aliases, adjust the imports.
|
|
85
|
+
- The method picker only lists methods enabled in **Paytree → Settings**.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copy-in integration files
|
|
2
|
+
|
|
3
|
+
Medusa discovers **API routes** and **admin extensions** from your own app's
|
|
4
|
+
`src/` directory (and bundles the admin with Vite), so these can't be loaded
|
|
5
|
+
from an installed module the way the payment provider and data module are.
|
|
6
|
+
Copy them into your backend once, then they behave like first-party code.
|
|
7
|
+
|
|
8
|
+
They import the shared helpers (`PAYTREE_MODULE`, `PaytreeClient`,
|
|
9
|
+
`resolveMethods`, etc.) from the installed package
|
|
10
|
+
`@paytree/medusa-payment-paytree`, so no relative paths need fixing.
|
|
11
|
+
|
|
12
|
+
## File map
|
|
13
|
+
|
|
14
|
+
| From (this package) | To (your backend) |
|
|
15
|
+
| ------------------- | ----------------- |
|
|
16
|
+
| `integration/api/hooks/paytree/route.ts` | `src/api/hooks/paytree/route.ts` |
|
|
17
|
+
| `integration/api/store/paytree/methods/route.ts` | `src/api/store/paytree/methods/route.ts` |
|
|
18
|
+
| `integration/api/admin/paytree/settings/route.ts` | `src/api/admin/paytree/settings/route.ts` |
|
|
19
|
+
| `integration/api/admin/paytree/events/route.ts` | `src/api/admin/paytree/events/route.ts` |
|
|
20
|
+
| `integration/admin/routes/paytree/**` | `src/admin/routes/paytree/**` |
|
|
21
|
+
|
|
22
|
+
## Quick copy
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cp -R node_modules/@paytree/medusa-payment-paytree/integration/api/* backend/src/api/
|
|
26
|
+
cp -R node_modules/@paytree/medusa-payment-paytree/integration/admin/* backend/src/admin/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What each piece does
|
|
30
|
+
|
|
31
|
+
- **`api/hooks/paytree`** — the GET callback Paytree calls when a payment status
|
|
32
|
+
changes. It re-fetches the authoritative status and drives the payment/refund
|
|
33
|
+
workflows. This is the live reconciliation path; without it, payments won't
|
|
34
|
+
finalize server-side.
|
|
35
|
+
- **`api/store/paytree/methods`** — public list of admin-enabled methods for the
|
|
36
|
+
storefront picker.
|
|
37
|
+
- **`api/admin/paytree/settings` + `events`** — admin settings read/write and the
|
|
38
|
+
audit-log query.
|
|
39
|
+
- **`admin/routes/paytree`** — the "Paytree" admin dashboard pages.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
The admin pages use `@medusajs/ui`, `@medusajs/icons`, and `react-router-dom`,
|
|
44
|
+
which a standard Medusa backend already provides. The routes require the data
|
|
45
|
+
module to be registered (see the main README) so `req.scope.resolve("paytree")`
|
|
46
|
+
works.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small fetch helper shared by the Paytree admin pages. Calls the admin API
|
|
3
|
+
* under /admin/paytree and surfaces a useful error message on failure.
|
|
4
|
+
*/
|
|
5
|
+
export async function api(path: string, init?: RequestInit) {
|
|
6
|
+
const res = await fetch(`/admin/paytree/${path}`, {
|
|
7
|
+
credentials: "include",
|
|
8
|
+
headers: { "Content-Type": "application/json" },
|
|
9
|
+
...init,
|
|
10
|
+
})
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
const detail = await res.json().catch(() => ({}))
|
|
13
|
+
throw new Error(detail?.message || `Request failed (${res.status})`)
|
|
14
|
+
}
|
|
15
|
+
return res.json()
|
|
16
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compact SVG icons for each Paytree method, shown in the admin methods list
|
|
5
|
+
* and intended to mirror what the storefront renders. Generic rails (card,
|
|
6
|
+
* bank transfer, crypto) use line glyphs; brand methods use a colored badge
|
|
7
|
+
* with a short brand mark so the set stays consistent and recognizable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Badge = { bg: string; fg: string; text: string; size?: number }
|
|
11
|
+
|
|
12
|
+
const BADGES: Record<string, Badge> = {
|
|
13
|
+
paypal: { bg: "#003087", fg: "#ffffff", text: "PP" },
|
|
14
|
+
klarna: { bg: "#FFB3C7", fg: "#0A0A0A", text: "K." },
|
|
15
|
+
ideal: { bg: "#CD0067", fg: "#ffffff", text: "iD" },
|
|
16
|
+
bancontact: { bg: "#005498", fg: "#FFD800", text: "BC" },
|
|
17
|
+
trustly: { bg: "#0EE06E", fg: "#0A0A0A", text: "T" },
|
|
18
|
+
kakaopay: { bg: "#FFCD00", fg: "#3C1E1E", text: "pay", size: 7 },
|
|
19
|
+
p24: { bg: "#D3132B", fg: "#ffffff", text: "P24", size: 7 },
|
|
20
|
+
blik: { bg: "#000000", fg: "#E61E2A", text: "blik", size: 6 },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BadgeIcon = ({ bg, fg, text, size = 9 }: Badge) => (
|
|
24
|
+
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">
|
|
25
|
+
<rect x="1" y="4" width="22" height="16" rx="3" fill={bg} />
|
|
26
|
+
<text
|
|
27
|
+
x="12"
|
|
28
|
+
y="12.5"
|
|
29
|
+
fill={fg}
|
|
30
|
+
fontSize={size}
|
|
31
|
+
fontWeight="700"
|
|
32
|
+
fontFamily="Inter, system-ui, sans-serif"
|
|
33
|
+
textAnchor="middle"
|
|
34
|
+
dominantBaseline="central"
|
|
35
|
+
>
|
|
36
|
+
{text}
|
|
37
|
+
</text>
|
|
38
|
+
</svg>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const CardIcon = () => (
|
|
42
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
43
|
+
<rect
|
|
44
|
+
x="2"
|
|
45
|
+
y="5"
|
|
46
|
+
width="20"
|
|
47
|
+
height="14"
|
|
48
|
+
rx="2.5"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
strokeWidth="1.6"
|
|
51
|
+
/>
|
|
52
|
+
<path d="M2 9.5h20" stroke="currentColor" strokeWidth="1.6" />
|
|
53
|
+
<path
|
|
54
|
+
d="M5.5 15.5h4"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
strokeWidth="1.6"
|
|
57
|
+
strokeLinecap="round"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const BankIcon = () => (
|
|
63
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
64
|
+
<path
|
|
65
|
+
d="M12 3 3 7.5h18L12 3Z"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth="1.5"
|
|
68
|
+
strokeLinejoin="round"
|
|
69
|
+
/>
|
|
70
|
+
<path
|
|
71
|
+
d="M5 10v6M9.3 10v6M14.7 10v6M19 10v6M3 19.5h18"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
strokeWidth="1.5"
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
/>
|
|
76
|
+
</svg>
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const CryptoIcon = () => (
|
|
80
|
+
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">
|
|
81
|
+
<circle cx="12" cy="12" r="9.2" fill="#F7931A" />
|
|
82
|
+
<text
|
|
83
|
+
x="12"
|
|
84
|
+
y="12.5"
|
|
85
|
+
fill="#ffffff"
|
|
86
|
+
fontSize="11"
|
|
87
|
+
fontWeight="700"
|
|
88
|
+
fontFamily="Inter, system-ui, sans-serif"
|
|
89
|
+
textAnchor="middle"
|
|
90
|
+
dominantBaseline="central"
|
|
91
|
+
>
|
|
92
|
+
₿
|
|
93
|
+
</text>
|
|
94
|
+
</svg>
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export const MethodIcon = ({ method }: { method: string }) => {
|
|
98
|
+
if (method === "card") return <CardIcon />
|
|
99
|
+
if (method === "wire") return <BankIcon />
|
|
100
|
+
if (method === "crypto") return <CryptoIcon />
|
|
101
|
+
const badge = BADGES[method]
|
|
102
|
+
if (badge) return <BadgeIcon {...badge} />
|
|
103
|
+
return <CardIcon />
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default MethodIcon
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
|
2
|
+
import { CreditCard } from "@medusajs/icons"
|
|
3
|
+
import { Navigate } from "react-router-dom"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The "Paytree" sidebar group. Its children — Payments and Settings — are the
|
|
7
|
+
* real pages; landing on the group itself sends you to Payments.
|
|
8
|
+
*/
|
|
9
|
+
const PaytreePage = () => {
|
|
10
|
+
return <Navigate to="/paytree/payments" replace />
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const config = defineRouteConfig({
|
|
14
|
+
label: "Paytree",
|
|
15
|
+
icon: CreditCard,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export default PaytreePage
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
|
2
|
+
import { CurrencyDollar, TriangleRightMini, TriangleDownMini } from "@medusajs/icons"
|
|
3
|
+
import { Badge, Button, Container, Heading, Input, Table, Text, toast } from "@medusajs/ui"
|
|
4
|
+
import { Fragment, useEffect, useState } from "react"
|
|
5
|
+
import { api } from "../api"
|
|
6
|
+
|
|
7
|
+
type EventRow = {
|
|
8
|
+
id: string
|
|
9
|
+
event: string
|
|
10
|
+
session_id: string | null
|
|
11
|
+
paytree_id: string | null
|
|
12
|
+
status: string | null
|
|
13
|
+
amount: string | null
|
|
14
|
+
currency: string | null
|
|
15
|
+
error: string | null
|
|
16
|
+
detail: Record<string, unknown> | null
|
|
17
|
+
created_at: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Pretty-print a JSON-ish value; detail may arrive as an object or a string. */
|
|
21
|
+
function formatDetail(value: unknown): string {
|
|
22
|
+
if (value == null) {
|
|
23
|
+
return ""
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.stringify(JSON.parse(value), null, 2)
|
|
28
|
+
} catch {
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify(value, null, 2)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const STATUS_COLOR: Record<string, "green" | "red" | "orange" | "grey"> = {
|
|
36
|
+
success: "green",
|
|
37
|
+
authorized: "green",
|
|
38
|
+
captured: "green",
|
|
39
|
+
refunded: "orange",
|
|
40
|
+
partial_refunded: "orange",
|
|
41
|
+
failed: "red",
|
|
42
|
+
canceled: "red",
|
|
43
|
+
expired: "red",
|
|
44
|
+
pending: "grey",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PaymentsPage = () => {
|
|
48
|
+
const [events, setEvents] = useState<EventRow[]>([])
|
|
49
|
+
const [count, setCount] = useState(0)
|
|
50
|
+
const [offset, setOffset] = useState(0)
|
|
51
|
+
const [sessionFilter, setSessionFilter] = useState("")
|
|
52
|
+
const [loading, setLoading] = useState(true)
|
|
53
|
+
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
54
|
+
const limit = 20
|
|
55
|
+
|
|
56
|
+
const toggleExpanded = (id: string) =>
|
|
57
|
+
setExpandedId((current) => (current === id ? null : id))
|
|
58
|
+
|
|
59
|
+
const load = (nextOffset = 0, session = sessionFilter) => {
|
|
60
|
+
setLoading(true)
|
|
61
|
+
const params = new URLSearchParams({
|
|
62
|
+
limit: String(limit),
|
|
63
|
+
offset: String(nextOffset),
|
|
64
|
+
})
|
|
65
|
+
if (session) {
|
|
66
|
+
params.set("session_id", session)
|
|
67
|
+
}
|
|
68
|
+
api(`events?${params.toString()}`)
|
|
69
|
+
.then((d) => {
|
|
70
|
+
setEvents(d.events)
|
|
71
|
+
setCount(d.count)
|
|
72
|
+
setOffset(nextOffset)
|
|
73
|
+
})
|
|
74
|
+
.catch((e) => toast.error(e.message))
|
|
75
|
+
.finally(() => setLoading(false))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
load(0)
|
|
80
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Container className="divide-y p-0">
|
|
85
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
86
|
+
<Heading level="h2">Payment activity</Heading>
|
|
87
|
+
<div className="flex gap-2">
|
|
88
|
+
<Input
|
|
89
|
+
placeholder="Filter by session id"
|
|
90
|
+
value={sessionFilter}
|
|
91
|
+
onChange={(e) => setSessionFilter(e.target.value)}
|
|
92
|
+
/>
|
|
93
|
+
<Button variant="secondary" onClick={() => load(0)}>
|
|
94
|
+
Search
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="px-6 py-2">
|
|
100
|
+
<Table>
|
|
101
|
+
<Table.Header>
|
|
102
|
+
<Table.Row>
|
|
103
|
+
<Table.HeaderCell className="w-8" />
|
|
104
|
+
<Table.HeaderCell>When</Table.HeaderCell>
|
|
105
|
+
<Table.HeaderCell>Event</Table.HeaderCell>
|
|
106
|
+
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
107
|
+
<Table.HeaderCell>Amount</Table.HeaderCell>
|
|
108
|
+
<Table.HeaderCell>Session</Table.HeaderCell>
|
|
109
|
+
</Table.Row>
|
|
110
|
+
</Table.Header>
|
|
111
|
+
<Table.Body>
|
|
112
|
+
{events.map((row) => {
|
|
113
|
+
const hasDetails = Boolean(
|
|
114
|
+
row.error ||
|
|
115
|
+
(row.detail && Object.keys(row.detail).length > 0)
|
|
116
|
+
)
|
|
117
|
+
const expanded = expandedId === row.id
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Fragment key={row.id}>
|
|
121
|
+
<Table.Row
|
|
122
|
+
className={hasDetails ? "cursor-pointer" : undefined}
|
|
123
|
+
onClick={
|
|
124
|
+
hasDetails ? () => toggleExpanded(row.id) : undefined
|
|
125
|
+
}
|
|
126
|
+
>
|
|
127
|
+
<Table.Cell className="w-8 text-ui-fg-muted">
|
|
128
|
+
{hasDetails ? (
|
|
129
|
+
expanded ? (
|
|
130
|
+
<TriangleDownMini />
|
|
131
|
+
) : (
|
|
132
|
+
<TriangleRightMini />
|
|
133
|
+
)
|
|
134
|
+
) : null}
|
|
135
|
+
</Table.Cell>
|
|
136
|
+
<Table.Cell>
|
|
137
|
+
{new Date(row.created_at).toLocaleString()}
|
|
138
|
+
</Table.Cell>
|
|
139
|
+
<Table.Cell>{row.event}</Table.Cell>
|
|
140
|
+
<Table.Cell>
|
|
141
|
+
{row.status ? (
|
|
142
|
+
<Badge
|
|
143
|
+
size="2xsmall"
|
|
144
|
+
color={STATUS_COLOR[row.status] ?? "grey"}
|
|
145
|
+
>
|
|
146
|
+
{row.status}
|
|
147
|
+
</Badge>
|
|
148
|
+
) : row.error ? (
|
|
149
|
+
<Badge size="2xsmall" color="red">
|
|
150
|
+
error
|
|
151
|
+
</Badge>
|
|
152
|
+
) : (
|
|
153
|
+
"—"
|
|
154
|
+
)}
|
|
155
|
+
</Table.Cell>
|
|
156
|
+
<Table.Cell>
|
|
157
|
+
{row.amount
|
|
158
|
+
? `${row.amount} ${row.currency ?? ""}`.trim()
|
|
159
|
+
: "—"}
|
|
160
|
+
</Table.Cell>
|
|
161
|
+
<Table.Cell className="font-mono text-xs">
|
|
162
|
+
{row.session_id ?? "—"}
|
|
163
|
+
</Table.Cell>
|
|
164
|
+
</Table.Row>
|
|
165
|
+
|
|
166
|
+
{expanded && (
|
|
167
|
+
<Table.Row className="bg-ui-bg-subtle hover:bg-ui-bg-subtle">
|
|
168
|
+
<Table.Cell colSpan={6}>
|
|
169
|
+
<div className="flex flex-col gap-3 py-2">
|
|
170
|
+
{row.error && (
|
|
171
|
+
<div className="flex flex-col gap-1">
|
|
172
|
+
<Text
|
|
173
|
+
size="small"
|
|
174
|
+
weight="plus"
|
|
175
|
+
className="text-ui-fg-error"
|
|
176
|
+
>
|
|
177
|
+
Error
|
|
178
|
+
</Text>
|
|
179
|
+
<pre className="whitespace-pre-wrap break-all rounded-md border border-ui-border-base bg-ui-bg-base p-3 text-xs">
|
|
180
|
+
{row.error}
|
|
181
|
+
</pre>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
{row.detail &&
|
|
185
|
+
Object.keys(row.detail).length > 0 && (
|
|
186
|
+
<div className="flex flex-col gap-1">
|
|
187
|
+
<Text
|
|
188
|
+
size="small"
|
|
189
|
+
weight="plus"
|
|
190
|
+
className="text-ui-fg-subtle"
|
|
191
|
+
>
|
|
192
|
+
Request & response
|
|
193
|
+
</Text>
|
|
194
|
+
<pre className="max-h-96 overflow-auto whitespace-pre-wrap break-all rounded-md border border-ui-border-base bg-ui-bg-base p-3 text-xs">
|
|
195
|
+
{formatDetail(row.detail)}
|
|
196
|
+
</pre>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</Table.Cell>
|
|
201
|
+
</Table.Row>
|
|
202
|
+
)}
|
|
203
|
+
</Fragment>
|
|
204
|
+
)
|
|
205
|
+
})}
|
|
206
|
+
</Table.Body>
|
|
207
|
+
</Table>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
211
|
+
<Text size="small" className="text-ui-fg-subtle">
|
|
212
|
+
{count} event{count === 1 ? "" : "s"}
|
|
213
|
+
</Text>
|
|
214
|
+
<div className="flex gap-2">
|
|
215
|
+
<Button
|
|
216
|
+
variant="secondary"
|
|
217
|
+
disabled={offset === 0 || loading}
|
|
218
|
+
onClick={() => load(Math.max(0, offset - limit))}
|
|
219
|
+
>
|
|
220
|
+
Previous
|
|
221
|
+
</Button>
|
|
222
|
+
<Button
|
|
223
|
+
variant="secondary"
|
|
224
|
+
disabled={offset + limit >= count || loading}
|
|
225
|
+
onClick={() => load(offset + limit)}
|
|
226
|
+
>
|
|
227
|
+
Next
|
|
228
|
+
</Button>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</Container>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const config = defineRouteConfig({
|
|
236
|
+
label: "Payments",
|
|
237
|
+
icon: CurrencyDollar,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
export default PaymentsPage
|