@libreapps/checkout 2.0.1
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE.md +21 -0
- package/README.md +211 -0
- package/client.ts +318 -0
- package/dist/client.d.ts +80 -0
- package/dist/client.js +229 -0
- package/dist/client.js.map +1 -0
- package/dist/elements/checkout-form.d.ts +14 -0
- package/dist/elements/checkout-form.js +112 -0
- package/dist/elements/checkout-form.js.map +1 -0
- package/dist/elements/index.d.ts +7 -0
- package/dist/elements/index.js +7 -0
- package/dist/elements/index.js.map +1 -0
- package/dist/embed/index.d.ts +6 -0
- package/dist/embed/index.js +7 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/libreapps-checkout.d.ts +32 -0
- package/dist/embed/libreapps-checkout.js +131 -0
- package/dist/embed/libreapps-checkout.js.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-checkout.d.ts +42 -0
- package/dist/hooks/use-checkout.js +168 -0
- package/dist/hooks/use-checkout.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/elements/checkout-form.tsx +543 -0
- package/elements/index.ts +8 -0
- package/embed/index.ts +7 -0
- package/embed/libreapps-checkout.ts +172 -0
- package/hooks/index.ts +6 -0
- package/hooks/use-checkout.tsx +244 -0
- package/index.ts +70 -0
- package/package.json +57 -0
- package/tsconfig.json +15 -0
- package/types.ts +301 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @libreapps/checkout - CheckoutForm
|
|
5
|
+
*
|
|
6
|
+
* Complete checkout form component - similar to Stripe Checkout
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react'
|
|
10
|
+
import { useCheckout } from '../hooks/use-checkout'
|
|
11
|
+
import type { CheckoutAddress } from '../types'
|
|
12
|
+
|
|
13
|
+
// Using cn utility pattern from @libreapps/ui
|
|
14
|
+
const cn = (...classes: (string | undefined | false)[]) => classes.filter(Boolean).join(' ')
|
|
15
|
+
|
|
16
|
+
interface CheckoutFormProps {
|
|
17
|
+
/** Additional class names */
|
|
18
|
+
className?: string
|
|
19
|
+
|
|
20
|
+
/** Show order summary */
|
|
21
|
+
showSummary?: boolean
|
|
22
|
+
|
|
23
|
+
/** Enable express checkout (Apple Pay, Google Pay) */
|
|
24
|
+
expressCheckout?: boolean
|
|
25
|
+
|
|
26
|
+
/** Collect phone number */
|
|
27
|
+
collectPhone?: boolean
|
|
28
|
+
|
|
29
|
+
/** Custom submit button text */
|
|
30
|
+
submitText?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function CheckoutForm({
|
|
34
|
+
className,
|
|
35
|
+
showSummary = true,
|
|
36
|
+
expressCheckout = true,
|
|
37
|
+
collectPhone = false,
|
|
38
|
+
submitText = 'Pay Now'
|
|
39
|
+
}: CheckoutFormProps) {
|
|
40
|
+
const {
|
|
41
|
+
session,
|
|
42
|
+
step,
|
|
43
|
+
isLoading,
|
|
44
|
+
isProcessing,
|
|
45
|
+
error,
|
|
46
|
+
updateShipping,
|
|
47
|
+
confirmPayment,
|
|
48
|
+
nextStep,
|
|
49
|
+
prevStep
|
|
50
|
+
} = useCheckout()
|
|
51
|
+
|
|
52
|
+
if (!session) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn('libreapps-checkout-form libreapps-checkout-empty', className)}>
|
|
55
|
+
<p>No checkout session</p>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={cn('libreapps-checkout-form', className)}>
|
|
62
|
+
{/* Progress Steps */}
|
|
63
|
+
<div className="libreapps-checkout-steps">
|
|
64
|
+
<CheckoutSteps currentStep={step} />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Error Display */}
|
|
68
|
+
{error && (
|
|
69
|
+
<div className="libreapps-checkout-error">
|
|
70
|
+
<p>{error.message}</p>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Step Content */}
|
|
75
|
+
<div className="libreapps-checkout-content">
|
|
76
|
+
{step === 'cart' && showSummary && (
|
|
77
|
+
<OrderSummary session={session} onContinue={nextStep} />
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{step === 'shipping' && (
|
|
81
|
+
<ShippingForm
|
|
82
|
+
onSubmit={async (address) => {
|
|
83
|
+
await updateShipping(address)
|
|
84
|
+
nextStep()
|
|
85
|
+
}}
|
|
86
|
+
onBack={prevStep}
|
|
87
|
+
isLoading={isLoading}
|
|
88
|
+
collectPhone={collectPhone}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{step === 'payment' && (
|
|
93
|
+
<PaymentForm
|
|
94
|
+
session={session}
|
|
95
|
+
onSubmit={confirmPayment}
|
|
96
|
+
onBack={prevStep}
|
|
97
|
+
isProcessing={isProcessing}
|
|
98
|
+
expressCheckout={expressCheckout}
|
|
99
|
+
submitText={submitText}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{step === 'confirmation' && (
|
|
104
|
+
<Confirmation session={session} />
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Sub-components
|
|
112
|
+
|
|
113
|
+
function CheckoutSteps({ currentStep }: { currentStep: string }) {
|
|
114
|
+
const steps = [
|
|
115
|
+
{ id: 'cart', label: 'Cart' },
|
|
116
|
+
{ id: 'shipping', label: 'Shipping' },
|
|
117
|
+
{ id: 'payment', label: 'Payment' },
|
|
118
|
+
{ id: 'confirmation', label: 'Confirm' }
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
const currentIndex = steps.findIndex(s => s.id === currentStep)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="libreapps-steps">
|
|
125
|
+
{steps.map((step, index) => (
|
|
126
|
+
<div
|
|
127
|
+
key={step.id}
|
|
128
|
+
className={cn(
|
|
129
|
+
'libreapps-step',
|
|
130
|
+
index < currentIndex && 'libreapps-step-completed',
|
|
131
|
+
index === currentIndex && 'libreapps-step-active'
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<span className="libreapps-step-number">{index + 1}</span>
|
|
135
|
+
<span className="libreapps-step-label">{step.label}</span>
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function OrderSummary({
|
|
143
|
+
session,
|
|
144
|
+
onContinue
|
|
145
|
+
}: {
|
|
146
|
+
session: NonNullable<ReturnType<typeof useCheckout>['session']>
|
|
147
|
+
onContinue: () => void
|
|
148
|
+
}) {
|
|
149
|
+
const formatCurrency = (amount: number, currency: string) => {
|
|
150
|
+
return new Intl.NumberFormat('en-US', {
|
|
151
|
+
style: 'currency',
|
|
152
|
+
currency: currency.toUpperCase()
|
|
153
|
+
}).format(amount / 100)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="libreapps-order-summary">
|
|
158
|
+
<h3>Order Summary</h3>
|
|
159
|
+
|
|
160
|
+
<div className="libreapps-line-items">
|
|
161
|
+
{session.lineItems.map(item => (
|
|
162
|
+
<div key={item.id} className="libreapps-line-item">
|
|
163
|
+
{item.imageUrl && (
|
|
164
|
+
<img src={item.imageUrl} alt={item.name} className="libreapps-item-image" />
|
|
165
|
+
)}
|
|
166
|
+
<div className="libreapps-item-details">
|
|
167
|
+
<p className="libreapps-item-name">{item.name}</p>
|
|
168
|
+
{item.description && (
|
|
169
|
+
<p className="libreapps-item-desc">{item.description}</p>
|
|
170
|
+
)}
|
|
171
|
+
<p className="libreapps-item-qty">Qty: {item.quantity}</p>
|
|
172
|
+
</div>
|
|
173
|
+
<p className="libreapps-item-price">
|
|
174
|
+
{formatCurrency(item.totalPrice, item.currency)}
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="libreapps-totals">
|
|
181
|
+
<div className="libreapps-total-row">
|
|
182
|
+
<span>Subtotal</span>
|
|
183
|
+
<span>{formatCurrency(session.subtotal, session.currency)}</span>
|
|
184
|
+
</div>
|
|
185
|
+
{session.shipping > 0 && (
|
|
186
|
+
<div className="libreapps-total-row">
|
|
187
|
+
<span>Shipping</span>
|
|
188
|
+
<span>{formatCurrency(session.shipping, session.currency)}</span>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
{session.tax > 0 && (
|
|
192
|
+
<div className="libreapps-total-row">
|
|
193
|
+
<span>Tax</span>
|
|
194
|
+
<span>{formatCurrency(session.tax, session.currency)}</span>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
<div className="libreapps-total-row libreapps-total-final">
|
|
198
|
+
<span>Total</span>
|
|
199
|
+
<span>{formatCurrency(session.total, session.currency)}</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
className="libreapps-btn libreapps-btn-primary"
|
|
206
|
+
onClick={onContinue}
|
|
207
|
+
>
|
|
208
|
+
Continue to Shipping
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ShippingForm({
|
|
215
|
+
onSubmit,
|
|
216
|
+
onBack,
|
|
217
|
+
isLoading,
|
|
218
|
+
collectPhone
|
|
219
|
+
}: {
|
|
220
|
+
onSubmit: (address: CheckoutAddress) => Promise<void>
|
|
221
|
+
onBack: () => void
|
|
222
|
+
isLoading: boolean
|
|
223
|
+
collectPhone: boolean
|
|
224
|
+
}) {
|
|
225
|
+
const [address, setAddress] = useState<CheckoutAddress>({
|
|
226
|
+
line1: '',
|
|
227
|
+
line2: '',
|
|
228
|
+
city: '',
|
|
229
|
+
state: '',
|
|
230
|
+
postalCode: '',
|
|
231
|
+
country: 'US'
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
235
|
+
e.preventDefault()
|
|
236
|
+
await onSubmit(address)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<form className="libreapps-shipping-form" onSubmit={handleSubmit}>
|
|
241
|
+
<h3>Shipping Address</h3>
|
|
242
|
+
|
|
243
|
+
<div className="libreapps-form-field">
|
|
244
|
+
<label htmlFor="line1">Address</label>
|
|
245
|
+
<input
|
|
246
|
+
id="line1"
|
|
247
|
+
type="text"
|
|
248
|
+
required
|
|
249
|
+
placeholder="Street address"
|
|
250
|
+
value={address.line1}
|
|
251
|
+
onChange={e => setAddress(a => ({ ...a, line1: e.target.value }))}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div className="libreapps-form-field">
|
|
256
|
+
<label htmlFor="line2">Apartment, suite, etc. (optional)</label>
|
|
257
|
+
<input
|
|
258
|
+
id="line2"
|
|
259
|
+
type="text"
|
|
260
|
+
placeholder="Apt, suite, unit"
|
|
261
|
+
value={address.line2 ?? ''}
|
|
262
|
+
onChange={e => setAddress(a => ({ ...a, line2: e.target.value }))}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div className="libreapps-form-row">
|
|
267
|
+
<div className="libreapps-form-field">
|
|
268
|
+
<label htmlFor="city">City</label>
|
|
269
|
+
<input
|
|
270
|
+
id="city"
|
|
271
|
+
type="text"
|
|
272
|
+
required
|
|
273
|
+
placeholder="City"
|
|
274
|
+
value={address.city}
|
|
275
|
+
onChange={e => setAddress(a => ({ ...a, city: e.target.value }))}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="libreapps-form-field">
|
|
280
|
+
<label htmlFor="state">State</label>
|
|
281
|
+
<input
|
|
282
|
+
id="state"
|
|
283
|
+
type="text"
|
|
284
|
+
placeholder="State"
|
|
285
|
+
value={address.state ?? ''}
|
|
286
|
+
onChange={e => setAddress(a => ({ ...a, state: e.target.value }))}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div className="libreapps-form-row">
|
|
292
|
+
<div className="libreapps-form-field">
|
|
293
|
+
<label htmlFor="postalCode">Postal Code</label>
|
|
294
|
+
<input
|
|
295
|
+
id="postalCode"
|
|
296
|
+
type="text"
|
|
297
|
+
required
|
|
298
|
+
placeholder="ZIP / Postal code"
|
|
299
|
+
value={address.postalCode}
|
|
300
|
+
onChange={e => setAddress(a => ({ ...a, postalCode: e.target.value }))}
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div className="libreapps-form-field">
|
|
305
|
+
<label htmlFor="country">Country</label>
|
|
306
|
+
<select
|
|
307
|
+
id="country"
|
|
308
|
+
required
|
|
309
|
+
value={address.country}
|
|
310
|
+
onChange={e => setAddress(a => ({ ...a, country: e.target.value }))}
|
|
311
|
+
>
|
|
312
|
+
<option value="US">United States</option>
|
|
313
|
+
<option value="CA">Canada</option>
|
|
314
|
+
<option value="GB">United Kingdom</option>
|
|
315
|
+
<option value="AU">Australia</option>
|
|
316
|
+
<option value="DE">Germany</option>
|
|
317
|
+
<option value="FR">France</option>
|
|
318
|
+
<option value="JP">Japan</option>
|
|
319
|
+
</select>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div className="libreapps-form-actions">
|
|
324
|
+
<button type="button" className="libreapps-btn libreapps-btn-secondary" onClick={onBack}>
|
|
325
|
+
Back
|
|
326
|
+
</button>
|
|
327
|
+
<button type="submit" className="libreapps-btn libreapps-btn-primary" disabled={isLoading}>
|
|
328
|
+
{isLoading ? 'Saving...' : 'Continue to Payment'}
|
|
329
|
+
</button>
|
|
330
|
+
</div>
|
|
331
|
+
</form>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function PaymentForm({
|
|
336
|
+
session,
|
|
337
|
+
onSubmit,
|
|
338
|
+
onBack,
|
|
339
|
+
isProcessing,
|
|
340
|
+
expressCheckout,
|
|
341
|
+
submitText
|
|
342
|
+
}: {
|
|
343
|
+
session: NonNullable<ReturnType<typeof useCheckout>['session']>
|
|
344
|
+
onSubmit: ReturnType<typeof useCheckout>['confirmPayment']
|
|
345
|
+
onBack: () => void
|
|
346
|
+
isProcessing: boolean
|
|
347
|
+
expressCheckout: boolean
|
|
348
|
+
submitText: string
|
|
349
|
+
}) {
|
|
350
|
+
const [card, setCard] = useState({
|
|
351
|
+
number: '',
|
|
352
|
+
expMonth: '',
|
|
353
|
+
expYear: '',
|
|
354
|
+
cvc: ''
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const formatCurrency = (amount: number, currency: string) => {
|
|
358
|
+
return new Intl.NumberFormat('en-US', {
|
|
359
|
+
style: 'currency',
|
|
360
|
+
currency: currency.toUpperCase()
|
|
361
|
+
}).format(amount / 100)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
365
|
+
e.preventDefault()
|
|
366
|
+
await onSubmit({
|
|
367
|
+
type: 'card',
|
|
368
|
+
card: {
|
|
369
|
+
number: card.number.replace(/\s/g, ''),
|
|
370
|
+
expMonth: parseInt(card.expMonth, 10),
|
|
371
|
+
expYear: parseInt(card.expYear, 10),
|
|
372
|
+
cvc: card.cvc
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Format card number with spaces
|
|
378
|
+
const formatCardNumber = (value: string) => {
|
|
379
|
+
const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '')
|
|
380
|
+
const matches = v.match(/\d{4,16}/g)
|
|
381
|
+
const match = (matches && matches[0]) || ''
|
|
382
|
+
const parts = []
|
|
383
|
+
for (let i = 0, len = match.length; i < len; i += 4) {
|
|
384
|
+
parts.push(match.substring(i, i + 4))
|
|
385
|
+
}
|
|
386
|
+
return parts.length ? parts.join(' ') : value
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<div className="libreapps-payment-form">
|
|
391
|
+
<h3>Payment</h3>
|
|
392
|
+
|
|
393
|
+
{/* Express Checkout */}
|
|
394
|
+
{expressCheckout && (
|
|
395
|
+
<div className="libreapps-express-checkout">
|
|
396
|
+
<button type="button" className="libreapps-btn libreapps-btn-apple-pay">
|
|
397
|
+
<ApplePayIcon /> Pay
|
|
398
|
+
</button>
|
|
399
|
+
<button type="button" className="libreapps-btn libreapps-btn-google-pay">
|
|
400
|
+
<GooglePayIcon /> Pay
|
|
401
|
+
</button>
|
|
402
|
+
<div className="libreapps-divider">
|
|
403
|
+
<span>or pay with card</span>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{/* Card Form */}
|
|
409
|
+
<form onSubmit={handleSubmit}>
|
|
410
|
+
<div className="libreapps-form-field">
|
|
411
|
+
<label htmlFor="cardNumber">Card Number</label>
|
|
412
|
+
<input
|
|
413
|
+
id="cardNumber"
|
|
414
|
+
type="text"
|
|
415
|
+
required
|
|
416
|
+
placeholder="1234 5678 9012 3456"
|
|
417
|
+
value={card.number}
|
|
418
|
+
onChange={e => setCard(c => ({ ...c, number: formatCardNumber(e.target.value) }))}
|
|
419
|
+
maxLength={19}
|
|
420
|
+
autoComplete="cc-number"
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<div className="libreapps-form-row">
|
|
425
|
+
<div className="libreapps-form-field">
|
|
426
|
+
<label htmlFor="expMonth">Expiry Month</label>
|
|
427
|
+
<input
|
|
428
|
+
id="expMonth"
|
|
429
|
+
type="text"
|
|
430
|
+
required
|
|
431
|
+
placeholder="MM"
|
|
432
|
+
value={card.expMonth}
|
|
433
|
+
onChange={e => setCard(c => ({ ...c, expMonth: e.target.value.slice(0, 2) }))}
|
|
434
|
+
maxLength={2}
|
|
435
|
+
autoComplete="cc-exp-month"
|
|
436
|
+
/>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div className="libreapps-form-field">
|
|
440
|
+
<label htmlFor="expYear">Expiry Year</label>
|
|
441
|
+
<input
|
|
442
|
+
id="expYear"
|
|
443
|
+
type="text"
|
|
444
|
+
required
|
|
445
|
+
placeholder="YY"
|
|
446
|
+
value={card.expYear}
|
|
447
|
+
onChange={e => setCard(c => ({ ...c, expYear: e.target.value.slice(0, 2) }))}
|
|
448
|
+
maxLength={2}
|
|
449
|
+
autoComplete="cc-exp-year"
|
|
450
|
+
/>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<div className="libreapps-form-field">
|
|
454
|
+
<label htmlFor="cvc">CVC</label>
|
|
455
|
+
<input
|
|
456
|
+
id="cvc"
|
|
457
|
+
type="text"
|
|
458
|
+
required
|
|
459
|
+
placeholder="123"
|
|
460
|
+
value={card.cvc}
|
|
461
|
+
onChange={e => setCard(c => ({ ...c, cvc: e.target.value.slice(0, 4) }))}
|
|
462
|
+
maxLength={4}
|
|
463
|
+
autoComplete="cc-csc"
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<div className="libreapps-payment-total">
|
|
469
|
+
<span>Total</span>
|
|
470
|
+
<span>{formatCurrency(session.total, session.currency)}</span>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<div className="libreapps-form-actions">
|
|
474
|
+
<button type="button" className="libreapps-btn libreapps-btn-secondary" onClick={onBack}>
|
|
475
|
+
Back
|
|
476
|
+
</button>
|
|
477
|
+
<button type="submit" className="libreapps-btn libreapps-btn-primary" disabled={isProcessing}>
|
|
478
|
+
{isProcessing ? 'Processing...' : submitText}
|
|
479
|
+
</button>
|
|
480
|
+
</div>
|
|
481
|
+
</form>
|
|
482
|
+
|
|
483
|
+
<p className="libreapps-secure-badge">
|
|
484
|
+
<LockIcon /> Secured by LibreApps
|
|
485
|
+
</p>
|
|
486
|
+
</div>
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function Confirmation({
|
|
491
|
+
session
|
|
492
|
+
}: {
|
|
493
|
+
session: NonNullable<ReturnType<typeof useCheckout>['session']>
|
|
494
|
+
}) {
|
|
495
|
+
return (
|
|
496
|
+
<div className="libreapps-confirmation">
|
|
497
|
+
<div className="libreapps-success-icon">
|
|
498
|
+
<CheckIcon />
|
|
499
|
+
</div>
|
|
500
|
+
<h3>Payment Successful!</h3>
|
|
501
|
+
<p>Thank you for your order.</p>
|
|
502
|
+
<p className="libreapps-order-id">Order ID: {session.id}</p>
|
|
503
|
+
{session.customer?.email && (
|
|
504
|
+
<p>A confirmation email has been sent to {session.customer.email}</p>
|
|
505
|
+
)}
|
|
506
|
+
</div>
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Icons
|
|
511
|
+
function ApplePayIcon() {
|
|
512
|
+
return (
|
|
513
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
|
514
|
+
<path d="M17.72 8.2c-.1.08-.97.56-1.44 1.06a3.26 3.26 0 00-.76 2.09c0 2.43 2.13 3.28 2.2 3.32-.01.05-.34 1.18-1.13 2.33-.68.98-1.39 1.96-2.5 1.96-1.1 0-1.45-.64-2.71-.64-1.24 0-1.68.66-2.7.66-1.03 0-1.71-.91-2.5-2.02-.92-1.28-1.71-3.27-1.71-5.14 0-3.02 1.96-4.62 3.9-4.62 1.03 0 1.88.67 2.53.67.63 0 1.6-.72 2.8-.72.45 0 2.07.04 3.14 1.56l-.12.09zM14.54 5.1c.5-.6.86-1.43.86-2.26 0-.11-.01-.24-.03-.33-.82.03-1.8.54-2.39 1.22-.47.53-.9 1.37-.9 2.2 0 .13.02.26.04.3.06.01.16.02.26.02.74 0 1.67-.49 2.16-1.15z"/>
|
|
515
|
+
</svg>
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function GooglePayIcon() {
|
|
520
|
+
return (
|
|
521
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
|
522
|
+
<path d="M12 11.9l-1.79 1.79c-.34.34-.34.89 0 1.23.34.34.89.34 1.23 0l1.79-1.79 1.79 1.79c.34.34.89.34 1.23 0 .34-.34.34-.89 0-1.23L14.46 11.9l1.79-1.79c.34-.34.34-.89 0-1.23-.34-.34-.89-.34-1.23 0L13.23 10.67l-1.79-1.79c-.34-.34-.89-.34-1.23 0-.34.34-.34.89 0 1.23L12 11.9z"/>
|
|
523
|
+
</svg>
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function LockIcon() {
|
|
528
|
+
return (
|
|
529
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
|
|
530
|
+
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
|
531
|
+
</svg>
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function CheckIcon() {
|
|
536
|
+
return (
|
|
537
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="48" height="48">
|
|
538
|
+
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
|
539
|
+
</svg>
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export default CheckoutForm
|