@easypayment/medusa-paypal-ui 1.0.32 → 1.0.34

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 CHANGED
@@ -1,138 +1,504 @@
1
- # @easypayment/medusa-paypal-ui
2
-
3
- Production-ready PayPal storefront UI package for **Medusa v2 + Next.js**.
4
-
5
- Includes:
6
-
7
- - `MedusaNextPayPalAdapter` for checkout integration
8
- - Smart Buttons support
9
- - Advanced Card Fields support
10
- - Runtime config loading from `/store/paypal/config`
11
-
12
- ---
13
-
14
- ## Compatibility
15
-
16
- - Next.js `>=14`
17
- - React `>=18`
18
- - Medusa backend with `@easypayment/medusa-paypal`
19
-
20
- ---
21
-
22
- ## Step-by-step setup (GitHub style)
23
-
24
- ### 1) Install dependencies
25
-
26
- ```bash
27
- npm install @easypayment/medusa-paypal-ui @paypal/react-paypal-js
28
- # or
29
- pnpm add @easypayment/medusa-paypal-ui @paypal/react-paypal-js
30
- # or
31
- yarn add @easypayment/medusa-paypal-ui @paypal/react-paypal-js
32
- ```
33
-
34
- ### 2) Add storefront environment variables
35
-
36
- In your storefront `.env.local`:
37
-
38
- ```bash
39
- NEXT_PUBLIC_MEDUSA_BACKEND_URL=https://your-medusa-api.example.com
40
- NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_xxxxxxxxxxxxxxxxxxxxx
41
- ```
42
-
43
- > Never expose secret keys (`sk_*`) in `NEXT_PUBLIC_*` vars.
44
-
45
- ### 3) Ensure payment session uses PayPal provider IDs
46
-
47
- Common provider IDs:
48
-
49
- - `pp_paypal_paypal`
50
- - `pp_paypal_card_paypal_card`
51
-
52
- When the shopper selects a payment method, initialize a matching Medusa payment session.
53
-
54
- ### 4) Render `MedusaNextPayPalAdapter` in checkout
55
-
56
- ```tsx
57
- import { MedusaNextPayPalAdapter } from "@easypayment/medusa-paypal-ui"
58
- import { initiatePaymentSession, placeOrder } from "@lib/data/cart"
59
-
60
- const PAYPAL_IDS = ["pp_paypal_paypal", "pp_paypal_card_paypal_card"]
61
-
62
- // Example on payment selection
63
- async function setPaymentMethod(providerId: string) {
64
- await initiatePaymentSession(cart, { provider_id: providerId })
65
- }
66
-
67
- <MedusaNextPayPalAdapter
68
- cartId={cart.id}
69
- selectedProviderId={selectedPaymentMethod}
70
- baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
71
- publishableApiKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}
72
- onSuccess={() => placeOrder(cart.id)}
73
- onError={(message) => setError(message)}
74
- />
75
- ```
76
-
77
- ### 5) Validate checkout flows
78
-
79
- - Test PayPal Wallet flow.
80
- - Test PayPal Advanced Card flow.
81
- - Confirm order placement callback (`onSuccess`) runs.
82
- - Confirm errors surface through `onError`.
83
-
84
- ---
85
-
86
- ## Props
87
-
88
- | Prop | Required | Description |
89
- |---|---|---|
90
- | `cartId` | ✅ | Active cart ID |
91
- | `selectedProviderId` | ✅ | Currently selected provider ID |
92
- | `baseUrl` | ✅ | Medusa backend URL |
93
- | `publishableApiKey` | optional | Medusa publishable key |
94
- | `providerIds` | optional | Override default provider IDs |
95
- | `onSuccess` | optional | Called after successful PayPal capture/authorize |
96
- | `onError` | optional | Called on payment error |
97
-
98
- ---
99
-
100
- ## Provider ID mapping
101
-
102
- Default adapter IDs:
103
-
104
- ```ts
105
- paypal: "pp_paypal_paypal"
106
- paypalCard: "pp_paypal_card_paypal_card"
107
- ```
108
-
109
- If backend IDs differ, pass `providerIds` explicitly.
110
-
111
- ---
112
-
113
- ## Quick checklist
114
-
115
- - [ ] UI package installed
116
- - [ ] Storefront env vars set
117
- - [ ] Checkout initializes PayPal provider payment session
118
- - [ ] Adapter rendered with required props
119
- - [ ] Wallet + Card flows verified
120
-
121
- ---
122
-
123
- ## Build and test (package development)
124
-
125
- ```bash
126
- npm run build
127
- npm test
128
- ```
129
-
130
- ---
131
-
132
- ## License
133
-
134
- MIT
135
-
136
- ## UI note
137
-
138
- The Advanced Card hosted fields (`Card number`, `Expiration date`, `Security code`) use `style={cardStyle}` on `PayPalCardFieldsProvider`, including `cardStyle.input.height: "46px"` (`fontSize: 16px`, `padding: 12px 14px`) so PayPal renders the iframe at the correct height and avoids clipping.
1
+ # @easypayment/medusa-paypal-ui
2
+
3
+ PayPal checkout UI for Medusa v2 storefronts.
4
+
5
+ A React storefront package for integrating **PayPal Wallet** and **PayPal Advanced Card Payments** into a Medusa + Next.js checkout.
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ `@easypayment/medusa-paypal-ui` is used on the **storefront** side and works with the backend package:
12
+
13
+ - `@easypayment/medusa-paypal`
14
+
15
+ It is designed to plug into the Medusa Next.js starter checkout and adds:
16
+
17
+ - PayPal Wallet rendering in checkout
18
+ - PayPal Advanced Card rendering in checkout
19
+ - runtime config loading from the Medusa backend
20
+ - PayPal-specific payment session initialization
21
+ - success and error callbacks for order placement
22
+
23
+ ---
24
+
25
+ ## Compatibility
26
+
27
+ | Component | Version |
28
+ |---|---|
29
+ | Next.js | `15.3.9` |
30
+ | React | `19.0.4` |
31
+ | Storefront starter | `medusa-next@1.0.3` |
32
+ | UI package | `@easypayment/medusa-paypal-ui@1.0.32` |
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ Install the UI package in your storefront:
39
+
40
+ ```bash
41
+ npm install @easypayment/medusa-paypal-ui
42
+ ```
43
+
44
+ If your project does not already include the PayPal React SDK, install it as well:
45
+
46
+ ```bash
47
+ npm install @paypal/react-paypal-js
48
+ ```
49
+
50
+ Or with pnpm:
51
+
52
+ ```bash
53
+ pnpm add @easypayment/medusa-paypal-ui @paypal/react-paypal-js
54
+ ```
55
+
56
+ Or with yarn:
57
+
58
+ ```bash
59
+ yarn add @easypayment/medusa-paypal-ui @paypal/react-paypal-js
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Storefront Environment Variables
65
+
66
+ Add the required variables to your storefront `.env.local` or `.env` file:
67
+
68
+ ```env
69
+ MEDUSA_BACKEND_URL=http://localhost:9000
70
+ NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000
71
+ NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_your_publishable_key
72
+ NEXT_PUBLIC_BASE_URL=http://localhost:8000
73
+ NEXT_PUBLIC_DEFAULT_REGION=us
74
+ ```
75
+
76
+ Other storefront variables such as Stripe, revalidation, or cloud image settings are unrelated to this PayPal package and can stay as they are in the Medusa starter template.
77
+
78
+ ---
79
+
80
+ ## Backend Requirements
81
+
82
+ Your Medusa backend must:
83
+
84
+ - have `@easypayment/medusa-paypal` installed
85
+ - register both PayPal providers
86
+ - expose `/store/paypal/config`
87
+ - have PayPal configured from Medusa Admin
88
+
89
+ Typical backend provider IDs:
90
+
91
+ ```txt
92
+ pp_paypal_paypal
93
+ pp_paypal_card_paypal_card
94
+ ```
95
+
96
+ ---
97
+
98
+ ## What Changed in Checkout Integration
99
+
100
+ Compared with the original Medusa starter payment step, the PayPal integration adds the following behavior:
101
+
102
+ - imports `MedusaNextPayPalAdapter`
103
+ - imports `placeOrder` for PayPal success completion
104
+ - defines PayPal provider IDs
105
+ - initializes payment sessions for PayPal methods
106
+ - fetches runtime config from `/store/paypal/config`
107
+ - hides PayPal methods when backend config disables them
108
+ - uses backend titles for PayPal Wallet and Card
109
+ - disables the default submit button for PayPal because PayPal handles its own submit flow
110
+
111
+ ---
112
+
113
+ ## Checkout Integration Example
114
+
115
+ Below is the updated payment-step pattern based on the current storefront integration:
116
+
117
+ ```tsx
118
+ "use client"
119
+
120
+ import { RadioGroup } from "@headlessui/react"
121
+ import { isStripeLike, paymentInfoMap } from "@lib/constants"
122
+ import { initiatePaymentSession, placeOrder } from "@lib/data/cart"
123
+ import { CheckCircleSolid, CreditCard } from "@medusajs/icons"
124
+ import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
125
+ import { MedusaNextPayPalAdapter } from "@easypayment/medusa-paypal-ui"
126
+ import ErrorMessage from "@modules/checkout/components/error-message"
127
+ import PaymentContainer, {
128
+ StripeCardContainer,
129
+ } from "@modules/checkout/components/payment-container"
130
+ import Divider from "@modules/common/components/divider"
131
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
132
+ import { useCallback, useEffect, useState } from "react"
133
+
134
+ const PAYPAL_IDS = ["pp_paypal_paypal", "pp_paypal_card_paypal_card"]
135
+ const isPayPal = (id: string) => PAYPAL_IDS.includes(id)
136
+
137
+ const Payment = ({
138
+ cart,
139
+ availablePaymentMethods,
140
+ }: {
141
+ cart: any
142
+ availablePaymentMethods: any[]
143
+ }) => {
144
+ const activeSession = cart.payment_collection?.payment_sessions?.find(
145
+ (paymentSession: any) => paymentSession.status === "pending"
146
+ )
147
+
148
+ const [isLoading, setIsLoading] = useState(false)
149
+ const [error, setError] = useState<string | null>(null)
150
+ const [cardBrand, setCardBrand] = useState<string | null>(null)
151
+ const [cardComplete, setCardComplete] = useState(false)
152
+ const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
153
+ activeSession?.provider_id ?? ""
154
+ )
155
+ const [paypalEnabled, setPaypalEnabled] = useState<boolean>(true)
156
+ const [paypalTitle, setPaypalTitle] = useState<string>("PayPal")
157
+ const [cardEnabled, setCardEnabled] = useState<boolean>(true)
158
+ const [cardTitle, setCardTitle] = useState<string>("Credit or Debit Card")
159
+
160
+ const searchParams = useSearchParams()
161
+ const router = useRouter()
162
+ const pathname = usePathname()
163
+
164
+ const isOpen = searchParams.get("step") === "payment"
165
+
166
+ const setPaymentMethod = async (method: string) => {
167
+ setError(null)
168
+ setSelectedPaymentMethod(method)
169
+
170
+ if (isStripeLike(method) || isPayPal(method)) {
171
+ await initiatePaymentSession(cart, { provider_id: method })
172
+ }
173
+ }
174
+
175
+ const paidByGiftcard =
176
+ cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0
177
+
178
+ const paymentReady =
179
+ (activeSession && cart?.shipping_methods.length !== 0) || paidByGiftcard
180
+
181
+ const createQueryString = useCallback(
182
+ (name: string, value: string) => {
183
+ const params = new URLSearchParams(searchParams)
184
+ params.set(name, value)
185
+ return params.toString()
186
+ },
187
+ [searchParams]
188
+ )
189
+
190
+ const handleEdit = () => {
191
+ router.push(pathname + "?" + createQueryString("step", "payment"), {
192
+ scroll: false,
193
+ })
194
+ }
195
+
196
+ const handleSubmit = async () => {
197
+ setIsLoading(true)
198
+ try {
199
+ const shouldInputCard =
200
+ isStripeLike(selectedPaymentMethod) && !activeSession
201
+
202
+ const checkActiveSession =
203
+ activeSession?.provider_id === selectedPaymentMethod
204
+
205
+ if (!checkActiveSession) {
206
+ await initiatePaymentSession(cart, {
207
+ provider_id: selectedPaymentMethod,
208
+ })
209
+ }
210
+
211
+ if (!shouldInputCard) {
212
+ return router.push(
213
+ pathname + "?" + createQueryString("step", "review"),
214
+ {
215
+ scroll: false,
216
+ }
217
+ )
218
+ }
219
+ } catch (err: any) {
220
+ setError(err.message)
221
+ } finally {
222
+ setIsLoading(false)
223
+ }
224
+ }
225
+
226
+ useEffect(() => {
227
+ setError(null)
228
+ }, [isOpen])
229
+
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
+ return (
277
+ <div className="bg-white">
278
+ <div className="flex flex-row items-center justify-between mb-6">
279
+ <Heading
280
+ level="h2"
281
+ className={clx(
282
+ "flex flex-row text-3xl-regular gap-x-2 items-baseline",
283
+ {
284
+ "opacity-50 pointer-events-none select-none":
285
+ !isOpen && !paymentReady,
286
+ }
287
+ )}
288
+ >
289
+ Payment
290
+ {!isOpen && paymentReady && <CheckCircleSolid />}
291
+ </Heading>
292
+
293
+ {!isOpen && paymentReady && (
294
+ <Text>
295
+ <button
296
+ onClick={handleEdit}
297
+ className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
298
+ data-testid="edit-payment-button"
299
+ >
300
+ Edit
301
+ </button>
302
+ </Text>
303
+ )}
304
+ </div>
305
+
306
+ <div>
307
+ <div className={isOpen ? "block" : "hidden"}>
308
+ {!paidByGiftcard &&
309
+ availablePaymentMethods?.length &&
310
+ (paypalEnabled ||
311
+ cardEnabled ||
312
+ availablePaymentMethods.some((m) => !isPayPal(m.id))) && (
313
+ <>
314
+ <RadioGroup
315
+ value={selectedPaymentMethod}
316
+ onChange={(value: string) => setPaymentMethod(value)}
317
+ >
318
+ {availablePaymentMethods
319
+ .filter((m) => {
320
+ if (m.id === "pp_paypal_paypal" && !paypalEnabled) return false
321
+ if (m.id === "pp_paypal_card_paypal_card" && !cardEnabled) return false
322
+ return true
323
+ })
324
+ .map((paymentMethod) => (
325
+ <div key={paymentMethod.id}>
326
+ {isStripeLike(paymentMethod.id) ? (
327
+ <StripeCardContainer
328
+ paymentProviderId={paymentMethod.id}
329
+ selectedPaymentOptionId={selectedPaymentMethod}
330
+ paymentInfoMap={paymentInfoMap}
331
+ setCardBrand={setCardBrand}
332
+ setError={setError}
333
+ setCardComplete={setCardComplete}
334
+ />
335
+ ) : (
336
+ <PaymentContainer
337
+ paymentInfoMap={{
338
+ ...paymentInfoMap,
339
+ ...(isPayPal(paymentMethod.id)
340
+ ? {
341
+ [paymentMethod.id]: {
342
+ ...(paymentInfoMap[paymentMethod.id] || {}),
343
+ title:
344
+ paymentMethod.id === "pp_paypal_card_paypal_card"
345
+ ? cardTitle
346
+ : paypalTitle,
347
+ },
348
+ }
349
+ : {}),
350
+ }}
351
+ paymentProviderId={paymentMethod.id}
352
+ selectedPaymentOptionId={selectedPaymentMethod}
353
+ />
354
+ )}
355
+ </div>
356
+ ))}
357
+ </RadioGroup>
358
+
359
+ {isPayPal(selectedPaymentMethod) && (
360
+ <MedusaNextPayPalAdapter
361
+ cartId={cart.id}
362
+ selectedProviderId={selectedPaymentMethod}
363
+ baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
364
+ publishableApiKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}
365
+ onSuccess={() => placeOrder(cart.id)}
366
+ onError={(msg) => setError(msg)}
367
+ />
368
+ )}
369
+ </>
370
+ )}
371
+
372
+ <ErrorMessage
373
+ error={error}
374
+ data-testid="payment-method-error-message"
375
+ />
376
+
377
+ <Button
378
+ size="large"
379
+ className="mt-6"
380
+ onClick={handleSubmit}
381
+ isLoading={isLoading}
382
+ disabled={
383
+ (isStripeLike(selectedPaymentMethod) && !cardComplete) ||
384
+ (!selectedPaymentMethod && !paidByGiftcard) ||
385
+ isPayPal(selectedPaymentMethod)
386
+ }
387
+ data-testid="submit-payment-button"
388
+ >
389
+ {!activeSession && isStripeLike(selectedPaymentMethod)
390
+ ? " Enter card details"
391
+ : "Continue to review"}
392
+ </Button>
393
+ </div>
394
+ </div>
395
+
396
+ <Divider className="mt-8" />
397
+ </div>
398
+ )
399
+ }
400
+
401
+ export default Payment
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Adapter Usage
407
+
408
+ Render the adapter only when the selected payment method is a PayPal provider:
409
+
410
+ ```tsx
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
+ ---
424
+
425
+ ## Runtime Config Endpoint
426
+
427
+ The storefront reads PayPal runtime settings from:
428
+
429
+ ```txt
430
+ /store/paypal/config
431
+ ```
432
+
433
+ This endpoint is used to control:
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
+ }
467
+ ```
468
+
469
+ If your backend uses custom IDs, pass them through `providerIds`.
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