@bailierich/booking-components 2.0.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.
Files changed (83) hide show
  1. package/README.md +319 -0
  2. package/TENANT_DATA_INTEGRATION.md +402 -0
  3. package/TENANT_SETUP.md +316 -0
  4. package/components/BookingFlow/BookingFlow.tsx +790 -0
  5. package/components/BookingFlow/index.ts +5 -0
  6. package/components/BookingFlow/steps/AddonsSelection.tsx +118 -0
  7. package/components/BookingFlow/steps/Confirmation.tsx +185 -0
  8. package/components/BookingFlow/steps/ContactForm.tsx +292 -0
  9. package/components/BookingFlow/steps/CycleAwareDateSelection.tsx +277 -0
  10. package/components/BookingFlow/steps/DateSelection.tsx +473 -0
  11. package/components/BookingFlow/steps/ServiceSelection.tsx +315 -0
  12. package/components/BookingFlow/steps/TimeSelection.tsx +230 -0
  13. package/components/BookingFlow/steps/index.ts +10 -0
  14. package/components/BottomSheet/index.tsx +120 -0
  15. package/components/Forms/FormBlock.tsx +283 -0
  16. package/components/Forms/FormField.tsx +385 -0
  17. package/components/Forms/FormRenderer.tsx +216 -0
  18. package/components/Forms/FormValidation.ts +122 -0
  19. package/components/Forms/index.ts +4 -0
  20. package/components/HoldTimer/HoldTimer.tsx +266 -0
  21. package/components/HoldTimer/index.ts +2 -0
  22. package/components/SectionRenderer.tsx +558 -0
  23. package/components/Sections/About.tsx +145 -0
  24. package/components/Sections/BeforeAfter.tsx +81 -0
  25. package/components/Sections/BookingSection.tsx +76 -0
  26. package/components/Sections/Contact.tsx +103 -0
  27. package/components/Sections/FAQSection.tsx +239 -0
  28. package/components/Sections/FeatureContent.tsx +113 -0
  29. package/components/Sections/FeaturedLink.tsx +103 -0
  30. package/components/Sections/FixedInfoCard.tsx +189 -0
  31. package/components/Sections/Gallery.tsx +83 -0
  32. package/components/Sections/Header.tsx +78 -0
  33. package/components/Sections/Hero.tsx +178 -0
  34. package/components/Sections/ImageSection.tsx +147 -0
  35. package/components/Sections/InstagramFeed.tsx +38 -0
  36. package/components/Sections/LinkList.tsx +76 -0
  37. package/components/Sections/LocationMap.tsx +202 -0
  38. package/components/Sections/Logo.tsx +61 -0
  39. package/components/Sections/MinimalFooter.tsx +78 -0
  40. package/components/Sections/MinimalHeader.tsx +81 -0
  41. package/components/Sections/MinimalNavigation.tsx +63 -0
  42. package/components/Sections/Navbar.tsx +258 -0
  43. package/components/Sections/PricingTable.tsx +106 -0
  44. package/components/Sections/ScrollingTextDivider.tsx +138 -0
  45. package/components/Sections/ScrollingTextDivider.tsx.bak +138 -0
  46. package/components/Sections/ServicesPreview.tsx +129 -0
  47. package/components/Sections/SocialBar.tsx +177 -0
  48. package/components/Sections/Team.tsx +80 -0
  49. package/components/Sections/Testimonials.tsx +92 -0
  50. package/components/Sections/TextSection.tsx +116 -0
  51. package/components/Sections/VideoSection.tsx +178 -0
  52. package/components/Sections/index.ts +57 -0
  53. package/components/index.ts +21 -0
  54. package/dist/index-DAai7Glf.d.mts +474 -0
  55. package/dist/index-DAai7Glf.d.ts +474 -0
  56. package/dist/index.d.mts +1075 -0
  57. package/dist/index.d.ts +1075 -0
  58. package/dist/index.js +22 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/index.mjs +22 -0
  61. package/dist/index.mjs.map +1 -0
  62. package/dist/styles/index.d.mts +1 -0
  63. package/dist/styles/index.d.ts +1 -0
  64. package/dist/styles/index.js +2 -0
  65. package/dist/styles/index.js.map +1 -0
  66. package/dist/styles/index.mjs +2 -0
  67. package/dist/styles/index.mjs.map +1 -0
  68. package/docs/API.md +849 -0
  69. package/docs/CALLBACKS.md +760 -0
  70. package/docs/COMPLETE_SESSION_SUMMARY.md +404 -0
  71. package/docs/DATA_SHAPES.md +684 -0
  72. package/docs/MIGRATION.md +662 -0
  73. package/docs/PAYMENT_INTEGRATION.md +766 -0
  74. package/docs/SESSION_SUMMARY.md +185 -0
  75. package/docs/STYLING.md +735 -0
  76. package/index.ts +4 -0
  77. package/lib/storage.ts +239 -0
  78. package/package.json +59 -0
  79. package/styles/animations.ts +210 -0
  80. package/styles/index.ts +1 -0
  81. package/tsconfig.json +32 -0
  82. package/tsup.config.ts +13 -0
  83. package/types/index.ts +369 -0
@@ -0,0 +1,766 @@
1
+ # Payment Integration Guide
2
+
3
+ Complete guide for integrating payment providers with the booking components.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Square Integration](#square-integration)
9
+ - [Stripe Integration](#stripe-integration)
10
+ - [Deposit vs Full Payment](#deposit-vs-full-payment)
11
+ - [Handling Payment Errors](#handling-payment-errors)
12
+ - [Security Best Practices](#security-best-practices)
13
+
14
+ ---
15
+
16
+ ## Overview
17
+
18
+ The Confirmation component supports payment collection during the booking process. You can integrate Square, Stripe, or any other payment provider.
19
+
20
+ ### Payment Flow
21
+
22
+ 1. User completes booking details (service, date, time, contact info)
23
+ 2. User reaches confirmation step
24
+ 3. Payment form is presented (deposit or full payment)
25
+ 4. Payment is processed
26
+ 5. Booking is confirmed and saved
27
+ 6. Confirmation email is sent
28
+
29
+ ---
30
+
31
+ ## Square Integration
32
+
33
+ ### 1. Install Square SDK
34
+
35
+ ```bash
36
+ npm install square
37
+ ```
38
+
39
+ ### 2. Set Up Square Web Payments SDK
40
+
41
+ ```tsx
42
+ // lib/square.ts
43
+ export async function initializeSquarePayments() {
44
+ if (!window.Square) {
45
+ throw new Error('Square.js failed to load');
46
+ }
47
+
48
+ const payments = window.Square.payments(
49
+ process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID!,
50
+ process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID!
51
+ );
52
+
53
+ return payments;
54
+ }
55
+
56
+ export async function createSquareCard(payments: any) {
57
+ const card = await payments.card();
58
+ await card.attach('#square-card-container');
59
+ return card;
60
+ }
61
+ ```
62
+
63
+ ### 3. Create Payment Component
64
+
65
+ ```tsx
66
+ // components/SquarePayment.tsx
67
+ 'use client';
68
+
69
+ import { useEffect, useState } from 'react';
70
+ import { initializeSquarePayments, createSquareCard } from '@/lib/square';
71
+
72
+ interface SquarePaymentProps {
73
+ amount: number;
74
+ onPaymentSuccess: (paymentId: string) => void;
75
+ onPaymentError: (error: Error) => void;
76
+ }
77
+
78
+ export function SquarePayment({
79
+ amount,
80
+ onPaymentSuccess,
81
+ onPaymentError
82
+ }: SquarePaymentProps) {
83
+ const [card, setCard] = useState<any>(null);
84
+ const [isProcessing, setIsProcessing] = useState(false);
85
+
86
+ useEffect(() => {
87
+ async function setupSquare() {
88
+ try {
89
+ const payments = await initializeSquarePayments();
90
+ const squareCard = await createSquareCard(payments);
91
+ setCard(squareCard);
92
+ } catch (error) {
93
+ console.error('Failed to initialize Square:', error);
94
+ onPaymentError(new Error('Payment system initialization failed'));
95
+ }
96
+ }
97
+
98
+ setupSquare();
99
+
100
+ return () => {
101
+ if (card) {
102
+ card.destroy();
103
+ }
104
+ };
105
+ }, []);
106
+
107
+ const handlePayment = async () => {
108
+ if (!card || isProcessing) return;
109
+
110
+ setIsProcessing(true);
111
+
112
+ try {
113
+ // Tokenize card
114
+ const result = await card.tokenize();
115
+
116
+ if (result.status === 'OK') {
117
+ // Send token to backend
118
+ const response = await fetch('/api/payments/square', {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({
122
+ sourceId: result.token,
123
+ amount: amount * 100, // Convert to cents
124
+ currency: 'USD'
125
+ })
126
+ });
127
+
128
+ if (!response.ok) {
129
+ throw new Error('Payment processing failed');
130
+ }
131
+
132
+ const data = await response.json();
133
+ onPaymentSuccess(data.paymentId);
134
+ } else {
135
+ throw new Error(result.errors[0]?.message || 'Tokenization failed');
136
+ }
137
+ } catch (error) {
138
+ onPaymentError(error as Error);
139
+ } finally {
140
+ setIsProcessing(false);
141
+ }
142
+ };
143
+
144
+ return (
145
+ <div className="space-y-4">
146
+ <div id="square-card-container" className="min-h-[200px]" />
147
+
148
+ <button
149
+ onClick={handlePayment}
150
+ disabled={!card || isProcessing}
151
+ className="w-full py-3 px-6 rounded-lg text-white font-medium disabled:opacity-50"
152
+ style={{ backgroundColor: '#006AFF' }}
153
+ >
154
+ {isProcessing ? 'Processing...' : `Pay $${amount.toFixed(2)}`}
155
+ </button>
156
+ </div>
157
+ );
158
+ }
159
+ ```
160
+
161
+ ### 4. Backend API Route
162
+
163
+ ```tsx
164
+ // app/api/payments/square/route.ts
165
+ import { Client, Environment } from 'square';
166
+
167
+ const client = new Client({
168
+ accessToken: process.env.SQUARE_ACCESS_TOKEN!,
169
+ environment: Environment.Production
170
+ });
171
+
172
+ export async function POST(request: Request) {
173
+ try {
174
+ const { sourceId, amount, currency } = await request.json();
175
+
176
+ const { result } = await client.paymentsApi.createPayment({
177
+ sourceId,
178
+ amountMoney: {
179
+ amount: BigInt(amount),
180
+ currency
181
+ },
182
+ locationId: process.env.SQUARE_LOCATION_ID!,
183
+ idempotencyKey: crypto.randomUUID()
184
+ });
185
+
186
+ return Response.json({
187
+ paymentId: result.payment?.id,
188
+ status: result.payment?.status
189
+ });
190
+ } catch (error) {
191
+ console.error('Square payment error:', error);
192
+ return Response.json(
193
+ { error: 'Payment failed' },
194
+ { status: 500 }
195
+ );
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### 5. Use with Confirmation Component
201
+
202
+ ```tsx
203
+ import { Confirmation } from '@oviah/booking-components';
204
+ import { SquarePayment } from '@/components/SquarePayment';
205
+
206
+ <Confirmation
207
+ service={selectedService}
208
+ addons={selectedAddons}
209
+ selectedDate={selectedDate}
210
+ selectedTime={selectedTime}
211
+ contactInfo={contactInfo}
212
+ paymentProvider="square"
213
+ paymentConfig={{
214
+ depositPercentage: 20
215
+ }}
216
+ onConfirm={async () => {
217
+ // Payment handled by SquarePayment component
218
+ }}
219
+ colors={colors}
220
+ />
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Stripe Integration
226
+
227
+ ### 1. Install Stripe SDK
228
+
229
+ ```bash
230
+ npm install @stripe/stripe-js @stripe/react-stripe-js
231
+ ```
232
+
233
+ ### 2. Set Up Stripe Elements
234
+
235
+ ```tsx
236
+ // lib/stripe.ts
237
+ import { loadStripe } from '@stripe/stripe-js';
238
+
239
+ export const stripePromise = loadStripe(
240
+ process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
241
+ );
242
+ ```
243
+
244
+ ### 3. Create Payment Component
245
+
246
+ ```tsx
247
+ // components/StripePayment.tsx
248
+ 'use client';
249
+
250
+ import { useState } from 'react';
251
+ import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
252
+ import { stripePromise } from '@/lib/stripe';
253
+
254
+ interface StripePaymentFormProps {
255
+ amount: number;
256
+ onPaymentSuccess: (paymentIntentId: string) => void;
257
+ onPaymentError: (error: Error) => void;
258
+ }
259
+
260
+ function StripePaymentForm({
261
+ amount,
262
+ onPaymentSuccess,
263
+ onPaymentError
264
+ }: StripePaymentFormProps) {
265
+ const stripe = useStripe();
266
+ const elements = useElements();
267
+ const [isProcessing, setIsProcessing] = useState(false);
268
+
269
+ const handleSubmit = async (e: React.FormEvent) => {
270
+ e.preventDefault();
271
+
272
+ if (!stripe || !elements) return;
273
+
274
+ setIsProcessing(true);
275
+
276
+ try {
277
+ // Create payment intent on backend
278
+ const response = await fetch('/api/payments/stripe/create-intent', {
279
+ method: 'POST',
280
+ headers: { 'Content-Type': 'application/json' },
281
+ body: JSON.stringify({ amount: amount * 100 }) // Convert to cents
282
+ });
283
+
284
+ const { clientSecret } = await response.json();
285
+
286
+ // Confirm payment
287
+ const cardElement = elements.getElement(CardElement);
288
+ if (!cardElement) throw new Error('Card element not found');
289
+
290
+ const result = await stripe.confirmCardPayment(clientSecret, {
291
+ payment_method: {
292
+ card: cardElement
293
+ }
294
+ });
295
+
296
+ if (result.error) {
297
+ throw new Error(result.error.message);
298
+ }
299
+
300
+ onPaymentSuccess(result.paymentIntent.id);
301
+ } catch (error) {
302
+ onPaymentError(error as Error);
303
+ } finally {
304
+ setIsProcessing(false);
305
+ }
306
+ };
307
+
308
+ return (
309
+ <form onSubmit={handleSubmit} className="space-y-4">
310
+ <div className="p-4 border border-gray-200 rounded-lg">
311
+ <CardElement
312
+ options={{
313
+ style: {
314
+ base: {
315
+ fontSize: '16px',
316
+ color: '#424770',
317
+ '::placeholder': {
318
+ color: '#aab7c4'
319
+ }
320
+ },
321
+ invalid: {
322
+ color: '#9e2146'
323
+ }
324
+ }
325
+ }}
326
+ />
327
+ </div>
328
+
329
+ <button
330
+ type="submit"
331
+ disabled={!stripe || isProcessing}
332
+ className="w-full py-3 px-6 rounded-lg text-white font-medium disabled:opacity-50"
333
+ style={{ backgroundColor: '#635BFF' }}
334
+ >
335
+ {isProcessing ? 'Processing...' : `Pay $${amount.toFixed(2)}`}
336
+ </button>
337
+ </form>
338
+ );
339
+ }
340
+
341
+ export function StripePayment(props: StripePaymentFormProps) {
342
+ return (
343
+ <Elements stripe={stripePromise}>
344
+ <StripePaymentForm {...props} />
345
+ </Elements>
346
+ );
347
+ }
348
+ ```
349
+
350
+ ### 4. Backend API Routes
351
+
352
+ ```tsx
353
+ // app/api/payments/stripe/create-intent/route.ts
354
+ import Stripe from 'stripe';
355
+
356
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
357
+ apiVersion: '2023-10-16'
358
+ });
359
+
360
+ export async function POST(request: Request) {
361
+ try {
362
+ const { amount } = await request.json();
363
+
364
+ const paymentIntent = await stripe.paymentIntents.create({
365
+ amount,
366
+ currency: 'usd',
367
+ automatic_payment_methods: {
368
+ enabled: true
369
+ }
370
+ });
371
+
372
+ return Response.json({
373
+ clientSecret: paymentIntent.client_secret
374
+ });
375
+ } catch (error) {
376
+ console.error('Stripe payment intent error:', error);
377
+ return Response.json(
378
+ { error: 'Failed to create payment intent' },
379
+ { status: 500 }
380
+ );
381
+ }
382
+ }
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Deposit vs Full Payment
388
+
389
+ ### Configuration
390
+
391
+ ```tsx
392
+ const paymentConfig = {
393
+ depositPercentage: 20, // 20% deposit
394
+ requireDeposit: true // or false for full payment
395
+ };
396
+
397
+ <Confirmation
398
+ paymentConfig={paymentConfig}
399
+ // ... other props
400
+ />
401
+ ```
402
+
403
+ ### Calculating Amounts
404
+
405
+ ```tsx
406
+ function calculatePaymentAmounts(
407
+ servicePrice: number,
408
+ addonsPrice: number,
409
+ depositPercentage: number
410
+ ) {
411
+ const total = servicePrice + addonsPrice;
412
+ const deposit = depositPercentage > 0
413
+ ? (total * depositPercentage) / 100
414
+ : total;
415
+ const remaining = total - deposit;
416
+
417
+ return {
418
+ total: total.toFixed(2),
419
+ deposit: deposit.toFixed(2),
420
+ remaining: remaining.toFixed(2)
421
+ };
422
+ }
423
+
424
+ // Usage
425
+ const amounts = calculatePaymentAmounts(100, 25, 20);
426
+ // {
427
+ // total: "125.00",
428
+ // deposit: "25.00",
429
+ // remaining: "100.00"
430
+ // }
431
+ ```
432
+
433
+ ### Handling Remaining Balance
434
+
435
+ ```tsx
436
+ // Store deposit payment info
437
+ await prisma.booking.create({
438
+ data: {
439
+ serviceId: selectedService,
440
+ date: selectedDate,
441
+ time: selectedTime,
442
+ totalAmount: amounts.total,
443
+ depositAmount: amounts.deposit,
444
+ depositPaid: true,
445
+ depositPaymentId: paymentId,
446
+ remainingBalance: amounts.remaining,
447
+ balanceDueDate: appointmentDate // Due at appointment
448
+ }
449
+ });
450
+
451
+ // Send confirmation with balance due info
452
+ await sendConfirmationEmail({
453
+ to: contactInfo.email,
454
+ booking: bookingData,
455
+ depositAmount: amounts.deposit,
456
+ remainingBalance: amounts.remaining,
457
+ balanceDueText: `$${amounts.remaining} due at your appointment`
458
+ });
459
+ ```
460
+
461
+ ---
462
+
463
+ ## Handling Payment Errors
464
+
465
+ ### Error Types
466
+
467
+ ```typescript
468
+ enum PaymentErrorType {
469
+ CARD_DECLINED = 'card_declined',
470
+ INSUFFICIENT_FUNDS = 'insufficient_funds',
471
+ INVALID_CARD = 'invalid_card',
472
+ PROCESSING_ERROR = 'processing_error',
473
+ NETWORK_ERROR = 'network_error'
474
+ }
475
+
476
+ interface PaymentError {
477
+ type: PaymentErrorType;
478
+ message: string;
479
+ code?: string;
480
+ }
481
+ ```
482
+
483
+ ### Error Handling Implementation
484
+
485
+ ```tsx
486
+ function handlePaymentError(error: Error) {
487
+ let userMessage = 'Payment failed. Please try again.';
488
+
489
+ // Parse error message
490
+ if (error.message.includes('declined')) {
491
+ userMessage = 'Your card was declined. Please use a different payment method.';
492
+ } else if (error.message.includes('insufficient')) {
493
+ userMessage = 'Insufficient funds. Please use a different card.';
494
+ } else if (error.message.includes('invalid')) {
495
+ userMessage = 'Invalid card information. Please check and try again.';
496
+ } else if (error.message.includes('network')) {
497
+ userMessage = 'Network error. Please check your connection and try again.';
498
+ }
499
+
500
+ // Log error for debugging
501
+ console.error('Payment error:', error);
502
+
503
+ // Track error analytics
504
+ analytics.track('Payment Error', {
505
+ error: error.message,
506
+ timestamp: new Date().toISOString()
507
+ });
508
+
509
+ // Show user-friendly message
510
+ toast.error(userMessage);
511
+
512
+ // Optional: Send to error tracking service
513
+ if (typeof window !== 'undefined' && window.Sentry) {
514
+ window.Sentry.captureException(error, {
515
+ tags: { context: 'payment' }
516
+ });
517
+ }
518
+ }
519
+ ```
520
+
521
+ ### Retry Logic
522
+
523
+ ```tsx
524
+ async function processPaymentWithRetry(
525
+ paymentFn: () => Promise<string>,
526
+ maxRetries = 3
527
+ ): Promise<string> {
528
+ let lastError: Error | null = null;
529
+
530
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
531
+ try {
532
+ return await paymentFn();
533
+ } catch (error) {
534
+ lastError = error as Error;
535
+
536
+ // Don't retry for card declined or invalid card
537
+ if (
538
+ error.message.includes('declined') ||
539
+ error.message.includes('invalid')
540
+ ) {
541
+ throw error;
542
+ }
543
+
544
+ // Wait before retrying (exponential backoff)
545
+ if (attempt < maxRetries) {
546
+ await new Promise(resolve =>
547
+ setTimeout(resolve, Math.pow(2, attempt) * 1000)
548
+ );
549
+ }
550
+ }
551
+ }
552
+
553
+ throw lastError || new Error('Payment failed after retries');
554
+ }
555
+ ```
556
+
557
+ ---
558
+
559
+ ## Security Best Practices
560
+
561
+ ### 1. Never Store Card Details
562
+
563
+ ```tsx
564
+ // ❌ BAD - Never do this
565
+ const cardInfo = {
566
+ number: '4242424242424242',
567
+ cvv: '123',
568
+ expiry: '12/25'
569
+ };
570
+ localStorage.setItem('card', JSON.stringify(cardInfo));
571
+
572
+ // ✅ GOOD - Use payment provider tokens
573
+ const { token } = await stripe.createToken(cardElement);
574
+ // Only store/send the token, never raw card data
575
+ ```
576
+
577
+ ### 2. Use HTTPS Only
578
+
579
+ ```tsx
580
+ // next.config.js
581
+ module.exports = {
582
+ async headers() {
583
+ return [
584
+ {
585
+ source: '/:path*',
586
+ headers: [
587
+ {
588
+ key: 'Strict-Transport-Security',
589
+ value: 'max-age=31536000; includeSubDomains'
590
+ }
591
+ ]
592
+ }
593
+ ];
594
+ }
595
+ };
596
+ ```
597
+
598
+ ### 3. Validate on Backend
599
+
600
+ ```tsx
601
+ // app/api/payments/route.ts
602
+ export async function POST(request: Request) {
603
+ const { amount, sourceId } = await request.json();
604
+
605
+ // Validate amount
606
+ if (!amount || amount < 50) {
607
+ return Response.json(
608
+ { error: 'Invalid amount' },
609
+ { status: 400 }
610
+ );
611
+ }
612
+
613
+ // Validate source
614
+ if (!sourceId || typeof sourceId !== 'string') {
615
+ return Response.json(
616
+ { error: 'Invalid payment source' },
617
+ { status: 400 }
618
+ );
619
+ }
620
+
621
+ // Process payment...
622
+ }
623
+ ```
624
+
625
+ ### 4. Use Idempotency Keys
626
+
627
+ ```tsx
628
+ // Prevent duplicate charges
629
+ const idempotencyKey = `booking-${bookingId}-${Date.now()}`;
630
+
631
+ await stripe.paymentIntents.create({
632
+ amount,
633
+ currency: 'usd',
634
+ // ... other params
635
+ }, {
636
+ idempotencyKey
637
+ });
638
+ ```
639
+
640
+ ### 5. Implement Rate Limiting
641
+
642
+ ```tsx
643
+ // middleware.ts
644
+ import { Ratelimit } from '@upstash/ratelimit';
645
+ import { Redis } from '@upstash/redis';
646
+
647
+ const ratelimit = new Ratelimit({
648
+ redis: Redis.fromEnv(),
649
+ limiter: Ratelimit.slidingWindow(5, '1 m') // 5 requests per minute
650
+ });
651
+
652
+ export async function middleware(request: Request) {
653
+ if (request.url.includes('/api/payments')) {
654
+ const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
655
+ const { success } = await ratelimit.limit(ip);
656
+
657
+ if (!success) {
658
+ return new Response('Too many requests', { status: 429 });
659
+ }
660
+ }
661
+
662
+ return NextResponse.next();
663
+ }
664
+ ```
665
+
666
+ ---
667
+
668
+ ## Complete Integration Example
669
+
670
+ ```tsx
671
+ // app/booking/page.tsx
672
+ 'use client';
673
+
674
+ import { useState } from 'react';
675
+ import { BookingFlow } from '@oviah/booking-components';
676
+ import { SquarePayment } from '@/components/SquarePayment';
677
+ import { toast } from 'sonner';
678
+
679
+ export default function BookingPage() {
680
+ const [bookingData, setBookingData] = useState(null);
681
+
682
+ const handleBookingComplete = async (data: any) => {
683
+ try {
684
+ // 1. Create booking (pending payment)
685
+ const bookingResponse = await fetch('/api/bookings', {
686
+ method: 'POST',
687
+ headers: { 'Content-Type': 'application/json' },
688
+ body: JSON.stringify({
689
+ ...data,
690
+ status: 'pending_payment'
691
+ })
692
+ });
693
+
694
+ const booking = await bookingResponse.json();
695
+ setBookingData(booking);
696
+
697
+ // 2. Show payment UI
698
+ // Payment handled by embedded component
699
+
700
+ } catch (error) {
701
+ toast.error('Failed to create booking');
702
+ console.error(error);
703
+ }
704
+ };
705
+
706
+ const handlePaymentSuccess = async (paymentId: string) => {
707
+ try {
708
+ // Update booking with payment info
709
+ await fetch(`/api/bookings/${bookingData.id}`, {
710
+ method: 'PATCH',
711
+ headers: { 'Content-Type': 'application/json' },
712
+ body: JSON.stringify({
713
+ status: 'confirmed',
714
+ paymentId
715
+ })
716
+ });
717
+
718
+ // Send confirmation email
719
+ await fetch('/api/emails/confirmation', {
720
+ method: 'POST',
721
+ headers: { 'Content-Type': 'application/json' },
722
+ body: JSON.stringify({ bookingId: bookingData.id })
723
+ });
724
+
725
+ // Redirect to success page
726
+ window.location.href = `/booking/success?id=${bookingData.id}`;
727
+
728
+ } catch (error) {
729
+ toast.error('Failed to confirm booking');
730
+ console.error(error);
731
+ }
732
+ };
733
+
734
+ const handlePaymentError = (error: Error) => {
735
+ toast.error('Payment failed. Please try again.');
736
+ console.error(error);
737
+ };
738
+
739
+ return (
740
+ <div className="container mx-auto px-4 py-8">
741
+ <BookingFlow
742
+ config={config}
743
+ colors={colors}
744
+ services={services}
745
+ onComplete={handleBookingComplete}
746
+ />
747
+
748
+ {bookingData && (
749
+ <SquarePayment
750
+ amount={bookingData.depositAmount}
751
+ onPaymentSuccess={handlePaymentSuccess}
752
+ onPaymentError={handlePaymentError}
753
+ />
754
+ )}
755
+ </div>
756
+ );
757
+ }
758
+ ```
759
+
760
+ ---
761
+
762
+ ## See Also
763
+
764
+ - [CALLBACKS.md](./CALLBACKS.md) - Event handling patterns
765
+ - [DATA_SHAPES.md](./DATA_SHAPES.md) - Data structures
766
+ - [API.md](./API.md) - Component reference