@easypayment/medusa-paypal-ui 1.0.39 โ 1.0.41
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/README.md +399 -288
- package/dist/index.cjs +250 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -3
- package/dist/index.d.ts +70 -3
- package/dist/index.mjs +246 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/PayPalPaymentSection.tsx +284 -0
- package/src/hooks/usePayPalPaymentMethods.ts +129 -0
- package/src/index.ts +10 -8
package/README.md
CHANGED
|
@@ -1,118 +1,295 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1>๐
ฟ medusa-paypal-frontend</h1>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<p><strong>Production-ready PayPal checkout UI for Medusa v2 storefronts</strong></p>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@easypayment/medusa-paypal-ui"><img src="https://img.shields.io/npm/v/@easypayment/medusa-paypal-ui?color=blue&label=npm" alt="npm version" /></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT" /></a>
|
|
10
|
+
<a href="https://medusajs.com"><img src="https://img.shields.io/badge/Medusa-v2-9b59b6" alt="Medusa v2" /></a>
|
|
11
|
+
<a href="https://nextjs.org"><img src="https://img.shields.io/badge/Next.js-14%2B-black" alt="Next.js" /></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p>PayPal Smart Buttons ยท Advanced Card Fields ยท Built-in loading states ยท Admin-controlled settings</p>
|
|
15
|
+
|
|
16
|
+
</div>
|
|
6
17
|
|
|
7
18
|
---
|
|
8
19
|
|
|
9
20
|
## Overview
|
|
10
21
|
|
|
11
|
-
`@easypayment/medusa-paypal-ui` is
|
|
22
|
+
`@easypayment/medusa-paypal-ui` is a React UI package that connects your Next.js (App Router) storefront to the `medusa-paypal-backend` plugin. It ships fully self-contained checkout components โ your storefront only needs a single component and one hook added to the existing payment step.
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
| Feature | Details |
|
|
25
|
+
|---|---|
|
|
26
|
+
| **PayPal Smart Buttons** | Wallet-based checkout via `pp_paypal_paypal` |
|
|
27
|
+
| **Advanced Card Fields** | Hosted PCI-compliant card inputs via `pp_paypal_card_paypal_card` |
|
|
28
|
+
| **Admin-driven config** | Enable/disable providers and set labels from Medusa Admin โ no code changes needed |
|
|
29
|
+
| **Built-in UX** | Loading spinners, error display, and currency warnings are all handled internally |
|
|
30
|
+
| **5-minute config cache** | `/store/paypal/config` is fetched once per session โ no redundant requests |
|
|
14
31
|
|
|
15
|
-
|
|
32
|
+
---
|
|
16
33
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
34
|
+
## Table of Contents
|
|
35
|
+
|
|
36
|
+
- [Requirements](#requirements)
|
|
37
|
+
- [Installation](#installation)
|
|
38
|
+
- [Environment Variables](#environment-variables)
|
|
39
|
+
- [Integration Guide](#integration-guide)
|
|
40
|
+
- [Step 1 โ Add the import](#step-1-add-the-import)
|
|
41
|
+
- [Step 2 โ Add session loading state](#step-2-add-session-loading-state)
|
|
42
|
+
- [Step 3 โ Add the config hook](#step-3-add-the-config-hook)
|
|
43
|
+
- [Step 4 โ Update setPaymentMethod](#step-4-update-setpaymentmethod)
|
|
44
|
+
- [Step 5 โ Filter the payment method list](#step-5-filter-the-payment-method-list)
|
|
45
|
+
- [Step 6 โ Inject admin-configured titles](#step-6-inject-admin-configured-titles)
|
|
46
|
+
- [Step 7 โ Render the PayPal UI](#step-7-render-the-paypal-ui)
|
|
47
|
+
- [Step 8 โ Disable the Continue button](#step-8-disable-the-continue-button)
|
|
48
|
+
- [Step 9 โ Fix the summary label](#step-9-fix-the-summary-label)
|
|
49
|
+
- [Complete File](#complete-file)
|
|
50
|
+
- [Testing](#testing)
|
|
22
51
|
|
|
23
52
|
---
|
|
24
53
|
|
|
25
|
-
##
|
|
54
|
+
## Requirements
|
|
26
55
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| Storefront starter | `medusa-next@1.0.3` |
|
|
32
|
-
| UI package | `@easypayment/medusa-paypal-ui@1.0.32` |
|
|
56
|
+
- **Node.js** 18+
|
|
57
|
+
- **Next.js** 14+ with App Router
|
|
58
|
+
- **`medusa-paypal-backend`** installed and running on your Medusa server
|
|
59
|
+
- A PayPal account connected in Medusa Admin โ Settings โ PayPal โ PayPal Connection
|
|
33
60
|
|
|
34
61
|
---
|
|
35
62
|
|
|
36
63
|
## Installation
|
|
37
64
|
|
|
38
|
-
Install the UI package in your storefront:
|
|
39
|
-
|
|
40
65
|
```bash
|
|
41
66
|
npm install @easypayment/medusa-paypal-ui
|
|
42
67
|
```
|
|
43
68
|
|
|
44
|
-
If your project does not already include the PayPal React SDK, install it as well:
|
|
45
|
-
|
|
46
69
|
```bash
|
|
47
|
-
|
|
70
|
+
yarn add @easypayment/medusa-paypal-ui
|
|
48
71
|
```
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Environment Variables
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
Add the following to your storefront `.env.local`. Use separate values for development and production.
|
|
78
|
+
|
|
79
|
+
```env
|
|
80
|
+
# โโ Development โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
81
|
+
NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000
|
|
82
|
+
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_...
|
|
83
|
+
|
|
84
|
+
# โโ Production โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
85
|
+
NEXT_PUBLIC_MEDUSA_BACKEND_URL=https://your-medusa-server.com
|
|
86
|
+
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_...
|
|
54
87
|
```
|
|
55
88
|
|
|
56
|
-
|
|
89
|
+
> **Where to get the publishable key:**
|
|
90
|
+
> Medusa Admin โ **Settings โ API Key Management โ Create API Key**
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Integration Guide
|
|
95
|
+
|
|
96
|
+
All changes are made to a single file in your storefront:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
src/modules/checkout/components/payment/index.tsx
|
|
60
100
|
```
|
|
61
101
|
|
|
102
|
+
> **Prefer copy-paste?** Skip to [Complete File](#complete-file) for a ready-to-use drop-in replacement.
|
|
103
|
+
|
|
62
104
|
---
|
|
63
105
|
|
|
64
|
-
|
|
106
|
+
### Step 1 โ Add the import
|
|
65
107
|
|
|
66
|
-
Add
|
|
108
|
+
Add this import alongside your existing imports at the top of the file:
|
|
67
109
|
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
110
|
+
```tsx
|
|
111
|
+
import {
|
|
112
|
+
PayPalPaymentSection,
|
|
113
|
+
isPayPalProviderId,
|
|
114
|
+
usePayPalPaymentMethods,
|
|
115
|
+
} from "@easypayment/medusa-paypal-ui"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### Step 2 โ Add session loading state
|
|
121
|
+
|
|
122
|
+
Inside the `Payment` component, add one new state variable alongside your existing `useState` calls:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
const [paypalSessionLoading, setPaypalSessionLoading] = useState(false)
|
|
74
126
|
```
|
|
75
127
|
|
|
76
|
-
|
|
128
|
+
This is set to `true` while `initiatePaymentSession` is running, allowing the PayPal UI to show a "Setting up paymentโฆ" spinner in place of the buttons during that window.
|
|
77
129
|
|
|
78
130
|
---
|
|
79
131
|
|
|
80
|
-
|
|
132
|
+
### Step 3 โ Add the config hook
|
|
81
133
|
|
|
82
|
-
|
|
134
|
+
Add this hook call inside the component, directly after the `const isOpen = ...` line:
|
|
83
135
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
136
|
+
```tsx
|
|
137
|
+
const { paypalEnabled, paypalTitle, cardEnabled, cardTitle } =
|
|
138
|
+
usePayPalPaymentMethods({
|
|
139
|
+
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
|
|
140
|
+
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
|
141
|
+
cartId: cart.id,
|
|
142
|
+
enabled: isOpen,
|
|
143
|
+
})
|
|
144
|
+
```
|
|
88
145
|
|
|
89
|
-
|
|
146
|
+
This fetches live settings from your Medusa backend โ provider enabled/disabled flags and admin-configured display labels. Responses are cached for 5 minutes per cart so there are no duplicate network requests.
|
|
90
147
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
148
|
+
Passing `enabled: isOpen` ensures the fetch only runs when the payment step is actually visible.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Step 4 โ Update `setPaymentMethod`
|
|
153
|
+
|
|
154
|
+
Replace your existing `setPaymentMethod` function with the following. The key addition is setting `paypalSessionLoading` while the session is being created:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
const setPaymentMethod = async (method: string) => {
|
|
158
|
+
setError(null)
|
|
159
|
+
setSelectedPaymentMethod(method)
|
|
160
|
+
if (isStripeLike(method) || isPayPalProviderId(method)) {
|
|
161
|
+
if (isPayPalProviderId(method)) setPaypalSessionLoading(true)
|
|
162
|
+
try {
|
|
163
|
+
await initiatePaymentSession(cart, { provider_id: method })
|
|
164
|
+
} finally {
|
|
165
|
+
setPaypalSessionLoading(false)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
94
169
|
```
|
|
95
170
|
|
|
171
|
+
> **Note:** `isStripeLike` comes from your storefront's `@lib/constants`. If your project does not use Stripe, remove the `isStripeLike(method)` condition and keep only `isPayPalProviderId(method)`.
|
|
172
|
+
|
|
96
173
|
---
|
|
97
174
|
|
|
98
|
-
|
|
175
|
+
### Step 5 โ Filter the payment method list
|
|
99
176
|
|
|
100
|
-
|
|
177
|
+
In your `RadioGroup`, add a `.filter()` before `.map()` to hide providers that have been disabled by the admin in the PayPal settings panel:
|
|
101
178
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
179
|
+
```tsx
|
|
180
|
+
{availablePaymentMethods
|
|
181
|
+
.filter((m) => {
|
|
182
|
+
if (m.id === "pp_paypal_paypal" && !paypalEnabled) return false
|
|
183
|
+
if (m.id === "pp_paypal_card_paypal_card" && !cardEnabled) return false
|
|
184
|
+
return true
|
|
185
|
+
})
|
|
186
|
+
.map((paymentMethod) => (
|
|
187
|
+
// ... your existing rendering code, unchanged
|
|
188
|
+
))}
|
|
189
|
+
```
|
|
110
190
|
|
|
111
191
|
---
|
|
112
192
|
|
|
113
|
-
|
|
193
|
+
### Step 6 โ Inject admin-configured titles
|
|
114
194
|
|
|
115
|
-
|
|
195
|
+
Inside your `.map()`, pass an overridden `paymentInfoMap` to the `PaymentContainer` component so the radio button labels reflect whatever the admin has set in Medusa Admin โ Settings โ PayPal โ PayPal Settings:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<PaymentContainer
|
|
199
|
+
paymentInfoMap={{
|
|
200
|
+
...paymentInfoMap,
|
|
201
|
+
"pp_paypal_paypal": {
|
|
202
|
+
...(paymentInfoMap["pp_paypal_paypal"] || {}),
|
|
203
|
+
title: paypalTitle,
|
|
204
|
+
},
|
|
205
|
+
"pp_paypal_card_paypal_card": {
|
|
206
|
+
...(paymentInfoMap["pp_paypal_card_paypal_card"] || {}),
|
|
207
|
+
title: cardTitle,
|
|
208
|
+
},
|
|
209
|
+
}}
|
|
210
|
+
paymentProviderId={paymentMethod.id}
|
|
211
|
+
selectedPaymentOptionId={selectedPaymentMethod}
|
|
212
|
+
/>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Step 7 โ Render the PayPal UI
|
|
218
|
+
|
|
219
|
+
After the closing `</RadioGroup>` tag, add the `PayPalPaymentSection` component:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<PayPalPaymentSection
|
|
223
|
+
cartId={cart.id}
|
|
224
|
+
selectedProviderId={selectedPaymentMethod}
|
|
225
|
+
baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
|
|
226
|
+
publishableApiKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}
|
|
227
|
+
sessionLoading={paypalSessionLoading}
|
|
228
|
+
onSuccess={async () => {
|
|
229
|
+
await placeOrder(cart.id)
|
|
230
|
+
}}
|
|
231
|
+
onError={(msg) => setError(msg)}
|
|
232
|
+
/>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This single component manages everything: config loading, Smart Buttons, Advanced Card fields, spinners, and errors. When a non-PayPal provider is selected it renders nothing, so it is safe to keep it in the tree at all times.
|
|
236
|
+
|
|
237
|
+
> **Critical โ `onSuccess` must call `placeOrder`**
|
|
238
|
+
>
|
|
239
|
+
> `placeOrder` is the Next.js Server Action exported from `@lib/data/cart`. It **must** run server-side โ it is the only mechanism that can clear the httpOnly cart cookie set by the server. Replacing it with a client-side `fetch` to `/store/carts/:id/complete` will complete the order in Medusa but leave the cart cookie intact, breaking the storefront session.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### Step 8 โ Disable the Continue button
|
|
244
|
+
|
|
245
|
+
`PayPalPaymentSection` renders its own Pay button. The storefront's "Continue to review" button must be disabled when PayPal is selected to avoid checkout flow confusion.
|
|
246
|
+
|
|
247
|
+
Add `isPayPalProviderId(selectedPaymentMethod)` to your `Button` component's `disabled` prop:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
<Button
|
|
251
|
+
size="large"
|
|
252
|
+
className="mt-6"
|
|
253
|
+
onClick={handleSubmit}
|
|
254
|
+
isLoading={isLoading}
|
|
255
|
+
disabled={
|
|
256
|
+
(isStripeLike(selectedPaymentMethod) && !cardComplete) ||
|
|
257
|
+
(!selectedPaymentMethod && !paidByGiftcard) ||
|
|
258
|
+
isPayPalProviderId(selectedPaymentMethod) // โ add this line
|
|
259
|
+
}
|
|
260
|
+
data-testid="submit-payment-button"
|
|
261
|
+
>
|
|
262
|
+
{!activeSession && isStripeLike(selectedPaymentMethod)
|
|
263
|
+
? "Enter card details"
|
|
264
|
+
: "Continue to review"}
|
|
265
|
+
</Button>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### Step 9 โ Fix the summary label
|
|
271
|
+
|
|
272
|
+
In the collapsed summary view (shown after the customer has selected a payment method), replace any hardcoded PayPal label with the admin-configured title from the hook:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
<Text
|
|
276
|
+
className="txt-medium text-ui-fg-subtle"
|
|
277
|
+
data-testid="payment-method-summary"
|
|
278
|
+
>
|
|
279
|
+
{activeSession?.provider_id === "pp_paypal_paypal"
|
|
280
|
+
? paypalTitle
|
|
281
|
+
: activeSession?.provider_id === "pp_paypal_card_paypal_card"
|
|
282
|
+
? cardTitle
|
|
283
|
+
: paymentInfoMap[activeSession?.provider_id]?.title ||
|
|
284
|
+
activeSession?.provider_id}
|
|
285
|
+
</Text>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Complete File
|
|
291
|
+
|
|
292
|
+
The following is a complete, working replacement for your payment step file with all changes from the guide above already applied. Copy and replace the entire contents of `src/modules/checkout/components/payment/index.tsx`.
|
|
116
293
|
|
|
117
294
|
```tsx
|
|
118
295
|
"use client"
|
|
@@ -122,7 +299,11 @@ import { isStripeLike, paymentInfoMap } from "@lib/constants"
|
|
|
122
299
|
import { initiatePaymentSession, placeOrder } from "@lib/data/cart"
|
|
123
300
|
import { CheckCircleSolid, CreditCard } from "@medusajs/icons"
|
|
124
301
|
import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
|
|
125
|
-
import {
|
|
302
|
+
import {
|
|
303
|
+
PayPalPaymentSection,
|
|
304
|
+
isPayPalProviderId,
|
|
305
|
+
usePayPalPaymentMethods,
|
|
306
|
+
} from "@easypayment/medusa-paypal-ui"
|
|
126
307
|
import ErrorMessage from "@modules/checkout/components/error-message"
|
|
127
308
|
import PaymentContainer, {
|
|
128
309
|
StripeCardContainer,
|
|
@@ -131,9 +312,6 @@ import Divider from "@modules/common/components/divider"
|
|
|
131
312
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|
132
313
|
import { useCallback, useEffect, useState } from "react"
|
|
133
314
|
|
|
134
|
-
const PAYPAL_IDS = ["pp_paypal_paypal", "pp_paypal_card_paypal_card"]
|
|
135
|
-
const isPayPal = (id: string) => PAYPAL_IDS.includes(id)
|
|
136
|
-
|
|
137
315
|
const Payment = ({
|
|
138
316
|
cart,
|
|
139
317
|
availablePaymentMethods,
|
|
@@ -142,7 +320,7 @@ const Payment = ({
|
|
|
142
320
|
availablePaymentMethods: any[]
|
|
143
321
|
}) => {
|
|
144
322
|
const activeSession = cart.payment_collection?.payment_sessions?.find(
|
|
145
|
-
(paymentSession: any) => paymentSession.status === "pending"
|
|
323
|
+
(paymentSession: any) => paymentSession.status === "pending",
|
|
146
324
|
)
|
|
147
325
|
|
|
148
326
|
const [isLoading, setIsLoading] = useState(false)
|
|
@@ -150,12 +328,9 @@ const Payment = ({
|
|
|
150
328
|
const [cardBrand, setCardBrand] = useState<string | null>(null)
|
|
151
329
|
const [cardComplete, setCardComplete] = useState(false)
|
|
152
330
|
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
|
153
|
-
activeSession?.provider_id ?? ""
|
|
331
|
+
activeSession?.provider_id ?? "",
|
|
154
332
|
)
|
|
155
|
-
const [
|
|
156
|
-
const [paypalTitle, setPaypalTitle] = useState<string>("PayPal")
|
|
157
|
-
const [cardEnabled, setCardEnabled] = useState<boolean>(true)
|
|
158
|
-
const [cardTitle, setCardTitle] = useState<string>("Credit or Debit Card")
|
|
333
|
+
const [paypalSessionLoading, setPaypalSessionLoading] = useState(false)
|
|
159
334
|
|
|
160
335
|
const searchParams = useSearchParams()
|
|
161
336
|
const router = useRouter()
|
|
@@ -163,12 +338,24 @@ const Payment = ({
|
|
|
163
338
|
|
|
164
339
|
const isOpen = searchParams.get("step") === "payment"
|
|
165
340
|
|
|
341
|
+
const { paypalEnabled, paypalTitle, cardEnabled, cardTitle } =
|
|
342
|
+
usePayPalPaymentMethods({
|
|
343
|
+
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
|
|
344
|
+
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
|
345
|
+
cartId: cart.id,
|
|
346
|
+
enabled: isOpen,
|
|
347
|
+
})
|
|
348
|
+
|
|
166
349
|
const setPaymentMethod = async (method: string) => {
|
|
167
350
|
setError(null)
|
|
168
351
|
setSelectedPaymentMethod(method)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
352
|
+
if (isStripeLike(method) || isPayPalProviderId(method)) {
|
|
353
|
+
if (isPayPalProviderId(method)) setPaypalSessionLoading(true)
|
|
354
|
+
try {
|
|
355
|
+
await initiatePaymentSession(cart, { provider_id: method })
|
|
356
|
+
} finally {
|
|
357
|
+
setPaypalSessionLoading(false)
|
|
358
|
+
}
|
|
172
359
|
}
|
|
173
360
|
}
|
|
174
361
|
|
|
@@ -184,7 +371,7 @@ const Payment = ({
|
|
|
184
371
|
params.set(name, value)
|
|
185
372
|
return params.toString()
|
|
186
373
|
},
|
|
187
|
-
[searchParams]
|
|
374
|
+
[searchParams],
|
|
188
375
|
)
|
|
189
376
|
|
|
190
377
|
const handleEdit = () => {
|
|
@@ -198,22 +385,15 @@ const Payment = ({
|
|
|
198
385
|
try {
|
|
199
386
|
const shouldInputCard =
|
|
200
387
|
isStripeLike(selectedPaymentMethod) && !activeSession
|
|
201
|
-
|
|
202
388
|
const checkActiveSession =
|
|
203
389
|
activeSession?.provider_id === selectedPaymentMethod
|
|
204
|
-
|
|
205
390
|
if (!checkActiveSession) {
|
|
206
|
-
await initiatePaymentSession(cart, {
|
|
207
|
-
provider_id: selectedPaymentMethod,
|
|
208
|
-
})
|
|
391
|
+
await initiatePaymentSession(cart, { provider_id: selectedPaymentMethod })
|
|
209
392
|
}
|
|
210
|
-
|
|
211
393
|
if (!shouldInputCard) {
|
|
212
394
|
return router.push(
|
|
213
395
|
pathname + "?" + createQueryString("step", "review"),
|
|
214
|
-
{
|
|
215
|
-
scroll: false,
|
|
216
|
-
}
|
|
396
|
+
{ scroll: false },
|
|
217
397
|
)
|
|
218
398
|
}
|
|
219
399
|
} catch (err: any) {
|
|
@@ -227,52 +407,6 @@ const Payment = ({
|
|
|
227
407
|
setError(null)
|
|
228
408
|
}, [isOpen])
|
|
229
409
|
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
if (!isOpen) return
|
|
232
|
-
|
|
233
|
-
const backendUrl = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
|
|
234
|
-
if (!backendUrl) return
|
|
235
|
-
|
|
236
|
-
const key = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
|
|
237
|
-
|
|
238
|
-
fetch(`${backendUrl}/store/paypal/config`, {
|
|
239
|
-
headers: key ? { "x-publishable-api-key": key } : {},
|
|
240
|
-
})
|
|
241
|
-
.then((r) => {
|
|
242
|
-
if (r.status === 403) {
|
|
243
|
-
setPaypalEnabled(false)
|
|
244
|
-
setCardEnabled(false)
|
|
245
|
-
return null
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (!r.ok) {
|
|
249
|
-
return null
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return r.json()
|
|
253
|
-
})
|
|
254
|
-
.then((cfg) => {
|
|
255
|
-
if (!cfg) return
|
|
256
|
-
|
|
257
|
-
if (typeof cfg?.paypal_enabled === "boolean") {
|
|
258
|
-
setPaypalEnabled(cfg.paypal_enabled)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (typeof cfg?.paypal_title === "string" && cfg.paypal_title) {
|
|
262
|
-
setPaypalTitle(cfg.paypal_title)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (typeof cfg?.card_enabled === "boolean") {
|
|
266
|
-
setCardEnabled(cfg.card_enabled)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (typeof cfg?.card_title === "string" && cfg.card_title) {
|
|
270
|
-
setCardTitle(cfg.card_title)
|
|
271
|
-
}
|
|
272
|
-
})
|
|
273
|
-
.catch(() => {})
|
|
274
|
-
}, [isOpen])
|
|
275
|
-
|
|
276
410
|
return (
|
|
277
411
|
<div className="bg-white">
|
|
278
412
|
<div className="flex flex-row items-center justify-between mb-6">
|
|
@@ -283,13 +417,12 @@ const Payment = ({
|
|
|
283
417
|
{
|
|
284
418
|
"opacity-50 pointer-events-none select-none":
|
|
285
419
|
!isOpen && !paymentReady,
|
|
286
|
-
}
|
|
420
|
+
},
|
|
287
421
|
)}
|
|
288
422
|
>
|
|
289
423
|
Payment
|
|
290
424
|
{!isOpen && paymentReady && <CheckCircleSolid />}
|
|
291
425
|
</Heading>
|
|
292
|
-
|
|
293
426
|
{!isOpen && paymentReady && (
|
|
294
427
|
<Text>
|
|
295
428
|
<button
|
|
@@ -305,69 +438,77 @@ const Payment = ({
|
|
|
305
438
|
|
|
306
439
|
<div>
|
|
307
440
|
<div className={isOpen ? "block" : "hidden"}>
|
|
308
|
-
{!paidByGiftcard &&
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
441
|
+
{!paidByGiftcard && availablePaymentMethods?.length && (
|
|
442
|
+
<>
|
|
443
|
+
<RadioGroup
|
|
444
|
+
value={selectedPaymentMethod}
|
|
445
|
+
onChange={(value: string) => setPaymentMethod(value)}
|
|
446
|
+
>
|
|
447
|
+
{availablePaymentMethods
|
|
448
|
+
.filter((m) => {
|
|
449
|
+
if (m.id === "pp_paypal_paypal" && !paypalEnabled) return false
|
|
450
|
+
if (m.id === "pp_paypal_card_paypal_card" && !cardEnabled) return false
|
|
451
|
+
return true
|
|
452
|
+
})
|
|
453
|
+
.map((paymentMethod) => (
|
|
454
|
+
<div key={paymentMethod.id}>
|
|
455
|
+
{isStripeLike(paymentMethod.id) ? (
|
|
456
|
+
<StripeCardContainer
|
|
457
|
+
paymentProviderId={paymentMethod.id}
|
|
458
|
+
selectedPaymentOptionId={selectedPaymentMethod}
|
|
459
|
+
paymentInfoMap={paymentInfoMap}
|
|
460
|
+
setCardBrand={setCardBrand}
|
|
461
|
+
setError={setError}
|
|
462
|
+
setCardComplete={setCardComplete}
|
|
463
|
+
/>
|
|
464
|
+
) : (
|
|
465
|
+
<PaymentContainer
|
|
466
|
+
paymentInfoMap={{
|
|
467
|
+
...paymentInfoMap,
|
|
468
|
+
"pp_paypal_paypal": {
|
|
469
|
+
...(paymentInfoMap["pp_paypal_paypal"] || {}),
|
|
470
|
+
title: paypalTitle,
|
|
471
|
+
},
|
|
472
|
+
"pp_paypal_card_paypal_card": {
|
|
473
|
+
...(paymentInfoMap["pp_paypal_card_paypal_card"] || {}),
|
|
474
|
+
title: cardTitle,
|
|
475
|
+
},
|
|
476
|
+
}}
|
|
477
|
+
paymentProviderId={paymentMethod.id}
|
|
478
|
+
selectedPaymentOptionId={selectedPaymentMethod}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
))}
|
|
483
|
+
</RadioGroup>
|
|
484
|
+
|
|
485
|
+
<PayPalPaymentSection
|
|
486
|
+
cartId={cart.id}
|
|
487
|
+
selectedProviderId={selectedPaymentMethod}
|
|
488
|
+
baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
|
|
489
|
+
publishableApiKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}
|
|
490
|
+
sessionLoading={paypalSessionLoading}
|
|
491
|
+
onSuccess={async () => {
|
|
492
|
+
await placeOrder(cart.id)
|
|
493
|
+
}}
|
|
494
|
+
onError={(msg) => setError(msg)}
|
|
495
|
+
/>
|
|
496
|
+
</>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
{paidByGiftcard && (
|
|
500
|
+
<div className="flex flex-col w-1/3">
|
|
501
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
502
|
+
Payment method
|
|
503
|
+
</Text>
|
|
504
|
+
<Text
|
|
505
|
+
className="txt-medium text-ui-fg-subtle"
|
|
506
|
+
data-testid="payment-method-summary"
|
|
507
|
+
>
|
|
508
|
+
Gift card
|
|
509
|
+
</Text>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
371
512
|
|
|
372
513
|
<ErrorMessage
|
|
373
514
|
error={error}
|
|
@@ -382,17 +523,69 @@ const Payment = ({
|
|
|
382
523
|
disabled={
|
|
383
524
|
(isStripeLike(selectedPaymentMethod) && !cardComplete) ||
|
|
384
525
|
(!selectedPaymentMethod && !paidByGiftcard) ||
|
|
385
|
-
|
|
526
|
+
isPayPalProviderId(selectedPaymentMethod)
|
|
386
527
|
}
|
|
387
528
|
data-testid="submit-payment-button"
|
|
388
529
|
>
|
|
389
530
|
{!activeSession && isStripeLike(selectedPaymentMethod)
|
|
390
|
-
? "
|
|
531
|
+
? "Enter card details"
|
|
391
532
|
: "Continue to review"}
|
|
392
533
|
</Button>
|
|
393
534
|
</div>
|
|
394
|
-
</div>
|
|
395
535
|
|
|
536
|
+
<div className={isOpen ? "hidden" : "block"}>
|
|
537
|
+
{cart && paymentReady && activeSession ? (
|
|
538
|
+
<div className="flex items-start gap-x-1 w-full">
|
|
539
|
+
<div className="flex flex-col w-1/3">
|
|
540
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
541
|
+
Payment method
|
|
542
|
+
</Text>
|
|
543
|
+
<Text
|
|
544
|
+
className="txt-medium text-ui-fg-subtle"
|
|
545
|
+
data-testid="payment-method-summary"
|
|
546
|
+
>
|
|
547
|
+
{activeSession?.provider_id === "pp_paypal_paypal"
|
|
548
|
+
? paypalTitle
|
|
549
|
+
: activeSession?.provider_id === "pp_paypal_card_paypal_card"
|
|
550
|
+
? cardTitle
|
|
551
|
+
: paymentInfoMap[activeSession?.provider_id]?.title ||
|
|
552
|
+
activeSession?.provider_id}
|
|
553
|
+
</Text>
|
|
554
|
+
</div>
|
|
555
|
+
<div className="flex flex-col w-1/3">
|
|
556
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
557
|
+
Payment details
|
|
558
|
+
</Text>
|
|
559
|
+
<div
|
|
560
|
+
className="flex gap-2 txt-medium text-ui-fg-subtle items-center"
|
|
561
|
+
data-testid="payment-details-summary"
|
|
562
|
+
>
|
|
563
|
+
<Container className="flex items-center h-7 w-fit p-2 bg-ui-button-neutral-hover">
|
|
564
|
+
{paymentInfoMap[selectedPaymentMethod]?.icon || <CreditCard />}
|
|
565
|
+
</Container>
|
|
566
|
+
<Text>
|
|
567
|
+
{isStripeLike(selectedPaymentMethod) && cardBrand
|
|
568
|
+
? cardBrand
|
|
569
|
+
: "Another step will appear"}
|
|
570
|
+
</Text>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
) : paidByGiftcard ? (
|
|
575
|
+
<div className="flex flex-col w-1/3">
|
|
576
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
577
|
+
Payment method
|
|
578
|
+
</Text>
|
|
579
|
+
<Text
|
|
580
|
+
className="txt-medium text-ui-fg-subtle"
|
|
581
|
+
data-testid="payment-method-summary"
|
|
582
|
+
>
|
|
583
|
+
Gift card
|
|
584
|
+
</Text>
|
|
585
|
+
</div>
|
|
586
|
+
) : null}
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
396
589
|
<Divider className="mt-8" />
|
|
397
590
|
</div>
|
|
398
591
|
)
|
|
@@ -403,102 +596,20 @@ export default Payment
|
|
|
403
596
|
|
|
404
597
|
---
|
|
405
598
|
|
|
406
|
-
##
|
|
599
|
+
## Testing
|
|
407
600
|
|
|
408
|
-
|
|
601
|
+
Toggle between sandbox and live in Medusa Admin โ **Settings โ PayPal โ PayPal Connection โ Environment**.
|
|
409
602
|
|
|
410
|
-
|
|
411
|
-
{isPayPal(selectedPaymentMethod) && (
|
|
412
|
-
<MedusaNextPayPalAdapter
|
|
413
|
-
cartId={cart.id}
|
|
414
|
-
selectedProviderId={selectedPaymentMethod}
|
|
415
|
-
baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
|
|
416
|
-
publishableApiKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}
|
|
417
|
-
onSuccess={() => placeOrder(cart.id)}
|
|
418
|
-
onError={(msg) => setError(msg)}
|
|
419
|
-
/>
|
|
420
|
-
)}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
---
|
|
603
|
+
**Sandbox buyer account**
|
|
424
604
|
|
|
425
|
-
|
|
605
|
+
Log in at [developer.paypal.com](https://developer.paypal.com) โ **Testing โ Sandbox Accounts** to find your auto-generated buyer credentials. Sandbox payments do not charge real money.
|
|
426
606
|
|
|
427
|
-
|
|
607
|
+
**Test card number for Advanced Card fields**
|
|
428
608
|
|
|
429
|
-
```txt
|
|
430
|
-
/store/paypal/config
|
|
431
609
|
```
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
- `paypal_enabled`
|
|
436
|
-
- `paypal_title`
|
|
437
|
-
- `card_enabled`
|
|
438
|
-
- `card_title`
|
|
439
|
-
|
|
440
|
-
If the endpoint returns `403`, the storefront should treat both PayPal methods as disabled.
|
|
441
|
-
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
## Props
|
|
445
|
-
|
|
446
|
-
| Prop | Required | Description |
|
|
447
|
-
|---|---|---|
|
|
448
|
-
| `cartId` | Yes | Active cart ID |
|
|
449
|
-
| `selectedProviderId` | Yes | Currently selected payment provider ID |
|
|
450
|
-
| `baseUrl` | Yes | Medusa backend base URL |
|
|
451
|
-
| `publishableApiKey` | No | Publishable API key sent in config request |
|
|
452
|
-
| `providerIds` | No | Override default PayPal provider IDs |
|
|
453
|
-
| `onSuccess` | No | Called after successful PayPal checkout |
|
|
454
|
-
| `onError` | No | Called when PayPal returns an error |
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
|
-
## Default Provider Mapping
|
|
459
|
-
|
|
460
|
-
The UI package uses these default provider IDs:
|
|
461
|
-
|
|
462
|
-
```ts
|
|
463
|
-
{
|
|
464
|
-
paypal: "pp_paypal_paypal",
|
|
465
|
-
paypalCard: "pp_paypal_card_paypal_card"
|
|
466
|
-
}
|
|
610
|
+
Card number 4111 1111 1111 1111
|
|
611
|
+
Expiry Any future date
|
|
612
|
+
CVV Any 3 digits
|
|
467
613
|
```
|
|
468
614
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
---
|
|
472
|
-
|
|
473
|
-
## Notes for Medusa Starter Users
|
|
474
|
-
|
|
475
|
-
The original Medusa starter payment step only initialized Stripe-like sessions and used the default review-step submit flow.
|
|
476
|
-
|
|
477
|
-
To support PayPal, your updated checkout should additionally:
|
|
478
|
-
|
|
479
|
-
- initialize sessions for PayPal methods
|
|
480
|
-
- render `MedusaNextPayPalAdapter`
|
|
481
|
-
- call `placeOrder` after successful PayPal completion
|
|
482
|
-
- disable the normal submit button while PayPal is selected
|
|
483
|
-
- optionally fetch backend config to control PayPal method visibility and labels
|
|
484
|
-
|
|
485
|
-
---
|
|
486
|
-
|
|
487
|
-
## Validation Checklist
|
|
488
|
-
|
|
489
|
-
- Install `@easypayment/medusa-paypal-ui`
|
|
490
|
-
- Install `@paypal/react-paypal-js`
|
|
491
|
-
- Set storefront environment variables
|
|
492
|
-
- Configure backend PayPal package
|
|
493
|
-
- Ensure provider IDs match backend configuration
|
|
494
|
-
- Update checkout payment step
|
|
495
|
-
- Verify PayPal Wallet flow
|
|
496
|
-
- Verify PayPal Card flow
|
|
497
|
-
- Verify order placement after successful payment
|
|
498
|
-
- Verify error handling
|
|
499
|
-
|
|
500
|
-
---
|
|
501
|
-
|
|
502
|
-
## License
|
|
503
|
-
|
|
504
|
-
MIT
|
|
615
|
+
---
|