@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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +24 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +211 -0
  5. package/client.ts +318 -0
  6. package/dist/client.d.ts +80 -0
  7. package/dist/client.js +229 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/elements/checkout-form.d.ts +14 -0
  10. package/dist/elements/checkout-form.js +112 -0
  11. package/dist/elements/checkout-form.js.map +1 -0
  12. package/dist/elements/index.d.ts +7 -0
  13. package/dist/elements/index.js +7 -0
  14. package/dist/elements/index.js.map +1 -0
  15. package/dist/embed/index.d.ts +6 -0
  16. package/dist/embed/index.js +7 -0
  17. package/dist/embed/index.js.map +1 -0
  18. package/dist/embed/libreapps-checkout.d.ts +32 -0
  19. package/dist/embed/libreapps-checkout.js +131 -0
  20. package/dist/embed/libreapps-checkout.js.map +1 -0
  21. package/dist/hooks/index.d.ts +5 -0
  22. package/dist/hooks/index.js +5 -0
  23. package/dist/hooks/index.js.map +1 -0
  24. package/dist/hooks/use-checkout.d.ts +42 -0
  25. package/dist/hooks/use-checkout.js +168 -0
  26. package/dist/hooks/use-checkout.js.map +1 -0
  27. package/dist/index.d.ts +48 -0
  28. package/dist/index.js +50 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/types.d.ts +219 -0
  31. package/dist/types.js +7 -0
  32. package/dist/types.js.map +1 -0
  33. package/elements/checkout-form.tsx +543 -0
  34. package/elements/index.ts +8 -0
  35. package/embed/index.ts +7 -0
  36. package/embed/libreapps-checkout.ts +172 -0
  37. package/hooks/index.ts +6 -0
  38. package/hooks/use-checkout.tsx +244 -0
  39. package/index.ts +70 -0
  40. package/package.json +57 -0
  41. package/tsconfig.json +15 -0
  42. 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
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @libreapps/checkout/elements
3
+ *
4
+ * React components for checkout
5
+ */
6
+
7
+ export { CheckoutForm } from './checkout-form'
8
+ export type { default as CheckoutFormType } from './checkout-form'
package/embed/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @libreapps/checkout/embed
3
+ *
4
+ * Drop-in checkout script
5
+ */
6
+
7
+ export { LibreAppsCheckoutEmbed, default } from './libreapps-checkout'