hey-pharmacist-ecommerce 1.1.29 → 1.1.30

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 (48) hide show
  1. package/dist/index.d.mts +10734 -1256
  2. package/dist/index.d.ts +10734 -1256
  3. package/dist/index.js +2741 -295
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2686 -296
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +1 -1
  8. package/src/hooks/useStoreCapabilities.ts +87 -0
  9. package/src/index.ts +4 -0
  10. package/src/lib/Apis/apis/auth-api.ts +19 -7
  11. package/src/lib/Apis/apis/categories-api.ts +97 -0
  12. package/src/lib/Apis/apis/products-api.ts +97 -0
  13. package/src/lib/Apis/apis/shipping-api.ts +105 -0
  14. package/src/lib/Apis/apis/stores-api.ts +356 -0
  15. package/src/lib/Apis/apis/sub-categories-api.ts +97 -0
  16. package/src/lib/Apis/apis/users-api.ts +8 -8
  17. package/src/lib/Apis/models/address-created-request.ts +0 -12
  18. package/src/lib/Apis/models/address.ts +0 -12
  19. package/src/lib/Apis/models/api-key-info-dto.ts +49 -0
  20. package/src/lib/Apis/models/create-address-dto.ts +0 -12
  21. package/src/lib/Apis/models/create-discount-dto.ts +0 -8
  22. package/src/lib/Apis/models/create-store-address-dto.ts +0 -12
  23. package/src/lib/Apis/models/create-store-dto-settings.ts +51 -0
  24. package/src/lib/Apis/models/create-store-dto.ts +7 -0
  25. package/src/lib/Apis/models/create-variant-dto.ts +0 -6
  26. package/src/lib/Apis/models/discount.ts +0 -8
  27. package/src/lib/Apis/models/index.ts +11 -0
  28. package/src/lib/Apis/models/populated-discount.ts +0 -8
  29. package/src/lib/Apis/models/product-variant.ts +0 -6
  30. package/src/lib/Apis/models/reorder-categories-dto.ts +27 -0
  31. package/src/lib/Apis/models/reorder-products-dto.ts +49 -0
  32. package/src/lib/Apis/models/reorder-products-success-response-dto.ts +33 -0
  33. package/src/lib/Apis/models/reorder-subcategories-dto.ts +33 -0
  34. package/src/lib/Apis/models/reorder-success-response-dto.ts +33 -0
  35. package/src/lib/Apis/models/shipment-with-order.ts +18 -0
  36. package/src/lib/Apis/models/shipment.ts +18 -0
  37. package/src/lib/Apis/models/store-api-keys-response-dto.ts +34 -0
  38. package/src/lib/Apis/models/store-capabilities-dto.ts +63 -0
  39. package/src/lib/Apis/models/store-entity.ts +7 -0
  40. package/src/lib/Apis/models/store.ts +7 -0
  41. package/src/lib/Apis/models/update-address-dto.ts +0 -12
  42. package/src/lib/Apis/models/update-api-keys-dto.ts +39 -0
  43. package/src/lib/Apis/models/update-discount-dto.ts +0 -8
  44. package/src/lib/Apis/models/update-manual-shipment-status-dto.ts +47 -0
  45. package/src/lib/Apis/models/update-store-dto.ts +7 -0
  46. package/src/lib/Apis/models/update-variant-dto.ts +0 -6
  47. package/src/screens/CheckoutScreen.tsx +363 -278
  48. package/src/screens/ResetPasswordScreen.tsx +10 -4
@@ -24,6 +24,7 @@ import { useAuth } from '@/providers/AuthProvider';
24
24
  import { OrdersApi } from '@/lib/Apis/apis/orders-api';
25
25
  import { ShippingApi } from '@/lib/Apis/apis/shipping-api';
26
26
  import { useAddresses } from '@/hooks/useAddresses';
27
+ import { useStoreCapabilities } from '@/hooks/useStoreCapabilities';
27
28
  import { formatPrice } from '@/lib/utils/format';
28
29
  import { useRouter } from 'next/navigation';
29
30
  import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
@@ -48,32 +49,28 @@ type CheckoutFormData = z.infer<typeof checkoutSchema>;
48
49
  const SHIPPING_THRESHOLD = 100;
49
50
  const TAX_RATE = 0.08;
50
51
 
51
- // Add enums for payment methods
52
- const PAYMENT_METHODS = [
52
+ // Base payment methods config (filtered at runtime based on capabilities)
53
+ const PAYMENT_METHODS_CONFIG = [
53
54
  {
55
+ id: 'Card',
54
56
  label: 'Card',
55
57
  value: 'Card',
56
58
  icon: <CreditCard className="w-5 h-5" />,
57
59
  description: 'Pay securely with your credit or debit card',
58
60
  className: 'border-blue-500 hover:bg-blue-50',
59
61
  activeClass: 'bg-blue-50 border-blue-500 text-blue-700',
62
+ requiresStripe: true,
60
63
  },
61
64
  {
65
+ id: 'Cash',
62
66
  label: 'Cash',
63
67
  value: 'Cash',
64
68
  icon: <PackageCheck className="w-5 h-5" />,
65
69
  description: 'Pay with cash on delivery or at pickup',
66
70
  className: 'border-amber-500 hover:bg-amber-50',
67
71
  activeClass: 'bg-amber-50 border-amber-500 text-amber-700',
72
+ requiresStripe: false,
68
73
  },
69
- // {
70
- // label: 'Credit',
71
- // value: 'Credit',
72
- // icon: <ShieldCheck className="w-5 h-5" />,
73
- // description: 'Use your account credit',
74
- // className: 'border-emerald-500 hover:bg-emerald-50',
75
- // activeClass: 'bg-emerald-50 border-emerald-500 text-emerald-700',
76
- // },
77
74
  ];
78
75
 
79
76
  export function CheckoutScreen() {
@@ -82,8 +79,9 @@ export function CheckoutScreen() {
82
79
  const { isAuthenticated, user } = useAuth();
83
80
  const { buildPath } = useBasePath();
84
81
  const [isSubmitting, setIsSubmitting] = useState(false);
85
- const [isDelivery, setIsDelivery] = useState(true); // delivery or pickup
86
- const [paymentMethod, setPaymentMethod] = useState('Card');
82
+ const [isDelivery, setIsDelivery] = useState<boolean | 'custom' | null>(null); // Start with no selection
83
+ const [hasSetDefaultDelivery, setHasSetDefaultDelivery] = useState(false);
84
+ const [paymentMethod, setPaymentMethod] = useState<string | null>(null); // null until capabilities load
87
85
  const [error, setError] = useState<string | null>(null);
88
86
  const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);
89
87
  const [storeAddresses, setStoreAddresses] = useState<any[]>([]); // For pickup selection
@@ -98,6 +96,73 @@ export function CheckoutScreen() {
98
96
  addresses: userAddresses,
99
97
  defaultAddress
100
98
  } = useAddresses();
99
+
100
+ // Get store capabilities for payment/delivery options
101
+ const {
102
+ capabilities,
103
+ isLoading: capabilitiesLoading,
104
+ showCardPayment,
105
+ showShippoDelivery,
106
+ showCustomDelivery,
107
+ } = useStoreCapabilities();
108
+
109
+ // Filter payment methods based on capabilities
110
+ const availablePaymentMethods = PAYMENT_METHODS_CONFIG.filter(pm => {
111
+ if (pm.requiresStripe) {
112
+ return showCardPayment;
113
+ }
114
+ return true; // Cash is always available
115
+ });
116
+
117
+ // Set default payment method once capabilities are loaded
118
+ useEffect(() => {
119
+ if (!capabilitiesLoading && paymentMethod === null && availablePaymentMethods.length > 0) {
120
+ // Prefer Card if available, otherwise use first available
121
+ const preferCard = availablePaymentMethods.find(pm => pm.id === 'Card');
122
+ setPaymentMethod(preferCard ? 'Card' : availablePaymentMethods[0].value);
123
+ }
124
+ }, [capabilitiesLoading, paymentMethod, availablePaymentMethods]);
125
+
126
+ // Set default delivery method once capabilities are loaded
127
+ useEffect(() => {
128
+ if (!capabilitiesLoading && !hasSetDefaultDelivery && capabilities) {
129
+ // Only default to Pickup if NO delivery options are available
130
+ if (!showShippoDelivery && !showCustomDelivery) {
131
+ setIsDelivery(false);
132
+ }
133
+ // Otherwise leave as null (User must choose)
134
+ setHasSetDefaultDelivery(true);
135
+ }
136
+ }, [capabilitiesLoading, hasSetDefaultDelivery, capabilities, showShippoDelivery, showCustomDelivery]);
137
+
138
+ // Build delivery options based on capabilities
139
+ const deliveryOptions = [
140
+ // Shippo delivery (if enabled)
141
+ ...(showShippoDelivery ? [{
142
+ id: 'shippo',
143
+ label: 'Delivery',
144
+ icon: <Truck className="w-5 h-5" />,
145
+ value: true as const,
146
+ desc: 'Shipped to your address',
147
+ }] : []),
148
+ // Custom/Store delivery (if enabled)
149
+ ...(showCustomDelivery ? [{
150
+ id: 'custom',
151
+ label: capabilities?.customDeliveryTitle || 'Store Delivery',
152
+ icon: <Truck className="w-5 h-5" />,
153
+ value: 'custom' as const,
154
+ desc: capabilities?.customDeliveryFlatRate
155
+ ? `Flat rate: ${formatPrice(capabilities.customDeliveryFlatRate)}`
156
+ : 'Flat rate delivery',
157
+ }] : []),
158
+ {
159
+ id: 'pickup',
160
+ label: 'Pickup',
161
+ icon: <MapPin className="w-5 h-5" />,
162
+ value: false as const,
163
+ desc: 'Collect from pharmacy',
164
+ },
165
+ ];
101
166
  const [selectedShippingRateId, setSelectedShippingRateId] = useState<string | null>(null);
102
167
  const [shippingRates, setShippingRates] = useState<any[]>([]);
103
168
  const [shippingRatesLoading, setShippingRatesLoading] = useState(false);
@@ -231,7 +296,7 @@ export function CheckoutScreen() {
231
296
  }
232
297
 
233
298
  useEffect(() => {
234
- if (!isDelivery || !selectedAddressId || !cart || cart?.cartBody?.items?.length === 0 || !cart?.cartBody?.items) {
299
+ if (isDelivery !== true || !selectedAddressId || !cart || cart?.cartBody?.items?.length === 0 || !cart?.cartBody?.items) {
235
300
  setShippingRates([]);
236
301
  setSelectedShippingRateId(null);
237
302
  return;
@@ -261,17 +326,24 @@ export function CheckoutScreen() {
261
326
  }, [isDelivery, selectedAddressId, cart]);
262
327
 
263
328
  useEffect(() => {
264
- if (!isDelivery) {
329
+ let cancelled = false;
330
+
331
+ if (isDelivery === false) {
265
332
  (async () => {
266
333
  const api = new ShippingApi(AXIOS_CONFIG);
267
334
  const res = await api.getStoreAddress();
268
- if (res.data) {
335
+ // Only update state if this effect hasn't been cleaned up
336
+ if (!cancelled && res.data) {
269
337
  setStoreAddresses([res.data]);
270
338
  setShippingPrice(0);
271
339
  }
272
340
  })();
273
341
  }
274
- // eslint-disable-next-line react-hooks/exhaustive-deps
342
+
343
+ // Cleanup function to prevent stale updates when delivery method changes
344
+ return () => {
345
+ cancelled = true;
346
+ };
275
347
  }, [isDelivery]);
276
348
 
277
349
 
@@ -293,12 +365,22 @@ export function CheckoutScreen() {
293
365
  setError('Please select a payment method.');
294
366
  return;
295
367
  }
296
- if (isDelivery) {
368
+ if (isDelivery === null) {
369
+ setError('Please select a delivery method.');
370
+ return;
371
+ }
372
+ // Check for delivery-related requirements (both shippo and custom delivery need address)
373
+ const isShippoDelivery = isDelivery === true;
374
+ const isCustomDelivery = isDelivery === 'custom';
375
+ const needsDeliveryAddress = isShippoDelivery || isCustomDelivery;
376
+
377
+ if (needsDeliveryAddress) {
297
378
  if (!selectedAddressId) {
298
379
  setError('Please select a shipping address.');
299
380
  return;
300
381
  }
301
- if (!selectedShippingRateId) {
382
+ // Only require shipping rate selection for Shippo delivery
383
+ if (isShippoDelivery && !selectedShippingRateId) {
302
384
  setError('Please select a shipping method.');
303
385
  return;
304
386
  }
@@ -309,7 +391,8 @@ export function CheckoutScreen() {
309
391
  return;
310
392
  }
311
393
  }
312
- } else {
394
+ } else if (isDelivery === false) {
395
+ // Pickup case
313
396
  if (storeAddresses.length === 0) {
314
397
  setError('Store pickup location is not available.');
315
398
  return;
@@ -346,6 +429,13 @@ export function CheckoutScreen() {
346
429
  } else if (paymentMethod === 'Card') {
347
430
  billingAddressId = userAddresses.length > 0 ? userAddresses[0].id : undefined;
348
431
  }
432
+ // Build manualShipping object for custom delivery
433
+ const manualShipping = isDelivery === 'custom' && selectedAddressId ? {
434
+ amount: capabilities?.customDeliveryFlatRate || 0,
435
+ userAddressId: selectedAddressId,
436
+ shippingTitle: capabilities?.customDeliveryTitle || 'Store Delivery',
437
+ } : undefined;
438
+
349
439
  const orderDTO: any = {
350
440
  items,
351
441
  paymentMethod,
@@ -353,14 +443,17 @@ export function CheckoutScreen() {
353
443
  orderRemindingDates: [],
354
444
  shippingAddress: data.shipping,
355
445
  billingAddress: sameAsShipping ? data.shipping : data.billing,
446
+ ...(manualShipping && { manualShipping }),
356
447
  };
357
448
 
358
449
  const api = new OrdersApi(AXIOS_CONFIG);
450
+ // Convert isDelivery to boolean for API (custom delivery counts as delivery)
451
+ const isDeliveryBoolean = isDelivery !== false;
359
452
  const response = await api.createCheckout(
360
453
  orderDTO,
361
- isDelivery,
454
+ isDeliveryBoolean,
362
455
  undefined, // dont pass userId
363
- isDelivery ? (selectedShippingRateId || undefined) : undefined,
456
+ isDeliveryBoolean && isDelivery === true ? (selectedShippingRateId || undefined) : undefined,
364
457
  billingAddressId
365
458
  );
366
459
  if (response?.data?.payment && response.data.payment.hostedInvoiceUrl === 'INSUFFICIENT_CREDIT') {
@@ -456,143 +549,267 @@ export function CheckoutScreen() {
456
549
  Complete your order information below
457
550
  </p>
458
551
  </div>
459
- <section className="bg-white border-2 border-gray-100 rounded-[24px] p-8 text-[#2B4B7C]">
460
- <div className="flex flex-wrap items-center justify-between gap-4">
552
+ {/* Delivery and Payment Methods Section - Side by Side */}
553
+ <div className="bg-white border-2 border-gray-100 rounded-[24px] p-8">
554
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
555
+ {/* Delivery Method */}
556
+ <div>
557
+ <div className="flex items-center gap-3 mb-6">
558
+ <Truck className="w-6 h-6 text-[#2B4B7C]" />
559
+ <h2 className="text-xl font-semibold text-[#2B4B7C]">Delivery Method</h2>
560
+ </div>
561
+
562
+ <div className="space-y-3">
563
+ {deliveryOptions.map((option) => {
564
+ const active = isDelivery === option.value;
565
+ return (
566
+ <button
567
+ key={option.id}
568
+ type="button"
569
+ onClick={() => {
570
+ setIsDelivery(option.value);
571
+ // Reset shipping rate when changing delivery method
572
+ setSelectedShippingRateId(null);
573
+ // Set shipping price based on delivery type
574
+ if (option.value === 'custom' && capabilities?.customDeliveryFlatRate) {
575
+ setShippingPrice(capabilities.customDeliveryFlatRate);
576
+ } else if (option.value === false) {
577
+ setShippingPrice(0);
578
+ } else {
579
+ setShippingPrice(0); // Will be set when shipping rate is selected
580
+ }
581
+ }}
582
+ className={`relative flex w-full items-center justify-between rounded-xl border-2 p-3 transition-all duration-200 ${active
583
+ ? 'border-primary-500 bg-primary-50'
584
+ : 'border-gray-200 hover:border-primary-300'
585
+ }`}
586
+ >
587
+ <div className="flex items-center gap-3">
588
+ <div
589
+ className={`p-2 rounded-lg ${active ? 'bg-primary-100 text-primary-600' : 'bg-gray-100 text-gray-600'
590
+ }`}
591
+ >
592
+ {option.icon}
593
+ </div>
594
+ <div className="text-left">
595
+ <div className="font-medium text-gray-900">{option.label}</div>
596
+ <p className="text-xs text-gray-500">{option.desc}</p>
597
+ </div>
598
+ </div>
599
+ {active && (
600
+ <div className="w-5 h-5 rounded-full bg-primary-500 flex items-center justify-center shrink-0">
601
+ <Check className="w-3 h-3 text-white" />
602
+ </div>
603
+ )}
604
+ </button>
605
+ );
606
+ })}
607
+ </div>
608
+ </div>
609
+
610
+ {/* Payment Method */}
461
611
  <div>
462
- <h2 className="font-['Poppins',sans-serif] font-semibold text-[#2B4B7C] mb-2 text-2xl">
463
- Contact Information
464
- </h2>
465
- <p className="text-sm text-slate-500">
466
- We use temperature-aware packaging and real-time tracking on every shipment.
612
+ <div className="flex items-center gap-3 mb-6">
613
+ <CreditCard className="w-6 h-6 text-[#2B4B7C]" />
614
+ <h2 className="text-xl font-semibold text-[#2B4B7C]">Payment Method</h2>
615
+ </div>
616
+
617
+ <div className="space-y-3">
618
+ {availablePaymentMethods.map((pm) => {
619
+ const active = paymentMethod === pm.value;
620
+
621
+ return (
622
+ <button
623
+ key={pm.value}
624
+ type="button"
625
+ onClick={() => setPaymentMethod(pm.value)}
626
+ className={`
627
+ group relative flex w-full items-center justify-between rounded-xl border-2 p-3
628
+ transition-all duration-200 ease-out
629
+ focus:outline-hidden
630
+ ${active
631
+ ? `${pm.activeClass} shadow-md scale-[1.02]`
632
+ : `${pm.className} border-gray-200 bg-white hover:shadow-xs hover:-translate-y-0.5`
633
+ }
634
+ `}
635
+ >
636
+ <div className="flex items-center gap-3">
637
+ <div
638
+ className={`
639
+ flex items-center justify-center rounded-lg p-2
640
+ transition-colors duration-200
641
+ ${pm.value === 'Card'
642
+ ? ' text-blue-600'
643
+ : pm.value === 'Cash'
644
+ ? ' text-amber-600'
645
+ : ' text-emerald-600'
646
+ }
647
+ ${active ? 'opacity-80' : 'group-hover:opacity-90'}
648
+ `}
649
+ >
650
+ {pm.icon}
651
+ </div>
652
+
653
+ <span className="text-sm font-semibold text-gray-900">
654
+ {pm.label}
655
+ </span>
656
+ </div>
657
+
658
+ {active && (
659
+ <div className="flex h-5 w-5 items-center justify-center rounded-full bg-current shrink-0">
660
+ <Check className="h-3 w-3 text-white" />
661
+ </div>
662
+ )}
663
+ </button>
664
+ );
665
+ })}
666
+ </div>
667
+
668
+ <p className="text-xs text-slate-500 mt-4 px-1">
669
+ {paymentMethod === 'Card' &&
670
+ 'You will be redirected to a secure payment page.'}
671
+ {paymentMethod === 'Cash' &&
672
+ 'Pay with cash at the time of delivery or pickup.'}
467
673
  </p>
468
674
  </div>
469
- <span className="inline-flex items-center gap-2 rounded-full bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700">
470
- <Truck className="h-4 w-4" />
471
- Dispatch in 12h
472
- </span>
473
675
  </div>
676
+ </div>
474
677
 
475
- <div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
476
- {/* Address selection for delivery */}
477
- {isDelivery && (
478
- <div className="md:col-span-2 space-y-4">
479
- <div className="flex items-center justify-between">
480
- <label className="block font-semibold">Select Address</label>
481
- <Button
482
- type="button"
483
- variant="outline-solid"
484
- size="sm"
485
- onClick={() => { setEditingAddress(null); setIsAddressModalOpen(true); }}
486
- >
487
- <Plus className="h-4 w-4 mr-2" />
488
- Add New Address
489
- </Button>
490
- </div>
678
+ {isDelivery !== null && (
679
+ <section className="bg-white border-2 border-gray-100 rounded-[24px] p-8 text-[#2B4B7C]">
680
+ <div className="flex flex-wrap items-center justify-between gap-4">
681
+ <div>
682
+ <h2 className="font-['Poppins',sans-serif] font-semibold text-[#2B4B7C] mb-2 text-2xl">
683
+ {isDelivery ? 'Delivery Address' : 'Contact Information'}
684
+ </h2>
685
+ <p className="text-sm text-slate-500">
686
+ We use temperature-aware packaging and real-time tracking on every shipment.
687
+ </p>
688
+ </div>
689
+ </div>
491
690
 
492
- {userAddresses.length > 0 ? (
493
- <div className="grid gap-4">
494
- {userAddresses.map(addr => (
495
- <label
496
- key={addr.id}
497
- className={`group relative flex items-start gap-3 p-4 rounded-2xl border ${selectedAddressId === addr.id
498
- ? 'border-primary-500 bg-primary-50 shadow-xs'
499
- : 'border-slate-200 bg-white'
500
- } cursor-pointer hover:border-primary-300 transition-colors`}
501
- >
502
- <input
503
- type="radio"
504
- name="selectedAddress"
505
- value={addr.id}
506
- checked={selectedAddressId === addr.id}
507
- onChange={() => {
508
- setSelectedAddressId(addr.id);
509
- setValue('shipping.name', addr.name);
510
- setValue('shipping.phone', addr.phone || '');
511
- setValue('shipping.street1', addr.street1);
512
- setValue('shipping.street2', addr.street2 || '');
513
- setValue('shipping.city', addr.city);
514
- setValue('shipping.state', addr.state);
515
- setValue('shipping.zip', addr.zip);
516
- setValue('shipping.country', addr.country);
517
- }}
518
- className="mt-1"
519
- />
520
- <div className="flex-1">
521
- <p className="font-semibold text-slate-900">{addr.name}</p>
522
- <p className="text-sm text-slate-600">{addr.street1}</p>
523
- {addr.street2 && (
524
- <p className="text-sm text-slate-600">{addr.street2}</p>
525
- )}
526
- <p className="text-sm text-slate-600">
527
- {addr.city}, {addr.state} {addr.zip}
528
- </p>
529
- <p className="text-sm text-slate-600">{addr.country}</p>
530
- {addr.phone && (
531
- <p className="text-sm text-slate-600 mt-1">{addr.phone}</p>
532
- )}
533
- <div className="mt-3 flex items-center gap-2">
534
- {addr.isDefault && (
535
- <span className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-0.5 text-xs font-semibold text-primary-700">
536
- Default
537
- </span>
691
+ <div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
692
+ {/* Address selection for delivery */}
693
+ {isDelivery && (
694
+ <div className="md:col-span-2 space-y-4">
695
+ <div className="flex items-center justify-between">
696
+ <label className="block font-semibold">Select Address</label>
697
+ <Button
698
+ type="button"
699
+ variant="outline-solid"
700
+ size="sm"
701
+ onClick={() => { setEditingAddress(null); setIsAddressModalOpen(true); }}
702
+ >
703
+ <Plus className="h-4 w-4 mr-2" />
704
+ Add New Address
705
+ </Button>
706
+ </div>
707
+
708
+ {userAddresses.length > 0 ? (
709
+ <div className="grid gap-4">
710
+ {userAddresses.map(addr => (
711
+ <label
712
+ key={addr.id}
713
+ className={`group relative flex items-start gap-3 p-4 rounded-2xl border ${selectedAddressId === addr.id
714
+ ? 'border-primary-500 bg-primary-50 shadow-xs'
715
+ : 'border-slate-200 bg-white'
716
+ } cursor-pointer hover:border-primary-300 transition-colors`}
717
+ >
718
+ <input
719
+ type="radio"
720
+ name="selectedAddress"
721
+ value={addr.id}
722
+ checked={selectedAddressId === addr.id}
723
+ onChange={() => {
724
+ setSelectedAddressId(addr.id);
725
+ setValue('shipping.name', addr.name);
726
+ setValue('shipping.phone', addr.phone || '');
727
+ setValue('shipping.street1', addr.street1);
728
+ setValue('shipping.street2', addr.street2 || '');
729
+ setValue('shipping.city', addr.city);
730
+ setValue('shipping.state', addr.state);
731
+ setValue('shipping.zip', addr.zip);
732
+ setValue('shipping.country', addr.country);
733
+ }}
734
+ className="mt-1"
735
+ />
736
+ <div className="flex-1">
737
+ <p className="font-semibold text-slate-900">{addr.name}</p>
738
+ <p className="text-sm text-slate-600">{addr.street1}</p>
739
+ {addr.street2 && (
740
+ <p className="text-sm text-slate-600">{addr.street2}</p>
538
741
  )}
539
- <button
540
- type="button"
541
- onClick={(e) => { e.preventDefault(); setEditingAddress(addr); setIsAddressModalOpen(true); }}
542
- className="inline-flex items-center gap-1 rounded-full border border-slate-200 px-2.5 py-0.5 text-xs font-medium text-slate-600 hover:border-primary-300 hover:text-primary-600"
543
- >
544
- <Edit3 className="h-3.5 w-3.5" /> Edit
545
- </button>
546
- <button
547
- type="button"
548
- onClick={async (e) => {
549
- e.preventDefault();
550
- const yes = window.confirm('Delete this address?');
551
- if (!yes) return;
552
- try {
553
- await removeAddress(addr.id);
554
- if (selectedAddressId === addr.id) setSelectedAddressId(null);
555
- notification.success('Address deleted');
556
- } catch (e) {
557
- notification.error('Failed to delete address');
558
- }
559
- }}
560
- className="inline-flex items-center gap-1 rounded-full border border-red-200 px-2.5 py-0.5 text-xs font-semibold text-red-600 hover:border-red-300 hover:text-red-700"
561
- >
562
- <Trash2 className="h-3.5 w-3.5" /> Delete
563
- </button>
742
+ <p className="text-sm text-slate-600">
743
+ {addr.city}, {addr.state} {addr.zip}
744
+ </p>
745
+ <p className="text-sm text-slate-600">{addr.country}</p>
746
+ {addr.phone && (
747
+ <p className="text-sm text-slate-600 mt-1">{addr.phone}</p>
748
+ )}
749
+ <div className="mt-3 flex items-center gap-2">
750
+ {addr.isDefault && (
751
+ <span className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-0.5 text-xs font-semibold text-primary-700">
752
+ Default
753
+ </span>
754
+ )}
755
+ <button
756
+ type="button"
757
+ onClick={(e) => { e.preventDefault(); setEditingAddress(addr); setIsAddressModalOpen(true); }}
758
+ className="inline-flex items-center gap-1 rounded-full border border-slate-200 px-2.5 py-0.5 text-xs font-medium text-slate-600 hover:border-primary-300 hover:text-primary-600"
759
+ >
760
+ <Edit3 className="h-3.5 w-3.5" /> Edit
761
+ </button>
762
+ <button
763
+ type="button"
764
+ onClick={async (e) => {
765
+ e.preventDefault();
766
+ const yes = window.confirm('Delete this address?');
767
+ if (!yes) return;
768
+ try {
769
+ await removeAddress(addr.id);
770
+ if (selectedAddressId === addr.id) setSelectedAddressId(null);
771
+ notification.success('Address deleted');
772
+ } catch (e) {
773
+ notification.error('Failed to delete address');
774
+ }
775
+ }}
776
+ className="inline-flex items-center gap-1 rounded-full border border-red-200 px-2.5 py-0.5 text-xs font-semibold text-red-600 hover:border-red-300 hover:text-red-700"
777
+ >
778
+ <Trash2 className="h-3.5 w-3.5" /> Delete
779
+ </button>
780
+ </div>
564
781
  </div>
565
- </div>
566
- </label>
782
+ </label>
783
+ ))}
784
+ </div>
785
+ ) : (
786
+ <div className="text-center py-8 bg-slate-50 rounded-lg">
787
+ <MapPin className="h-12 w-12 mx-auto text-slate-400" />
788
+ <p className="mt-2 text-slate-600">No addresses found</p>
789
+ <p className="text-sm text-slate-500">Add a new address to continue</p>
790
+ </div>
791
+ )}
792
+ </div>
793
+ )}
794
+ {/* Store address selection for pickup */}
795
+ {isDelivery === false && storeAddresses.length > 0 && (
796
+ <div className="md:col-span-2">
797
+ <label className="block mb-2 font-semibold">Select Pickup Location</label>
798
+ <div className="flex items-center gap-2">
799
+ {storeAddresses.map(addr => (
800
+ <div key={addr.id} className="w-full border rounded-sm p-2">
801
+ <p>{addr.name} - {addr.street1}, {addr.city}, {addr.state} {addr.zip}</p>
802
+ </div>
567
803
  ))}
568
804
  </div>
569
- ) : (
570
- <div className="text-center py-8 bg-slate-50 rounded-lg">
571
- <MapPin className="h-12 w-12 mx-auto text-slate-400" />
572
- <p className="mt-2 text-slate-600">No addresses found</p>
573
- <p className="text-sm text-slate-500">Add a new address to continue</p>
574
- </div>
575
- )}
576
- </div>
577
- )}
578
- {/* Store address selection for pickup */}
579
- {!isDelivery && storeAddresses.length > 0 && (
580
- <div className="md:col-span-2">
581
- <label className="block mb-2 font-semibold">Select Pickup Location</label>
582
- <div className="flex items-center gap-2">
583
- {storeAddresses.map(addr => (
584
- <div key={addr.id} className="w-full border rounded-sm p-2">
585
- <p>{addr.name} - {addr.street1}, {addr.city}, {addr.state} {addr.zip}</p>
586
- </div>
587
- ))}
588
805
  </div>
589
- </div>
590
- )}
591
- </div>
592
- </section>
806
+ )}
807
+ </div>
808
+ </section>
809
+ )}
593
810
 
594
- {/* Shipping Options - Only show if there's a shipping cost */}
595
- {isDelivery && selectedAddressId && (
811
+ {/* Shipping Options - Only show if there's a shipping cost and it's Shippo delivery */}
812
+ {isDelivery === true && selectedAddressId && (
596
813
  <div className="bg-white border-2 border-gray-100 rounded-[24px] p-8 text-[#2B4B7C]">
597
814
  <div className="flex items-center gap-3 text-xl font-semibold text-gray-900 pb-4 mb-8 border-b">
598
815
  <Truck className="w-8 h-8 flex items-center justify-center text-[#2B4B7C]" />
@@ -737,139 +954,7 @@ export function CheckoutScreen() {
737
954
  </div>
738
955
  )}
739
956
 
740
- {/* Delivery and Payment Methods Section */}
741
- <div className="space-y-6">
742
- {/* Delivery Method */}
743
- <div className="bg-white border-2 border-gray-100 rounded-[24px] p-8">
744
- <div className="flex items-center gap-3 mb-6">
745
- <Truck className="w-8 h-8 text-[#2B4B7C]" />
746
- <h2 className="text-2xl font-semibold text-[#2B4B7C]">Delivery Method</h2>
747
- </div>
748
-
749
- <div className="grid grid-cols-1 gap-3">
750
- {[
751
- {
752
- label: 'Delivery',
753
- icon: <Truck className="w-5 h-5" />,
754
- value: true,
755
- desc: 'Shipped to your address',
756
- },
757
- {
758
- label: 'Pickup',
759
- icon: <MapPin className="w-5 h-5" />,
760
- value: false,
761
- desc: 'Collect from pharmacy',
762
- },
763
- ].map((option) => {
764
- const active = isDelivery === option.value;
765
- return (
766
- <button
767
- key={option.label}
768
- type="button"
769
- onClick={() => {
770
- setIsDelivery(option.value);
771
- // Reset selected shipping rate when changing delivery method
772
- if (option.value) {
773
- setSelectedShippingRateId(null);
774
- setShippingPrice(0);
775
- }
776
- }}
777
- className={`relative flex w-full items-center justify-between rounded-xl border-2 p-4 transition-all duration-200 ${active
778
- ? 'border-primary-500 bg-primary-50'
779
- : 'border-gray-200 hover:border-primary-300'
780
- }`}
781
- >
782
- <div className="flex items-center gap-3">
783
- <div
784
- className={`p-2 rounded-lg ${active ? 'bg-primary-100 text-primary-600' : 'bg-gray-100 text-gray-600'
785
- }`}
786
- >
787
- {option.icon}
788
- </div>
789
- <div className="text-left">
790
- <div className="font-medium text-gray-900">{option.label}</div>
791
- <p className="text-sm text-gray-500">{option.desc}</p>
792
- </div>
793
- </div>
794
- {active && (
795
- <div className="w-5 h-5 rounded-full bg-primary-500 flex items-center justify-center">
796
- <Check className="w-3 h-3 text-white" />
797
- </div>
798
- )}
799
- </button>
800
- );
801
- })}
802
- </div>
803
- </div>
804
-
805
- {/* Payment Method */}
806
- <div className="bg-white border-2 border-gray-100 rounded-[24px] p-8">
807
- <div className="flex items-center gap-3 mb-6">
808
- <CreditCard className="w-8 h-8 text-[#2B4B7C]" />
809
- <h2 className="text-2xl font-semibold text-[#2B4B7C]">Payment Method</h2>
810
- </div>
811
-
812
- <div className="grid gap-4 md:grid-cols-3">
813
- {PAYMENT_METHODS.map((pm) => {
814
- const active = paymentMethod === pm.value;
815
957
 
816
- return (
817
- <button
818
- key={pm.value}
819
- type="button"
820
- onClick={() => setPaymentMethod(pm.value)}
821
- className={`
822
- group relative flex w-full items-center justify-between rounded-xl border-2 p-3
823
- transition-all duration-200 ease-out
824
- focus:outline-hidden f
825
- ${active
826
- ? `${pm.activeClass} shadow-md scale-[1.02]`
827
- : `${pm.className} border-gray-200 bg-white hover:shadow-xs hover:-translate-y-0.5`
828
- }
829
- `}
830
- >
831
- <div className="flex items-center gap-3">
832
- <div
833
- className={`
834
- flex items-center justify-center rounded-lg p-2
835
- transition-colors duration-200
836
- ${pm.value === 'Card'
837
- ? ' text-blue-600'
838
- : pm.value === 'Cash'
839
- ? ' text-amber-600'
840
- : ' text-emerald-600'
841
- }
842
- ${active ? 'opacity-80' : 'group-hover:opacity-90'}
843
- `}
844
- >
845
- {pm.icon}
846
- </div>
847
-
848
- <span className="text-sm font-semibold text-gray-900">
849
- {pm.label}
850
- </span>
851
- </div>
852
-
853
- {active && (
854
- <div className="flex h-6 w-6 items-center justify-center rounded-full shadow-xs">
855
- <Check className="h-3.5 w-3.5 text-white" />
856
- </div>
857
- )}
858
- </button>
859
- );
860
- })}
861
- </div>
862
-
863
- <p className="text-sm text-gray-500 mt-4">
864
- {paymentMethod === 'Card' &&
865
- 'You will be redirected to a secure payment page.'}
866
- {paymentMethod === 'Cash' &&
867
- 'Pay with cash at the time of delivery or pickup.'}
868
- {/* {paymentMethod === 'Credit' &&
869
- 'Use your available account credit for this order.'} */}
870
- </p>
871
- </div>
872
- </div>
873
958
  </motion.div>
874
959
 
875
960
  <motion.aside