@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,361 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@medusajs/ui"
|
|
4
|
+
import { OnApproveActions, OnApproveData } from "@paypal/paypal-js"
|
|
5
|
+
import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js"
|
|
6
|
+
import { useElements, useStripe } from "@stripe/react-stripe-js"
|
|
7
|
+
import React, { useState } from "react"
|
|
8
|
+
import ErrorMessage from "../error-message"
|
|
9
|
+
import Spinner from "@modules/common/icons/spinner"
|
|
10
|
+
import { placeOrder } from "@lib/data/cart"
|
|
11
|
+
import { HttpTypes } from "@medusajs/types"
|
|
12
|
+
import { isManual, isPaypal, isPaytree, isStripe } from "@lib/constants"
|
|
13
|
+
|
|
14
|
+
type PaymentButtonProps = {
|
|
15
|
+
cart: HttpTypes.StoreCart
|
|
16
|
+
"data-testid": string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PaymentButton: React.FC<PaymentButtonProps> = ({
|
|
20
|
+
cart,
|
|
21
|
+
"data-testid": dataTestId,
|
|
22
|
+
}) => {
|
|
23
|
+
const notReady =
|
|
24
|
+
!cart ||
|
|
25
|
+
!cart.shipping_address ||
|
|
26
|
+
!cart.billing_address ||
|
|
27
|
+
!cart.email ||
|
|
28
|
+
(cart.shipping_methods?.length ?? 0) < 1
|
|
29
|
+
|
|
30
|
+
// TODO: Add this once gift cards are implemented
|
|
31
|
+
// const paidByGiftcard =
|
|
32
|
+
// cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0
|
|
33
|
+
|
|
34
|
+
// if (paidByGiftcard) {
|
|
35
|
+
// return <GiftCardPaymentButton />
|
|
36
|
+
// }
|
|
37
|
+
|
|
38
|
+
const paymentSession = cart.payment_collection?.payment_sessions?.[0]
|
|
39
|
+
|
|
40
|
+
switch (true) {
|
|
41
|
+
case isStripe(paymentSession?.provider_id):
|
|
42
|
+
return (
|
|
43
|
+
<StripePaymentButton
|
|
44
|
+
notReady={notReady}
|
|
45
|
+
cart={cart}
|
|
46
|
+
data-testid={dataTestId}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
case isManual(paymentSession?.provider_id):
|
|
50
|
+
return (
|
|
51
|
+
<ManualTestPaymentButton notReady={notReady} data-testid={dataTestId} />
|
|
52
|
+
)
|
|
53
|
+
case isPaypal(paymentSession?.provider_id):
|
|
54
|
+
return (
|
|
55
|
+
<PayPalPaymentButton
|
|
56
|
+
notReady={notReady}
|
|
57
|
+
cart={cart}
|
|
58
|
+
data-testid={dataTestId}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
case isPaytree(paymentSession?.provider_id):
|
|
62
|
+
return (
|
|
63
|
+
<PaytreePaymentButton
|
|
64
|
+
notReady={notReady}
|
|
65
|
+
cart={cart}
|
|
66
|
+
data-testid={dataTestId}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
default:
|
|
70
|
+
return <Button disabled>Select a payment method</Button>
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const GiftCardPaymentButton = () => {
|
|
75
|
+
const [submitting, setSubmitting] = useState(false)
|
|
76
|
+
|
|
77
|
+
const handleOrder = async () => {
|
|
78
|
+
setSubmitting(true)
|
|
79
|
+
await placeOrder()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Button
|
|
84
|
+
onClick={handleOrder}
|
|
85
|
+
isLoading={submitting}
|
|
86
|
+
data-testid="submit-order-button"
|
|
87
|
+
>
|
|
88
|
+
Place order
|
|
89
|
+
</Button>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const StripePaymentButton = ({
|
|
94
|
+
cart,
|
|
95
|
+
notReady,
|
|
96
|
+
"data-testid": dataTestId,
|
|
97
|
+
}: {
|
|
98
|
+
cart: HttpTypes.StoreCart
|
|
99
|
+
notReady: boolean
|
|
100
|
+
"data-testid"?: string
|
|
101
|
+
}) => {
|
|
102
|
+
const [submitting, setSubmitting] = useState(false)
|
|
103
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
104
|
+
|
|
105
|
+
const onPaymentCompleted = async () => {
|
|
106
|
+
await placeOrder()
|
|
107
|
+
.catch((err) => {
|
|
108
|
+
setErrorMessage(err.message)
|
|
109
|
+
})
|
|
110
|
+
.finally(() => {
|
|
111
|
+
setSubmitting(false)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const stripe = useStripe()
|
|
116
|
+
const elements = useElements()
|
|
117
|
+
const card = elements?.getElement("card")
|
|
118
|
+
|
|
119
|
+
const session = cart.payment_collection?.payment_sessions?.find(
|
|
120
|
+
(s) => s.status === "pending"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const disabled = !stripe || !elements ? true : false
|
|
124
|
+
|
|
125
|
+
const handlePayment = async () => {
|
|
126
|
+
setSubmitting(true)
|
|
127
|
+
|
|
128
|
+
if (!stripe || !elements || !card || !cart) {
|
|
129
|
+
setSubmitting(false)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await stripe
|
|
134
|
+
.confirmCardPayment(session?.data.client_secret as string, {
|
|
135
|
+
payment_method: {
|
|
136
|
+
card: card,
|
|
137
|
+
billing_details: {
|
|
138
|
+
name:
|
|
139
|
+
cart.billing_address?.first_name +
|
|
140
|
+
" " +
|
|
141
|
+
cart.billing_address?.last_name,
|
|
142
|
+
address: {
|
|
143
|
+
city: cart.billing_address?.city ?? undefined,
|
|
144
|
+
country: cart.billing_address?.country_code ?? undefined,
|
|
145
|
+
line1: cart.billing_address?.address_1 ?? undefined,
|
|
146
|
+
line2: cart.billing_address?.address_2 ?? undefined,
|
|
147
|
+
postal_code: cart.billing_address?.postal_code ?? undefined,
|
|
148
|
+
state: cart.billing_address?.province ?? undefined,
|
|
149
|
+
},
|
|
150
|
+
email: cart.email,
|
|
151
|
+
phone: cart.billing_address?.phone ?? undefined,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
.then(({ error, paymentIntent }) => {
|
|
156
|
+
if (error) {
|
|
157
|
+
const pi = error.payment_intent
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
(pi && pi.status === "requires_capture") ||
|
|
161
|
+
(pi && pi.status === "succeeded")
|
|
162
|
+
) {
|
|
163
|
+
onPaymentCompleted()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setErrorMessage(error.message || null)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
(paymentIntent && paymentIntent.status === "requires_capture") ||
|
|
172
|
+
paymentIntent.status === "succeeded"
|
|
173
|
+
) {
|
|
174
|
+
return onPaymentCompleted()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<Button
|
|
184
|
+
disabled={disabled || notReady}
|
|
185
|
+
onClick={handlePayment}
|
|
186
|
+
size="large"
|
|
187
|
+
isLoading={submitting}
|
|
188
|
+
data-testid={dataTestId}
|
|
189
|
+
>
|
|
190
|
+
Place order
|
|
191
|
+
</Button>
|
|
192
|
+
<ErrorMessage
|
|
193
|
+
error={errorMessage}
|
|
194
|
+
data-testid="stripe-payment-error-message"
|
|
195
|
+
/>
|
|
196
|
+
</>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const PayPalPaymentButton = ({
|
|
201
|
+
cart,
|
|
202
|
+
notReady,
|
|
203
|
+
"data-testid": dataTestId,
|
|
204
|
+
}: {
|
|
205
|
+
cart: HttpTypes.StoreCart
|
|
206
|
+
notReady: boolean
|
|
207
|
+
"data-testid"?: string
|
|
208
|
+
}) => {
|
|
209
|
+
const [submitting, setSubmitting] = useState(false)
|
|
210
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
211
|
+
|
|
212
|
+
const onPaymentCompleted = async () => {
|
|
213
|
+
await placeOrder()
|
|
214
|
+
.catch((err) => {
|
|
215
|
+
setErrorMessage(err.message)
|
|
216
|
+
})
|
|
217
|
+
.finally(() => {
|
|
218
|
+
setSubmitting(false)
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const session = cart.payment_collection?.payment_sessions?.find(
|
|
223
|
+
(s) => s.status === "pending"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const handlePayment = async (
|
|
227
|
+
_data: OnApproveData,
|
|
228
|
+
actions: OnApproveActions
|
|
229
|
+
) => {
|
|
230
|
+
actions?.order
|
|
231
|
+
?.authorize()
|
|
232
|
+
.then((authorization) => {
|
|
233
|
+
if (authorization.status !== "COMPLETED") {
|
|
234
|
+
setErrorMessage(`An error occurred, status: ${authorization.status}`)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
onPaymentCompleted()
|
|
238
|
+
})
|
|
239
|
+
.catch(() => {
|
|
240
|
+
setErrorMessage(`An unknown error occurred, please try again.`)
|
|
241
|
+
setSubmitting(false)
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const [{ isPending, isResolved }] = usePayPalScriptReducer()
|
|
246
|
+
|
|
247
|
+
if (isPending) {
|
|
248
|
+
return <Spinner />
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isResolved) {
|
|
252
|
+
return (
|
|
253
|
+
<>
|
|
254
|
+
<PayPalButtons
|
|
255
|
+
style={{ layout: "horizontal" }}
|
|
256
|
+
createOrder={async () => session?.data.id as string}
|
|
257
|
+
onApprove={handlePayment}
|
|
258
|
+
disabled={notReady || submitting || isPending}
|
|
259
|
+
data-testid={dataTestId}
|
|
260
|
+
/>
|
|
261
|
+
<ErrorMessage
|
|
262
|
+
error={errorMessage}
|
|
263
|
+
data-testid="paypal-payment-error-message"
|
|
264
|
+
/>
|
|
265
|
+
</>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const PaytreePaymentButton = ({
|
|
271
|
+
cart,
|
|
272
|
+
notReady,
|
|
273
|
+
"data-testid": dataTestId,
|
|
274
|
+
}: {
|
|
275
|
+
cart: HttpTypes.StoreCart
|
|
276
|
+
notReady: boolean
|
|
277
|
+
"data-testid"?: string
|
|
278
|
+
}) => {
|
|
279
|
+
const [submitting, setSubmitting] = useState(false)
|
|
280
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
281
|
+
|
|
282
|
+
const session = cart.payment_collection?.payment_sessions?.find(
|
|
283
|
+
(s) => s.status === "pending"
|
|
284
|
+
)
|
|
285
|
+
const paymentLink = session?.data?.payment_link as string | undefined
|
|
286
|
+
|
|
287
|
+
const handlePayment = () => {
|
|
288
|
+
setSubmitting(true)
|
|
289
|
+
|
|
290
|
+
if (!paymentLink) {
|
|
291
|
+
setErrorMessage(
|
|
292
|
+
"Payment link is not ready yet. Please go back to the payment step and select your method again."
|
|
293
|
+
)
|
|
294
|
+
setSubmitting(false)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Hand off to Paytree's hosted payment page. After paying, the customer is
|
|
299
|
+
// redirected back to the return URL, which finalizes the order.
|
|
300
|
+
window.location.href = paymentLink
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<>
|
|
305
|
+
<Button
|
|
306
|
+
disabled={notReady || !paymentLink}
|
|
307
|
+
isLoading={submitting}
|
|
308
|
+
onClick={handlePayment}
|
|
309
|
+
size="large"
|
|
310
|
+
data-testid={dataTestId}
|
|
311
|
+
>
|
|
312
|
+
Pay with Paytree
|
|
313
|
+
</Button>
|
|
314
|
+
<ErrorMessage
|
|
315
|
+
error={errorMessage}
|
|
316
|
+
data-testid="paytree-payment-error-message"
|
|
317
|
+
/>
|
|
318
|
+
</>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const ManualTestPaymentButton = ({ notReady }: { notReady: boolean }) => {
|
|
323
|
+
const [submitting, setSubmitting] = useState(false)
|
|
324
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
325
|
+
|
|
326
|
+
const onPaymentCompleted = async () => {
|
|
327
|
+
await placeOrder()
|
|
328
|
+
.catch((err) => {
|
|
329
|
+
setErrorMessage(err.message)
|
|
330
|
+
})
|
|
331
|
+
.finally(() => {
|
|
332
|
+
setSubmitting(false)
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const handlePayment = () => {
|
|
337
|
+
setSubmitting(true)
|
|
338
|
+
|
|
339
|
+
onPaymentCompleted()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<>
|
|
344
|
+
<Button
|
|
345
|
+
disabled={notReady}
|
|
346
|
+
isLoading={submitting}
|
|
347
|
+
onClick={handlePayment}
|
|
348
|
+
size="large"
|
|
349
|
+
data-testid="submit-order-button"
|
|
350
|
+
>
|
|
351
|
+
Place order
|
|
352
|
+
</Button>
|
|
353
|
+
<ErrorMessage
|
|
354
|
+
error={errorMessage}
|
|
355
|
+
data-testid="manual-payment-error-message"
|
|
356
|
+
/>
|
|
357
|
+
</>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export default PaymentButton
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useContext, useEffect, useMemo, useState } from "react"
|
|
4
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|
5
|
+
import { RadioGroup } from "@headlessui/react"
|
|
6
|
+
import ErrorMessage from "@modules/checkout/components/error-message"
|
|
7
|
+
import { CheckCircleSolid, CreditCard } from "@medusajs/icons"
|
|
8
|
+
import { Button, Container, Heading, Text, Tooltip, clx } from "@medusajs/ui"
|
|
9
|
+
import { CardElement } from "@stripe/react-stripe-js"
|
|
10
|
+
import { StripeCardElementOptions } from "@stripe/stripe-js"
|
|
11
|
+
|
|
12
|
+
import Divider from "@modules/common/components/divider"
|
|
13
|
+
import PaymentContainer from "@modules/checkout/components/payment-container"
|
|
14
|
+
import {
|
|
15
|
+
isStripe as isStripeFunc,
|
|
16
|
+
isPaytree as isPaytreeFunc,
|
|
17
|
+
paymentInfoMap,
|
|
18
|
+
} from "@lib/constants"
|
|
19
|
+
import { StripeContext } from "@modules/checkout/components/payment-wrapper"
|
|
20
|
+
import { initiatePaymentSession } from "@lib/data/cart"
|
|
21
|
+
import { listPaytreeMethods } from "@lib/data/paytree"
|
|
22
|
+
import PaytreeMethodIcon from "@modules/checkout/components/paytree-method-icon"
|
|
23
|
+
|
|
24
|
+
type PaytreeMethod = { method: string; label: string }
|
|
25
|
+
|
|
26
|
+
const Payment = ({
|
|
27
|
+
cart,
|
|
28
|
+
availablePaymentMethods,
|
|
29
|
+
}: {
|
|
30
|
+
cart: any
|
|
31
|
+
availablePaymentMethods: any[]
|
|
32
|
+
}) => {
|
|
33
|
+
const activeSession = cart.payment_collection?.payment_sessions?.find(
|
|
34
|
+
(paymentSession: any) => paymentSession.status === "pending"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
38
|
+
const [error, setError] = useState<string | null>(null)
|
|
39
|
+
const [cardBrand, setCardBrand] = useState<string | null>(null)
|
|
40
|
+
const [cardComplete, setCardComplete] = useState(false)
|
|
41
|
+
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
|
42
|
+
activeSession?.provider_id ?? ""
|
|
43
|
+
)
|
|
44
|
+
const [paytreeMethods, setPaytreeMethods] = useState<PaytreeMethod[]>([])
|
|
45
|
+
const [paytreeMethod, setPaytreeMethod] = useState<string>(
|
|
46
|
+
(activeSession?.data?.method as string) ?? ""
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const searchParams = useSearchParams()
|
|
50
|
+
const router = useRouter()
|
|
51
|
+
const pathname = usePathname()
|
|
52
|
+
|
|
53
|
+
const isOpen = searchParams.get("step") === "payment"
|
|
54
|
+
|
|
55
|
+
const isStripe = isStripeFunc(activeSession?.provider_id)
|
|
56
|
+
const isPaytreeSelected = isPaytreeFunc(selectedPaymentMethod)
|
|
57
|
+
const stripeReady = useContext(StripeContext)
|
|
58
|
+
|
|
59
|
+
const paidByGiftcard =
|
|
60
|
+
cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0
|
|
61
|
+
|
|
62
|
+
const paymentReady =
|
|
63
|
+
(activeSession && cart?.shipping_methods.length !== 0) || paidByGiftcard
|
|
64
|
+
|
|
65
|
+
const useOptions: StripeCardElementOptions = useMemo(() => {
|
|
66
|
+
return {
|
|
67
|
+
style: {
|
|
68
|
+
base: {
|
|
69
|
+
fontFamily: "Inter, sans-serif",
|
|
70
|
+
color: "#424270",
|
|
71
|
+
"::placeholder": {
|
|
72
|
+
color: "rgb(107 114 128)",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
classes: {
|
|
77
|
+
base: "pt-3 pb-1 block w-full h-11 px-4 mt-0 bg-ui-bg-field border rounded-md appearance-none focus:outline-none focus:ring-0 focus:shadow-borders-interactive-with-active border-ui-border-base hover:bg-ui-bg-field-hover transition-all duration-300 ease-in-out",
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
const createQueryString = useCallback(
|
|
83
|
+
(name: string, value: string) => {
|
|
84
|
+
const params = new URLSearchParams(searchParams)
|
|
85
|
+
params.set(name, value)
|
|
86
|
+
|
|
87
|
+
return params.toString()
|
|
88
|
+
},
|
|
89
|
+
[searchParams]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const handleEdit = () => {
|
|
93
|
+
router.push(pathname + "?" + createQueryString("step", "payment"), {
|
|
94
|
+
scroll: false,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handleSubmit = async () => {
|
|
99
|
+
setIsLoading(true)
|
|
100
|
+
try {
|
|
101
|
+
const shouldInputCard =
|
|
102
|
+
isStripeFunc(selectedPaymentMethod) && !activeSession
|
|
103
|
+
|
|
104
|
+
// Paytree re-initiates on every submit so the latest method choice is
|
|
105
|
+
// reflected in a fresh hosted-payment link.
|
|
106
|
+
if (!activeSession || isPaytreeSelected) {
|
|
107
|
+
await initiatePaymentSession(cart, {
|
|
108
|
+
provider_id: selectedPaymentMethod,
|
|
109
|
+
...(isPaytreeSelected
|
|
110
|
+
? { data: { method: paytreeMethod } }
|
|
111
|
+
: {}),
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!shouldInputCard) {
|
|
116
|
+
return router.push(
|
|
117
|
+
pathname + "?" + createQueryString("step", "review"),
|
|
118
|
+
{
|
|
119
|
+
scroll: false,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
setError(err.message)
|
|
125
|
+
} finally {
|
|
126
|
+
setIsLoading(false)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
setError(null)
|
|
132
|
+
}, [isOpen])
|
|
133
|
+
|
|
134
|
+
// Load the admin-curated Paytree methods once Paytree is selected.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!isPaytreeSelected || paytreeMethods.length > 0) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
listPaytreeMethods().then((methods) => {
|
|
140
|
+
setPaytreeMethods(methods)
|
|
141
|
+
setPaytreeMethod((current) => current || methods[0]?.method || "")
|
|
142
|
+
})
|
|
143
|
+
}, [isPaytreeSelected, paytreeMethods.length])
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="bg-white">
|
|
147
|
+
<div className="flex flex-row items-center justify-between mb-6">
|
|
148
|
+
<Heading
|
|
149
|
+
level="h2"
|
|
150
|
+
className={clx(
|
|
151
|
+
"flex flex-row text-3xl-regular gap-x-2 items-baseline",
|
|
152
|
+
{
|
|
153
|
+
"opacity-50 pointer-events-none select-none":
|
|
154
|
+
!isOpen && !paymentReady,
|
|
155
|
+
}
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
Payment
|
|
159
|
+
{!isOpen && paymentReady && <CheckCircleSolid />}
|
|
160
|
+
</Heading>
|
|
161
|
+
{!isOpen && paymentReady && (
|
|
162
|
+
<Text>
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleEdit}
|
|
165
|
+
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
|
|
166
|
+
data-testid="edit-payment-button"
|
|
167
|
+
>
|
|
168
|
+
Edit
|
|
169
|
+
</button>
|
|
170
|
+
</Text>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
<div>
|
|
174
|
+
<div className={isOpen ? "block" : "hidden"}>
|
|
175
|
+
{!paidByGiftcard && availablePaymentMethods?.length && (
|
|
176
|
+
<>
|
|
177
|
+
<RadioGroup
|
|
178
|
+
value={selectedPaymentMethod}
|
|
179
|
+
onChange={(value: string) => setSelectedPaymentMethod(value)}
|
|
180
|
+
>
|
|
181
|
+
{availablePaymentMethods
|
|
182
|
+
.sort((a, b) => {
|
|
183
|
+
return a.provider_id > b.provider_id ? 1 : -1
|
|
184
|
+
})
|
|
185
|
+
.map((paymentMethod) => {
|
|
186
|
+
return (
|
|
187
|
+
<PaymentContainer
|
|
188
|
+
paymentInfoMap={paymentInfoMap}
|
|
189
|
+
paymentProviderId={paymentMethod.id}
|
|
190
|
+
key={paymentMethod.id}
|
|
191
|
+
selectedPaymentOptionId={selectedPaymentMethod}
|
|
192
|
+
/>
|
|
193
|
+
)
|
|
194
|
+
})}
|
|
195
|
+
</RadioGroup>
|
|
196
|
+
{isStripe && stripeReady && (
|
|
197
|
+
<div className="mt-5 transition-all duration-150 ease-in-out">
|
|
198
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
199
|
+
Enter your card details:
|
|
200
|
+
</Text>
|
|
201
|
+
|
|
202
|
+
<CardElement
|
|
203
|
+
options={useOptions as StripeCardElementOptions}
|
|
204
|
+
onChange={(e) => {
|
|
205
|
+
setCardBrand(
|
|
206
|
+
e.brand &&
|
|
207
|
+
e.brand.charAt(0).toUpperCase() + e.brand.slice(1)
|
|
208
|
+
)
|
|
209
|
+
setError(e.error?.message || null)
|
|
210
|
+
setCardComplete(e.complete)
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{isPaytreeSelected && (
|
|
217
|
+
<div className="mt-5 transition-all duration-150 ease-in-out">
|
|
218
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-2">
|
|
219
|
+
Choose how you'd like to pay:
|
|
220
|
+
</Text>
|
|
221
|
+
{paytreeMethods.length === 0 ? (
|
|
222
|
+
<Text className="txt-medium text-ui-fg-subtle">
|
|
223
|
+
No payment methods are currently available.
|
|
224
|
+
</Text>
|
|
225
|
+
) : (
|
|
226
|
+
<RadioGroup
|
|
227
|
+
value={paytreeMethod}
|
|
228
|
+
onChange={(value: string) => setPaytreeMethod(value)}
|
|
229
|
+
className="flex flex-col gap-2"
|
|
230
|
+
>
|
|
231
|
+
{paytreeMethods.map((m) => (
|
|
232
|
+
<RadioGroup.Option
|
|
233
|
+
key={m.method}
|
|
234
|
+
value={m.method}
|
|
235
|
+
className={clx(
|
|
236
|
+
"flex items-center gap-x-3 cursor-pointer rounded-rounded border px-4 py-3 transition-colors",
|
|
237
|
+
{
|
|
238
|
+
"border-ui-border-interactive bg-ui-bg-interactive/5":
|
|
239
|
+
paytreeMethod === m.method,
|
|
240
|
+
"border-ui-border-base hover:bg-ui-bg-subtle-hover":
|
|
241
|
+
paytreeMethod !== m.method,
|
|
242
|
+
}
|
|
243
|
+
)}
|
|
244
|
+
>
|
|
245
|
+
<span className="flex h-7 w-7 items-center justify-center text-ui-fg-subtle">
|
|
246
|
+
<PaytreeMethodIcon method={m.method} />
|
|
247
|
+
</span>
|
|
248
|
+
<Text className="txt-medium text-ui-fg-base">
|
|
249
|
+
{m.label}
|
|
250
|
+
</Text>
|
|
251
|
+
{paytreeMethod === m.method && (
|
|
252
|
+
<CheckCircleSolid className="ml-auto text-ui-fg-interactive" />
|
|
253
|
+
)}
|
|
254
|
+
</RadioGroup.Option>
|
|
255
|
+
))}
|
|
256
|
+
</RadioGroup>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{paidByGiftcard && (
|
|
264
|
+
<div className="flex flex-col w-1/3">
|
|
265
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
266
|
+
Payment method
|
|
267
|
+
</Text>
|
|
268
|
+
<Text
|
|
269
|
+
className="txt-medium text-ui-fg-subtle"
|
|
270
|
+
data-testid="payment-method-summary"
|
|
271
|
+
>
|
|
272
|
+
Gift card
|
|
273
|
+
</Text>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
<ErrorMessage
|
|
278
|
+
error={error}
|
|
279
|
+
data-testid="payment-method-error-message"
|
|
280
|
+
/>
|
|
281
|
+
|
|
282
|
+
<Button
|
|
283
|
+
size="large"
|
|
284
|
+
className="mt-6"
|
|
285
|
+
onClick={handleSubmit}
|
|
286
|
+
isLoading={isLoading}
|
|
287
|
+
disabled={
|
|
288
|
+
(isStripe && !cardComplete) ||
|
|
289
|
+
(isPaytreeSelected && !paytreeMethod) ||
|
|
290
|
+
(!selectedPaymentMethod && !paidByGiftcard)
|
|
291
|
+
}
|
|
292
|
+
data-testid="submit-payment-button"
|
|
293
|
+
>
|
|
294
|
+
{!activeSession && isStripeFunc(selectedPaymentMethod)
|
|
295
|
+
? " Enter card details"
|
|
296
|
+
: "Continue to review"}
|
|
297
|
+
</Button>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div className={isOpen ? "hidden" : "block"}>
|
|
301
|
+
{cart && paymentReady && activeSession ? (
|
|
302
|
+
<div className="flex items-start gap-x-1 w-full">
|
|
303
|
+
<div className="flex flex-col w-1/3">
|
|
304
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
305
|
+
Payment method
|
|
306
|
+
</Text>
|
|
307
|
+
<Text
|
|
308
|
+
className="txt-medium text-ui-fg-subtle"
|
|
309
|
+
data-testid="payment-method-summary"
|
|
310
|
+
>
|
|
311
|
+
{paymentInfoMap[selectedPaymentMethod]?.title ||
|
|
312
|
+
selectedPaymentMethod}
|
|
313
|
+
</Text>
|
|
314
|
+
</div>
|
|
315
|
+
<div className="flex flex-col w-1/3">
|
|
316
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
317
|
+
Payment details
|
|
318
|
+
</Text>
|
|
319
|
+
<div
|
|
320
|
+
className="flex gap-2 txt-medium text-ui-fg-subtle items-center"
|
|
321
|
+
data-testid="payment-details-summary"
|
|
322
|
+
>
|
|
323
|
+
<Container className="flex items-center h-7 w-fit p-2 bg-ui-button-neutral-hover">
|
|
324
|
+
{paymentInfoMap[selectedPaymentMethod]?.icon || (
|
|
325
|
+
<CreditCard />
|
|
326
|
+
)}
|
|
327
|
+
</Container>
|
|
328
|
+
<Text>
|
|
329
|
+
{isStripeFunc(selectedPaymentMethod) && cardBrand
|
|
330
|
+
? cardBrand
|
|
331
|
+
: "Another step will appear"}
|
|
332
|
+
</Text>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
) : paidByGiftcard ? (
|
|
337
|
+
<div className="flex flex-col w-1/3">
|
|
338
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
339
|
+
Payment method
|
|
340
|
+
</Text>
|
|
341
|
+
<Text
|
|
342
|
+
className="txt-medium text-ui-fg-subtle"
|
|
343
|
+
data-testid="payment-method-summary"
|
|
344
|
+
>
|
|
345
|
+
Gift card
|
|
346
|
+
</Text>
|
|
347
|
+
</div>
|
|
348
|
+
) : null}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
<Divider className="mt-8" />
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export default Payment
|