@instockng/storefront-ui 1.0.9 → 1.0.11

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 (104) hide show
  1. package/README.md +26 -0
  2. package/dist/components/Checkout.d.ts.map +1 -1
  3. package/dist/contexts/CartContext.d.ts.map +1 -1
  4. package/dist/index10.mjs +144 -141
  5. package/dist/index101.mjs +1 -1
  6. package/dist/index102.mjs +3 -3
  7. package/dist/index103.mjs +3 -3
  8. package/dist/index105.mjs +1 -1
  9. package/dist/index111.mjs +1 -1
  10. package/dist/index20.mjs +2 -2
  11. package/dist/index21.mjs +1 -1
  12. package/dist/index28.mjs +11 -11
  13. package/dist/index29.mjs +1 -1
  14. package/dist/index3.mjs +88 -74
  15. package/dist/index30.mjs +1 -1
  16. package/dist/index31.mjs +1 -1
  17. package/dist/index32.mjs +1 -1
  18. package/dist/index37.mjs +1 -1
  19. package/dist/index41.mjs +36 -23
  20. package/dist/index42.mjs +44 -36
  21. package/dist/index43.mjs +99 -44
  22. package/dist/index44.mjs +112 -99
  23. package/dist/index45.mjs +44 -80
  24. package/dist/index46.mjs +64 -53
  25. package/dist/index47.mjs +66 -49
  26. package/dist/index48.mjs +54 -73
  27. package/dist/index49.mjs +52 -63
  28. package/dist/index50.mjs +14 -70
  29. package/dist/index51.mjs +13 -14
  30. package/dist/index52.mjs +58 -13
  31. package/dist/index53.mjs +101 -34
  32. package/dist/index54.mjs +99 -95
  33. package/dist/index55.mjs +22 -132
  34. package/dist/index58.mjs +2 -2
  35. package/dist/index59.mjs +4 -3
  36. package/dist/index60.mjs +4 -2
  37. package/dist/index61.mjs +2 -5
  38. package/dist/index62.mjs +30 -231
  39. package/dist/index63.mjs +42 -5
  40. package/dist/index64.mjs +228 -127
  41. package/dist/index65.mjs +4 -66
  42. package/dist/index66.mjs +124 -77
  43. package/dist/index67.mjs +65 -26
  44. package/dist/index68.mjs +84 -6
  45. package/dist/index69.mjs +26 -72
  46. package/dist/index70.mjs +8 -3
  47. package/dist/index71.mjs +75 -2
  48. package/dist/index72.mjs +3 -82
  49. package/dist/index73.mjs +2 -54
  50. package/dist/index74.mjs +82 -5
  51. package/dist/index75.mjs +53 -4
  52. package/dist/index76.mjs +5 -178
  53. package/dist/index77.mjs +5 -53
  54. package/dist/index78.mjs +178 -68
  55. package/dist/index79.mjs +50 -31
  56. package/dist/index80.mjs +69 -43
  57. package/dist/index81.mjs +2 -2
  58. package/dist/index82.mjs +1 -1
  59. package/dist/index83.mjs +6 -6
  60. package/dist/index84.mjs +2 -2
  61. package/dist/index85.mjs +2 -2
  62. package/dist/index87.mjs +2 -2
  63. package/dist/index88.mjs +2 -2
  64. package/dist/index89.mjs +1 -1
  65. package/dist/index91.mjs +4 -4
  66. package/dist/index92.mjs +3 -3
  67. package/dist/index93.mjs +30 -12
  68. package/dist/index94.mjs +11 -7
  69. package/dist/index95.mjs +3 -30
  70. package/dist/index96.mjs +3 -10
  71. package/dist/index97.mjs +13 -4
  72. package/dist/index98.mjs +7 -4
  73. package/dist/index99.mjs +1 -1
  74. package/dist/styles.css +1 -0
  75. package/package.json +14 -13
  76. package/src/components/CartItem.stories.tsx +94 -0
  77. package/src/components/CartItem.tsx +141 -0
  78. package/src/components/Checkout.stories.tsx +380 -0
  79. package/src/components/Checkout.tsx +954 -0
  80. package/src/components/DiscountCodeInput.stories.tsx +76 -0
  81. package/src/components/DiscountCodeInput.tsx +162 -0
  82. package/src/components/OrderConfirmation.stories.tsx +142 -0
  83. package/src/components/OrderConfirmation.tsx +301 -0
  84. package/src/components/ProductCard.stories.tsx +112 -0
  85. package/src/components/ProductCard.tsx +195 -0
  86. package/src/components/ProductGrid.stories.tsx +137 -0
  87. package/src/components/ProductGrid.tsx +141 -0
  88. package/src/components/ShoppingCart.stories.tsx +459 -0
  89. package/src/components/ShoppingCart.tsx +262 -0
  90. package/src/components/ui/badge.tsx +37 -0
  91. package/src/components/ui/button.tsx +71 -0
  92. package/src/components/ui/card.tsx +79 -0
  93. package/src/components/ui/form-input.tsx +78 -0
  94. package/src/components/ui/form-select.tsx +73 -0
  95. package/src/components/ui/modal.tsx +181 -0
  96. package/src/contexts/CartContext.tsx +305 -0
  97. package/src/hooks/usePaystackPayment.ts +137 -0
  98. package/src/index.ts +51 -0
  99. package/src/lib/utils.ts +45 -0
  100. package/src/paystack.svg +67 -0
  101. package/src/providers/StorefrontProvider.tsx +70 -0
  102. package/src/styles.css +1 -0
  103. package/src/test-utils/MockCartProvider.tsx +424 -0
  104. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,94 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { CartItem } from './CartItem';
3
+ import type {CartItem as CartItemType} from '@oms/api-client';
4
+
5
+ const meta = {
6
+ title: 'Components/CartItem',
7
+ component: CartItem,
8
+ parameters: {
9
+ layout: 'padded',
10
+ },
11
+ tags: ['autodocs'],
12
+ } satisfies Meta<typeof CartItem>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ const mockCartItem: CartItemType = {
18
+ id: 'item-1',
19
+ quantity: 2,
20
+ variant: {
21
+ createdAt: new Date().toISOString(),
22
+ updatedAt: new Date().toISOString(),
23
+ deletedAt: null,
24
+ isActive: true,
25
+ id: 'variant-1',
26
+ name: 'Medium - Blue',
27
+ price: 29.99,
28
+ sku: 'TSHIRT-MD-BLUE',
29
+ thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
30
+ trackInventory: true,
31
+ lowStockThreshold: 10,
32
+ productId: 'product-1',
33
+ product: {
34
+ name: 'Premium Cotton T-Shirt',
35
+ slug: 'cotton-shirt',
36
+ thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
37
+ id: 'product-1',
38
+ brandId: 'brand-1',
39
+ createdAt: new Date().toISOString(),
40
+ updatedAt: new Date().toISOString(),
41
+ deletedAt: null,
42
+ isActive: true,
43
+ description: 'Premium cotton t-shirt',
44
+ quantityDiscounts: null,
45
+ },
46
+ },
47
+ basePrice: 29.99,
48
+ discountPercent: 0,
49
+ finalPrice: 29.99,
50
+ subtotal: 59.98,
51
+ };
52
+
53
+ export const Default: Story = {
54
+ args: {
55
+ item: mockCartItem,
56
+ },
57
+ };
58
+
59
+ export const NoImage: Story = {
60
+ args: {
61
+ item: {
62
+ ...mockCartItem,
63
+ variant: {
64
+ ...mockCartItem.variant,
65
+ product: {
66
+ name: 'Premium Cotton T-Shirt',
67
+ id: 'product-1',
68
+ brandId: 'brand-1',
69
+ slug: 'cotton-shirt',
70
+ createdAt: new Date().toISOString(),
71
+ updatedAt: new Date().toISOString(),
72
+ deletedAt: null,
73
+ isActive: true,
74
+ description: 'Premium cotton t-shirt',
75
+ quantityDiscounts: null,
76
+ thumbnailUrl: null,
77
+ },
78
+ },
79
+ },
80
+ },
81
+ };
82
+
83
+ export const Disabled: Story = {
84
+ args: {
85
+ item: mockCartItem,
86
+ disabled: true,
87
+ },
88
+ };
89
+
90
+ export const Removing: Story = {
91
+ args: {
92
+ item: mockCartItem,
93
+ },
94
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * CartItem Component
3
+ *
4
+ * Displays a single item in the shopping cart with quantity controls and remove button.
5
+ */
6
+
7
+ import { Trash2, Package } from 'lucide-react';
8
+ import { formatCurrency, cn } from '../lib/utils';
9
+ import type { CartItem as CartItemType } from '@oms/api-client';
10
+ import { useCart } from '@/contexts/CartContext';
11
+ import { useCallback, useState } from 'react';
12
+
13
+ export interface CartItemProps {
14
+ item: CartItemType;
15
+ /** Custom class name */
16
+ className?: string;
17
+ /** Disable controls */
18
+ disabled?: boolean;
19
+ }
20
+
21
+ export function CartItem({
22
+ item,
23
+ className,
24
+ disabled = false,
25
+ }: CartItemProps) {
26
+ const [isRemoving, setIsRemoving] = useState(false);
27
+ const [isUpdating, setIsUpdating] = useState(false);
28
+ const { removeItem, updateItem } = useCart();
29
+
30
+ const handleQuantityChange = useCallback(async (newQuantity: number) => {
31
+ if (newQuantity <= 0) return;
32
+ setIsUpdating(true);
33
+
34
+ try {
35
+ await updateItem(item.id, newQuantity);
36
+ } catch (error) {
37
+ console.error('Failed to update item quantity:', error);
38
+ }
39
+
40
+ setIsUpdating(false);
41
+ }, [item.id, updateItem]);
42
+
43
+ const handleRemove = useCallback(async () => {
44
+ setIsRemoving(true);
45
+ try {
46
+ await removeItem(item.id);
47
+ } catch (error) {
48
+ console.error('Failed to remove item:', error);
49
+ }
50
+ setIsRemoving(false);
51
+ }, [item.id, removeItem]);
52
+
53
+ return (
54
+ <div
55
+ className={cn(
56
+ 'flex gap-3 transition-opacity',
57
+ (disabled || isRemoving || isUpdating) && 'opacity-50',
58
+ className
59
+ )}
60
+ >
61
+ {/* Product Image */}
62
+ <div className="h-20 w-20 flex-shrink-0 overflow-hidden rounded-lg bg-gray-100">
63
+ {item.variant.product.thumbnailUrl ? (
64
+ <img
65
+ src={item.variant.product.thumbnailUrl}
66
+ alt={item.variant.product.name}
67
+ className="h-full w-full object-cover"
68
+ />
69
+ ) : (
70
+ <div className="flex h-full w-full items-center justify-center">
71
+ <Package className="h-6 w-6 text-gray-400" />
72
+ </div>
73
+ )}
74
+ </div>
75
+
76
+ {/* Item Details */}
77
+ <div className="flex flex-1 flex-col justify-between py-1">
78
+ <div>
79
+ <div className="flex items-start justify-between gap-2">
80
+ <div className="flex-1">
81
+ <h3 className="text-base font-semibold leading-tight">
82
+ {item.variant.product.name}
83
+ </h3>
84
+ {item.variant.name && <p className="mt-0.5 text-sm text-gray-600">
85
+ {item.variant.name}
86
+ </p>}
87
+ </div>
88
+
89
+ {/* Remove Button */}
90
+ <button
91
+ onClick={handleRemove}
92
+ disabled={disabled || isRemoving}
93
+ className="text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
94
+ aria-label="Remove item"
95
+ >
96
+ <Trash2 className="h-5 w-5" />
97
+ </button>
98
+ </div>
99
+ </div>
100
+
101
+ {/* Quantity Controls and Price */}
102
+ <div className="flex items-center justify-between mt-2">
103
+ {/* Quantity Controls */}
104
+ <div className="inline-flex items-center overflow-hidden rounded-full bg-gray-200 text-gray-700">
105
+ <button
106
+ aria-label="Decrease quantity"
107
+ onClick={() => handleQuantityChange(item.quantity - 1)}
108
+ disabled={disabled || item.quantity <= 1 || isUpdating}
109
+ className="flex h-8 w-8 items-center justify-center transition-colors hover:bg-gray-300 disabled:cursor-not-allowed disabled:opacity-50"
110
+ >
111
+
112
+ </button>
113
+ <span className="px-3 text-base font-medium select-none">
114
+ {item.quantity}
115
+ </span>
116
+ <button
117
+ aria-label="Increase quantity"
118
+ onClick={() => handleQuantityChange(item.quantity + 1)}
119
+ disabled={disabled || isUpdating}
120
+ className="flex h-8 w-8 items-center justify-center transition-colors hover:bg-gray-300 disabled:cursor-not-allowed disabled:opacity-50"
121
+ >
122
+ +
123
+ </button>
124
+ </div>
125
+
126
+ {/* Price */}
127
+ <div className="text-right">
128
+ <p className="text-base font-medium">
129
+ {formatCurrency(item.subtotal)}
130
+ </p>
131
+ {item.basePrice !== item.finalPrice && (
132
+ <p className="text-xs text-[#DC143C] line-through">
133
+ {formatCurrency(item.basePrice * item.quantity)}
134
+ </p>
135
+ )}
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ );
141
+ }
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Checkout Storybook Stories
3
+ *
4
+ * Demonstrates the checkout flow with mocked cart and delivery zone data.
5
+ */
6
+
7
+ import type { Meta, StoryObj } from '@storybook/react';
8
+ import { useState } from 'react';
9
+ import { fn } from '@storybook/test';
10
+ import { Checkout, type CheckoutProps } from './Checkout';
11
+ import { Button } from './ui/button';
12
+ import { MockCartProvider } from '../test-utils/MockCartProvider';
13
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
14
+ import type { Cart } from '@oms/api-client';
15
+
16
+ // Mock delivery zones data
17
+ const mockDeliveryZones = [
18
+ {
19
+ id: 'state-1',
20
+ name: 'Lagos',
21
+ zones: [
22
+ {
23
+ id: 'zone-1',
24
+ name: 'Ikeja',
25
+ deliveryCost: 1500,
26
+ freeShippingThreshold: 10000,
27
+ allowCOD: true,
28
+ allowOnline: true,
29
+ waybillOnly: false,
30
+ estimatedDays: 2,
31
+ },
32
+ {
33
+ id: 'zone-2',
34
+ name: 'Victoria Island',
35
+ deliveryCost: 2000,
36
+ freeShippingThreshold: 15000,
37
+ allowCOD: true,
38
+ allowOnline: true,
39
+ waybillOnly: false,
40
+ estimatedDays: 1,
41
+ },
42
+ {
43
+ id: 'zone-3',
44
+ name: 'Lekki',
45
+ deliveryCost: 2500,
46
+ freeShippingThreshold: 15000,
47
+ allowCOD: true,
48
+ allowOnline: true,
49
+ waybillOnly: false,
50
+ estimatedDays: 2,
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ id: 'state-2',
56
+ name: 'Abuja',
57
+ zones: [
58
+ {
59
+ id: 'zone-4',
60
+ name: 'Wuse',
61
+ deliveryCost: 3000,
62
+ freeShippingThreshold: 20000,
63
+ allowCOD: true,
64
+ allowOnline: true,
65
+ waybillOnly: false,
66
+ estimatedDays: 3,
67
+ },
68
+ {
69
+ id: 'zone-5',
70
+ name: 'Garki',
71
+ deliveryCost: 3000,
72
+ freeShippingThreshold: 20000,
73
+ allowCOD: true,
74
+ allowOnline: true,
75
+ waybillOnly: false,
76
+ estimatedDays: 3,
77
+ },
78
+ ],
79
+ },
80
+ ];
81
+
82
+ // Create a query client with mocked delivery zones data
83
+ const createMockQueryClient = () => {
84
+ const queryClient = new QueryClient({
85
+ defaultOptions: {
86
+ queries: {
87
+ retry: false,
88
+ },
89
+ },
90
+ });
91
+
92
+ // Pre-populate the cache with mock delivery zones
93
+ queryClient.setQueryData(['deliveryZones', 'mock-brand-456'], mockDeliveryZones);
94
+
95
+ return queryClient;
96
+ };
97
+
98
+ // Mock cart data
99
+ const mockCart: Cart = {
100
+ id: 'mock-cart-123',
101
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
102
+ brand: {
103
+ id: 'mock-brand-456',
104
+ name: 'Demo Store',
105
+ slug: 'demo-store',
106
+ logoUrl: 'https://images.unsplash.com/photo-1599305445671-ac291c95aaa9?w=200&h=80&fit=crop',
107
+ siteUrl: 'https://demo-store.com',
108
+ domain: 'demo-store.com',
109
+ createdAt: new Date().toISOString(),
110
+ updatedAt: new Date().toISOString(),
111
+ deletedAt: null,
112
+ },
113
+ customerPhone: null,
114
+ customerEmail: null,
115
+ customerFirstName: null,
116
+ customerLastName: null,
117
+ availablePaymentMethods: ['cod'],
118
+ deliveryZone: null,
119
+ recoveryAttempts: 0,
120
+ lastRecoveryAttemptAt: null,
121
+ recoveryDiscountCode: null,
122
+ items: [
123
+ {
124
+ id: 'item-1',
125
+ variant: {
126
+ id: 'variant-1',
127
+ productId: 'product-1',
128
+ name: 'Medium - Blue',
129
+ price: 2999,
130
+ sku: 'TSHIRT-MD-BLUE',
131
+ thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
132
+ trackInventory: true,
133
+ lowStockThreshold: 10,
134
+ isActive: true,
135
+ createdAt: new Date().toISOString(),
136
+ updatedAt: new Date().toISOString(),
137
+ deletedAt: null,
138
+ product: {
139
+ id: 'product-1',
140
+ name: 'Premium Cotton T-Shirt',
141
+ slug: 'cotton-shirt',
142
+ brandId: 'mock-brand-456',
143
+ isActive: true,
144
+ description: 'Comfortable premium cotton t-shirt',
145
+ thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
146
+ createdAt: new Date().toISOString(),
147
+ updatedAt: new Date().toISOString(),
148
+ deletedAt: null,
149
+ quantityDiscounts: null,
150
+ },
151
+ },
152
+ quantity: 2,
153
+ basePrice: 2999,
154
+ discountPercent: 0,
155
+ finalPrice: 2599,
156
+ subtotal: 5198,
157
+ },
158
+ {
159
+ id: 'item-2',
160
+ variant: {
161
+ id: 'variant-2',
162
+ productId: 'product-2',
163
+ name: 'Large - Black',
164
+ price: 3499,
165
+ sku: 'JEANS-LG-BLACK',
166
+ thumbnailUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
167
+ trackInventory: true,
168
+ lowStockThreshold: 10,
169
+ isActive: true,
170
+ createdAt: new Date().toISOString(),
171
+ updatedAt: new Date().toISOString(),
172
+ deletedAt: null,
173
+ product: {
174
+ id: 'product-2',
175
+ name: 'Slim Fit Jeans',
176
+ brandId: 'mock-brand-456',
177
+ slug: 'cotton-shirt',
178
+ isActive: true,
179
+ description: 'Modern slim fit jeans',
180
+ thumbnailUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
181
+ createdAt: new Date().toISOString(),
182
+ updatedAt: new Date().toISOString(),
183
+ deletedAt: null,
184
+ quantityDiscounts: null,
185
+ },
186
+ },
187
+ quantity: 1,
188
+ basePrice: 3499,
189
+ discountPercent: 0,
190
+ finalPrice: 3499,
191
+ subtotal: 3499,
192
+ },
193
+ ],
194
+ pricing: {
195
+ subtotal: 8697,
196
+ deliveryCharge: 0,
197
+ discount: null,
198
+ total: 8697,
199
+ },
200
+ createdAt: new Date().toISOString(),
201
+ updatedAt: new Date().toISOString(),
202
+ convertedToOrderId: null,
203
+ wasRecovered: false,
204
+ recoveryUrl: 'https://demo-store.com?cartId=mock-cart-123',
205
+ };
206
+
207
+ const meta = {
208
+ title: 'Components/Checkout',
209
+ component: Checkout,
210
+ parameters: {
211
+ layout: 'centered',
212
+ },
213
+ tags: ['autodocs'],
214
+ } satisfies Meta<typeof Checkout>;
215
+
216
+ export default meta;
217
+
218
+ // Wrapper component to handle modal state and provide mock data
219
+ const CheckoutWrapper = (args: any) => {
220
+ const [isOpen, setIsOpen] = useState(false);
221
+ const queryClient = createMockQueryClient();
222
+
223
+ return (
224
+ <QueryClientProvider client={queryClient}>
225
+ <MockCartProvider cart={mockCart} renderCart={false}>
226
+ <div className="flex flex-col items-center gap-4 p-8">
227
+ <div className="text-center">
228
+ <h2 className="text-2xl font-bold mb-2">Checkout Flow Demo</h2>
229
+ <p className="text-gray-600 mb-4">
230
+ Click the button below to start the checkout process with mock data
231
+ </p>
232
+ </div>
233
+ <Button onClick={() => setIsOpen(true)} size="lg">
234
+ Start Checkout
235
+ </Button>
236
+ </div>
237
+ <Checkout
238
+ {...args}
239
+ isOpen={isOpen}
240
+ onClose={() => setIsOpen(false)}
241
+ onSuccess={(orderId) => {
242
+ args.onSuccess?.(orderId);
243
+ }}
244
+ onError={(error) => {
245
+ console.error('Checkout error:', error);
246
+ alert(`Checkout failed: ${error.message}`);
247
+ args.onError?.(error);
248
+ }}
249
+ />
250
+ </MockCartProvider>
251
+ </QueryClientProvider>
252
+ );
253
+ };
254
+
255
+ const mockRefundPolicy = {
256
+ title: '🤔 Curious about Refunds?',
257
+ policies: [
258
+ '2 day return window from delivery date',
259
+ 'Items must be unused and in original packaging',
260
+ 'Refunds processed as soon as item is received',
261
+ 'Refunds issued to original payment method',
262
+ 'Contact support@example.com to initiate refund',
263
+ ],
264
+ };
265
+
266
+ // Type for story args that excludes modal-controlled props
267
+ type CheckoutStoryArgs = Omit<CheckoutProps, 'isOpen' | 'onClose'>;
268
+
269
+ // Default checkout with mock data
270
+ export const Default: StoryObj<CheckoutStoryArgs> = {
271
+ render: (args) => <CheckoutWrapper {...args} />,
272
+ args: {
273
+ onSuccess: fn(),
274
+ onError: fn(),
275
+ refundPolicy: mockRefundPolicy,
276
+ },
277
+ };
278
+
279
+ // Checkout with pre-filled customer data
280
+ export const WithPrefilledData: StoryObj<CheckoutStoryArgs> = {
281
+ render: (args) => <CheckoutWrapper {...args} />,
282
+ args: {
283
+ ...Default.args,
284
+ initialData: {
285
+ firstName: 'John',
286
+ lastName: 'Doe',
287
+ email: 'john.doe@example.com',
288
+ phone: '08012345678',
289
+ address: '123 Main Street',
290
+ city: 'Ikeja',
291
+ deliveryZoneId: 'zone-1',
292
+ paymentMethod: 'online',
293
+ },
294
+ },
295
+ };
296
+
297
+ // Without refund policy accordion
298
+ export const NoRefundPolicy: StoryObj<CheckoutStoryArgs> = {
299
+ render: (args) => <CheckoutWrapper {...args} />,
300
+ args: {
301
+ ...Default.args,
302
+ refundPolicy: undefined,
303
+ },
304
+ };
305
+
306
+ // Cash on delivery pre-selected
307
+ export const CashOnDeliverySelected: StoryObj<CheckoutStoryArgs> = {
308
+ render: (args) => <CheckoutWrapper {...args} />,
309
+ args: {
310
+ ...Default.args,
311
+ initialData: {
312
+ paymentMethod: 'cod',
313
+ },
314
+ },
315
+ };
316
+
317
+ // Cart with discount applied
318
+ export const WithDiscount: StoryObj<CheckoutStoryArgs> = {
319
+ render: (args) => {
320
+ const [isOpen, setIsOpen] = useState(false);
321
+ const queryClient = createMockQueryClient();
322
+
323
+ const cartWithDiscount: Cart = {
324
+ ...mockCart,
325
+ pricing: {
326
+ subtotal: 8697,
327
+ discount: {
328
+ code: 'SAVE10',
329
+ type: 'percentage',
330
+ value: 10,
331
+ amount: 870,
332
+ description: '10% off your order',
333
+ },
334
+ deliveryCharge: 0,
335
+ total: 7827,
336
+ },
337
+ };
338
+
339
+ return (
340
+ <QueryClientProvider client={queryClient}>
341
+ <MockCartProvider cart={cartWithDiscount} renderCart={false}>
342
+ <div className="flex flex-col items-center gap-4 p-8">
343
+ <div className="text-center">
344
+ <h2 className="text-2xl font-bold mb-2">Checkout with Discount</h2>
345
+ <p className="text-gray-600 mb-4">10% discount applied to cart</p>
346
+ </div>
347
+ <Button onClick={() => setIsOpen(true)} size="lg">
348
+ Start Checkout
349
+ </Button>
350
+ </div>
351
+ <Checkout
352
+ {...args}
353
+ isOpen={isOpen}
354
+ onClose={() => setIsOpen(false)}
355
+ onSuccess={(orderId) => {
356
+ args.onSuccess?.(orderId);
357
+ }}
358
+ onError={(error) => {
359
+ console.error('Checkout error:', error);
360
+ alert(`Checkout failed: ${error.message}`);
361
+ args.onError?.(error);
362
+ }}
363
+ />
364
+ </MockCartProvider>
365
+ </QueryClientProvider>
366
+ );
367
+ },
368
+ args: {
369
+ ...Default.args,
370
+ },
371
+ };
372
+
373
+ // Custom submit button text
374
+ export const CustomButtonText: StoryObj<CheckoutStoryArgs> = {
375
+ render: (args) => <CheckoutWrapper {...args} />,
376
+ args: {
377
+ ...Default.args,
378
+ submitButtonText: 'Complete Purchase',
379
+ },
380
+ };