hey-pharmacist-ecommerce 1.1.30 → 1.1.32

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/dist/index.d.mts +1451 -1303
  2. package/dist/index.d.ts +1451 -1303
  3. package/dist/index.js +6162 -1563
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +5854 -1271
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +4 -3
  8. package/src/components/AccountReviewsTab.tsx +97 -0
  9. package/src/components/AccountSettingsTab.tsx +0 -50
  10. package/src/components/CouponCodeInput.tsx +190 -0
  11. package/src/components/Header.tsx +5 -1
  12. package/src/components/Notification.tsx +1 -1
  13. package/src/components/NotificationBell.tsx +33 -0
  14. package/src/components/NotificationCard.tsx +211 -0
  15. package/src/components/NotificationDrawer.tsx +188 -0
  16. package/src/components/OrderCard.tsx +164 -99
  17. package/src/components/ProductCard.tsx +3 -3
  18. package/src/components/ProductReviewsSection.tsx +30 -0
  19. package/src/components/RatingDistribution.tsx +86 -0
  20. package/src/components/ReviewCard.tsx +59 -0
  21. package/src/components/ReviewForm.tsx +207 -0
  22. package/src/components/ReviewPromptBanner.tsx +98 -0
  23. package/src/components/ReviewsList.tsx +151 -0
  24. package/src/components/StarRating.tsx +98 -0
  25. package/src/components/TabNavigation.tsx +1 -1
  26. package/src/components/ui/Button.tsx +1 -1
  27. package/src/hooks/useDiscounts.ts +7 -0
  28. package/src/hooks/useOrders.ts +15 -0
  29. package/src/hooks/useReviews.ts +230 -0
  30. package/src/index.ts +25 -0
  31. package/src/lib/Apis/apis/discounts-api.ts +23 -72
  32. package/src/lib/Apis/apis/notifications-api.ts +196 -231
  33. package/src/lib/Apis/apis/products-api.ts +84 -0
  34. package/src/lib/Apis/apis/review-api.ts +283 -4
  35. package/src/lib/Apis/apis/stores-api.ts +180 -0
  36. package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
  37. package/src/lib/Apis/models/cart-body-populated.ts +3 -3
  38. package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
  39. package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
  40. package/src/lib/Apis/models/create-discount-dto.ts +31 -92
  41. package/src/lib/Apis/models/create-review-dto.ts +4 -4
  42. package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
  43. package/src/lib/Apis/models/create-store-dto.ts +6 -0
  44. package/src/lib/Apis/models/discount.ts +37 -98
  45. package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
  46. package/src/lib/Apis/models/index.ts +13 -7
  47. package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
  48. package/src/lib/Apis/models/manual-order-dto.ts +3 -3
  49. package/src/lib/Apis/models/populated-discount.ts +41 -101
  50. package/src/lib/Apis/models/preference-update-item.ts +59 -0
  51. package/src/lib/Apis/models/product-light-dto.ts +40 -0
  52. package/src/lib/Apis/models/{check-notifications-response-dto.ts → review-status-dto.ts} +8 -7
  53. package/src/lib/Apis/models/review.ts +9 -3
  54. package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
  55. package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
  56. package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
  57. package/src/lib/Apis/models/store-entity.ts +6 -0
  58. package/src/lib/Apis/models/store.ts +6 -0
  59. package/src/lib/Apis/models/update-discount-dto.ts +31 -92
  60. package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
  61. package/src/lib/Apis/models/update-review-dto.ts +4 -4
  62. package/src/lib/Apis/models/update-store-dto.ts +6 -0
  63. package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
  64. package/src/lib/utils/discount.ts +155 -0
  65. package/src/lib/validations/discount.ts +11 -0
  66. package/src/providers/CartProvider.tsx +2 -2
  67. package/src/providers/DiscountProvider.tsx +97 -0
  68. package/src/providers/EcommerceProvider.tsx +13 -5
  69. package/src/providers/NotificationCenterProvider.tsx +420 -0
  70. package/src/screens/CartScreen.tsx +1 -1
  71. package/src/screens/CheckoutScreen.tsx +39 -12
  72. package/src/screens/NotificationSettingsScreen.tsx +321 -0
  73. package/src/screens/OrderDetailScreen.tsx +283 -0
  74. package/src/screens/OrderReviewsScreen.tsx +308 -0
  75. package/src/screens/OrdersScreen.tsx +31 -7
  76. package/src/screens/ProductDetailScreen.tsx +24 -11
  77. package/src/screens/ProfileScreen.tsx +5 -0
  78. package/src/styles/globals.css +4 -0
  79. package/styles/base.css +6 -0
  80. package/styles/globals.css +3 -0
  81. package/src/lib/Apis/models/create-notification-dto.ts +0 -75
  82. package/src/lib/Apis/models/notification.ts +0 -93
  83. package/src/lib/Apis/models/single-notification-dto.ts +0 -99
@@ -0,0 +1,420 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
4
+ import { NotificationsApi } from '@/lib/Apis/apis/notifications-api';
5
+ import { getApiConfiguration, getAuthToken, getCurrentConfig } from '@/lib/api-adapter/config';
6
+ import { useAuth } from './AuthProvider';
7
+ import { PreferenceUpdateItemTypeEnum, UpdateNotificationSettingsDto } from '@/lib/Apis/models';
8
+
9
+ // Re-export the enum for convenience
10
+ export type NotificationType = PreferenceUpdateItemTypeEnum;
11
+ export const NotificationType = PreferenceUpdateItemTypeEnum;
12
+ export { PreferenceUpdateItemTypeEnum };
13
+
14
+ export interface NotificationData {
15
+ _id: string;
16
+ type: NotificationType;
17
+ title: string;
18
+ body: string;
19
+ isRead: boolean;
20
+ createdAt: string;
21
+ data?: {
22
+ orderId?: string;
23
+ productId?: string;
24
+ [key: string]: any;
25
+ };
26
+ }
27
+
28
+ export interface NotificationSettings {
29
+ _id?: string;
30
+ userId?: string;
31
+ preferences: Array<{
32
+ type: NotificationType;
33
+ settings: {
34
+ email?: boolean;
35
+ push?: boolean;
36
+ inApp?: boolean;
37
+ };
38
+ }>;
39
+ }
40
+
41
+ interface NotificationCenterContextValue {
42
+ notifications: NotificationData[];
43
+ unreadCount: number;
44
+ isLoading: boolean;
45
+ isDrawerOpen: boolean;
46
+ settings: NotificationSettings | null;
47
+ openDrawer: () => void;
48
+ closeDrawer: () => void;
49
+ markAsRead: (id: string) => Promise<void>;
50
+ markAllAsRead: () => Promise<void>;
51
+ deleteNotification: (id: string) => Promise<void>;
52
+ loadMore: () => Promise<void>;
53
+ hasMore: boolean;
54
+ refreshNotifications: () => Promise<void>;
55
+ updateSettings: (settings: NotificationSettings) => Promise<void>;
56
+ }
57
+
58
+ const NotificationCenterContext = createContext<NotificationCenterContextValue | undefined>(undefined);
59
+
60
+ export function useNotificationCenter() {
61
+ const context = useContext(NotificationCenterContext);
62
+ if (!context) {
63
+ throw new Error('useNotificationCenter must be used within NotificationCenterProvider');
64
+ }
65
+ return context;
66
+ }
67
+
68
+ interface NotificationCenterProviderProps {
69
+ children: React.ReactNode;
70
+ }
71
+
72
+ export function NotificationCenterProvider({ children }: NotificationCenterProviderProps) {
73
+ const { isAuthenticated } = useAuth();
74
+ const [notifications, setNotifications] = useState<NotificationData[]>([]);
75
+ const [unreadCount, setUnreadCount] = useState(0);
76
+ const [isLoading, setIsLoading] = useState(false);
77
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
78
+ const [settings, setSettings] = useState<NotificationSettings | null>(null);
79
+ const [page, setPage] = useState(1);
80
+ const [hasMore, setHasMore] = useState(true);
81
+ const eventSourceRef = useRef<EventSource | null>(null);
82
+ const notificationsApi = useRef(new NotificationsApi(getApiConfiguration()));
83
+
84
+ // Fetch unread count
85
+ const fetchUnreadCount = useCallback(async () => {
86
+ if (!isAuthenticated) return;
87
+
88
+ try {
89
+ const response = await notificationsApi.current.getUnreadCount();
90
+ setUnreadCount((response.data as any)?.unreadCount || 0);
91
+ } catch (error) {
92
+ console.error('Failed to fetch unread count:', error);
93
+ }
94
+ }, [isAuthenticated]);
95
+
96
+ // Fetch notifications
97
+ const fetchNotifications = useCallback(async (pageNum: number = 1, append: boolean = false) => {
98
+ if (!isAuthenticated) return;
99
+
100
+ setIsLoading(true);
101
+ try {
102
+ const response = await notificationsApi.current.getNotifications(pageNum, 20, false);
103
+ const data = response.data as any;
104
+ let rawNotifications = [];
105
+
106
+ if (Array.isArray(data)) {
107
+ rawNotifications = data;
108
+ } else if (data?.notifications && Array.isArray(data.notifications)) {
109
+ rawNotifications = data.notifications;
110
+ } else if (data?.data && Array.isArray(data.data)) {
111
+ rawNotifications = data.data;
112
+ }
113
+
114
+
115
+ // Map API response to our NotificationData interface
116
+ const newNotifications = rawNotifications.map((n: any) => ({
117
+ _id: n.id || n._id, // API returns 'id', we use '_id' internally
118
+ type: n.type,
119
+ title: n.title,
120
+ body: n.body,
121
+ isRead: n.isRead,
122
+ createdAt: n.createdAt,
123
+ data: n.data,
124
+ }));
125
+
126
+ if (append) {
127
+ setNotifications(prev => [...prev, ...newNotifications]);
128
+ } else {
129
+ setNotifications(newNotifications);
130
+ }
131
+
132
+ setHasMore(newNotifications.length === 20);
133
+ setPage(pageNum);
134
+ } catch (error) {
135
+ console.error('Failed to fetch notifications:', error);
136
+ } finally {
137
+ setIsLoading(false);
138
+ }
139
+ }, [isAuthenticated]);
140
+
141
+ // Fetch settings
142
+ const fetchSettings = useCallback(async () => {
143
+ if (!isAuthenticated) return;
144
+
145
+ setIsLoading(true);
146
+ try {
147
+ const response = await notificationsApi.current.getSettings();
148
+ const rawData = response.data as any;
149
+
150
+ // Handle different API response structures robustly
151
+ let finalSettings: NotificationSettings | null = null;
152
+
153
+ if (rawData) {
154
+ if (Array.isArray(rawData)) {
155
+ // Raw array [ { type: '...', settings: { ... } } ]
156
+ finalSettings = { preferences: rawData };
157
+ } else if (rawData.preferences) {
158
+ if (Array.isArray(rawData.preferences)) {
159
+ // Standard nested preferences array
160
+ finalSettings = rawData;
161
+ } else if (typeof rawData.preferences === 'object') {
162
+ // Backend returned preferences as an object { TYPE: { email: true } }
163
+ // Convert to our expected array format
164
+ const preferences = Object.entries(rawData.preferences).map(([type, settings]) => ({
165
+ type: type as any,
166
+ settings: settings as any
167
+ }));
168
+ finalSettings = { ...rawData, preferences };
169
+ }
170
+ } else if (rawData.data) {
171
+ const d = rawData.data;
172
+ if (d.preferences) {
173
+ if (Array.isArray(d.preferences)) {
174
+ finalSettings = d;
175
+ } else if (typeof d.preferences === 'object') {
176
+ const preferences = Object.entries(d.preferences).map(([type, settings]) => ({
177
+ type: type as any,
178
+ settings: settings as any
179
+ }));
180
+ finalSettings = { ...d, preferences };
181
+ }
182
+ } else if (Array.isArray(d)) {
183
+ finalSettings = { preferences: d };
184
+ }
185
+ } else if (typeof rawData === 'object' && !rawData.preferences) {
186
+ // Maybe it's just the preferences object directly { TYPE: { email: true } }
187
+ const preferences = Object.entries(rawData).map(([type, settings]) => ({
188
+ type: type as any,
189
+ settings: settings as any
190
+ }));
191
+ if (preferences.length > 0 && preferences[0].settings && typeof preferences[0].settings === 'object') {
192
+ finalSettings = { preferences };
193
+ } else {
194
+ finalSettings = rawData;
195
+ }
196
+ } else {
197
+ finalSettings = rawData;
198
+ }
199
+ }
200
+ setSettings(finalSettings);
201
+ } catch (error) {
202
+ console.error('Failed to fetch settings:', error);
203
+ } finally {
204
+ setIsLoading(false);
205
+ }
206
+ }, [isAuthenticated]);
207
+
208
+ // SSE Connection for real-time notifications
209
+ useEffect(() => {
210
+ if (!isAuthenticated) {
211
+ // Close existing connection if user logs out
212
+ if (eventSourceRef.current) {
213
+ eventSourceRef.current.close();
214
+ eventSourceRef.current = null;
215
+ }
216
+ return;
217
+ }
218
+
219
+ try {
220
+ const config = getCurrentConfig();
221
+ const token = getAuthToken();
222
+
223
+ if (!token) return;
224
+
225
+ // Ensure no double slashes in URL
226
+ const baseUrl = config.apiBaseUrl.endsWith('/') ? config.apiBaseUrl.slice(0, -1) : config.apiBaseUrl;
227
+ const sseUrl = `${baseUrl}/notifications/stream?token=${encodeURIComponent(token)}`;
228
+
229
+ const eventSource = new EventSource(sseUrl);
230
+ eventSourceRef.current = eventSource;
231
+
232
+ eventSource.onmessage = (event) => {
233
+ try {
234
+ const rawNotification = JSON.parse(event.data);
235
+
236
+ // Map API response to our NotificationData interface
237
+ const notification: NotificationData = {
238
+ _id: rawNotification.id || rawNotification._id,
239
+ type: rawNotification.type,
240
+ title: rawNotification.title,
241
+ body: rawNotification.body,
242
+ isRead: rawNotification.isRead || false,
243
+ createdAt: rawNotification.createdAt,
244
+ data: rawNotification.data,
245
+ };
246
+
247
+ // Add to top of list
248
+ setNotifications(prev => [notification, ...prev]);
249
+
250
+ // Increment unread count
251
+ setUnreadCount(prev => prev + 1);
252
+
253
+ // Show browser notification if supported and drawer is closed
254
+ if ('Notification' in window && Notification.permission === 'granted' && !isDrawerOpen) {
255
+ new Notification(notification.title, {
256
+ body: notification.body,
257
+ icon: '/icon.png', // Add your app icon
258
+ });
259
+ }
260
+ } catch (error) {
261
+ console.error('Failed to parse SSE notification:', error);
262
+ }
263
+ };
264
+
265
+ eventSource.onerror = (error) => {
266
+ console.error('SSE connection error:', error);
267
+ eventSource.close();
268
+
269
+ // Attempt to reconnect after 5 seconds
270
+ setTimeout(() => {
271
+ if (isAuthenticated && getAuthToken()) {
272
+ // Trigger re-render to recreate connection
273
+ fetchUnreadCount();
274
+ }
275
+ }, 5000);
276
+ };
277
+
278
+ return () => {
279
+ eventSource.close();
280
+ };
281
+ } catch (error) {
282
+ console.error('Failed to establish SSE connection:', error);
283
+ }
284
+ }, [isAuthenticated, isDrawerOpen, fetchUnreadCount]);
285
+
286
+ // Initial data fetch
287
+ useEffect(() => {
288
+ if (isAuthenticated) {
289
+ fetchUnreadCount();
290
+ fetchNotifications(1);
291
+ fetchSettings();
292
+ } else {
293
+ // Reset state when logged out
294
+ setNotifications([]);
295
+ setUnreadCount(0);
296
+ setSettings(null);
297
+ setPage(1);
298
+ setHasMore(true);
299
+ }
300
+ }, [isAuthenticated, fetchUnreadCount, fetchNotifications, fetchSettings]);
301
+
302
+ // Request notification permission
303
+ useEffect(() => {
304
+ if ('Notification' in window && Notification.permission === 'default') {
305
+ Notification.requestPermission();
306
+ }
307
+ }, []);
308
+
309
+ const openDrawer = useCallback(() => {
310
+ setIsDrawerOpen(true);
311
+ }, []);
312
+
313
+ const closeDrawer = useCallback(() => {
314
+ setIsDrawerOpen(false);
315
+ }, []);
316
+
317
+ const markAsRead = useCallback(async (id: string) => {
318
+ try {
319
+ await notificationsApi.current.markAsRead(id);
320
+
321
+ // Update local state
322
+ setNotifications(prev =>
323
+ prev.map(n => (n._id === id ? { ...n, isRead: true } : n))
324
+ );
325
+
326
+ // Decrement unread count
327
+ setUnreadCount(prev => Math.max(0, prev - 1));
328
+ } catch (error) {
329
+ console.error('Failed to mark notification as read:', error);
330
+ }
331
+ }, []);
332
+
333
+ const markAllAsRead = useCallback(async () => {
334
+ try {
335
+ await notificationsApi.current.markAllAsRead();
336
+
337
+ // Update local state
338
+ setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
339
+ setUnreadCount(0);
340
+ } catch (error) {
341
+ console.error('Failed to mark all as read:', error);
342
+ }
343
+ }, []);
344
+
345
+ const deleteNotification = useCallback(async (id: string) => {
346
+ try {
347
+ await notificationsApi.current.deleteNotification(id);
348
+
349
+ // Update local state
350
+ const notification = notifications.find(n => n._id === id);
351
+ setNotifications(prev => prev.filter(n => n._id !== id));
352
+
353
+ // Decrement unread count if notification was unread
354
+ if (notification && !notification.isRead) {
355
+ setUnreadCount(prev => Math.max(0, prev - 1));
356
+ }
357
+ } catch (error) {
358
+ console.error('Failed to delete notification:', error);
359
+ }
360
+ }, [notifications]);
361
+
362
+ const loadMore = useCallback(async () => {
363
+ if (!hasMore || isLoading) return;
364
+ await fetchNotifications(page + 1, true);
365
+ }, [hasMore, isLoading, page, fetchNotifications]);
366
+
367
+ const refreshNotifications = useCallback(async () => {
368
+ await fetchNotifications(1, false);
369
+ await fetchUnreadCount();
370
+ }, [fetchNotifications, fetchUnreadCount]);
371
+
372
+ const updateSettings = useCallback(async (newSettings: NotificationSettings) => {
373
+ try {
374
+ // Clean the settings object to match UpdateNotificationSettingsDto
375
+ // and remove any frontend-only fields that might cause validation errors
376
+ const payload: UpdateNotificationSettingsDto = {
377
+ _id: newSettings._id,
378
+ preferences: newSettings.preferences.map(p => ({
379
+ type: p.type,
380
+ settings: {
381
+ email: p.settings?.email,
382
+ push: p.settings?.push,
383
+ inApp: p.settings?.inApp,
384
+ }
385
+ }))
386
+ };
387
+
388
+ await notificationsApi.current.updateSettings(payload);
389
+
390
+ // Refetch settings to ensure we have the server-persisted state
391
+ await fetchSettings();
392
+ } catch (error) {
393
+ console.error('Failed to update settings:', error);
394
+ throw error;
395
+ }
396
+ }, [fetchSettings]);
397
+
398
+ const value: NotificationCenterContextValue = {
399
+ notifications,
400
+ unreadCount,
401
+ isLoading,
402
+ isDrawerOpen,
403
+ settings,
404
+ openDrawer,
405
+ closeDrawer,
406
+ markAsRead,
407
+ markAllAsRead,
408
+ deleteNotification,
409
+ loadMore,
410
+ hasMore,
411
+ refreshNotifications,
412
+ updateSettings,
413
+ };
414
+
415
+ return (
416
+ <NotificationCenterContext.Provider value={value}>
417
+ {children}
418
+ </NotificationCenterContext.Provider>
419
+ );
420
+ }
@@ -88,7 +88,7 @@ export function CartScreen() {
88
88
  );
89
89
  }
90
90
 
91
- const subtotal = cart.cartBody.items.reduce((total, item) => total + item.productVariantData.finalPrice * item.quantity, 0);
91
+ const subtotal = Math.round(cart.cartBody.items.reduce((total, item) => total + item.productVariantData.finalPrice * item.quantity, 0) * 100) / 100;
92
92
  const shipping = 0;
93
93
  const tax = 0;
94
94
  const total = subtotal + shipping + tax;
@@ -36,6 +36,8 @@ import { useBasePath } from '@/providers/BasePathProvider';
36
36
  import { addressSchema } from '@/lib/validations/address';
37
37
  import Image from 'next/image';
38
38
  import { useNotification } from '@/providers/NotificationProvider';
39
+ import { CouponCodeInput } from '@/components/CouponCodeInput';
40
+ import { useDiscounts } from '@/hooks/useDiscounts';
39
41
 
40
42
  const checkoutSchema = z.object({
41
43
  shipping: addressSchema,
@@ -89,7 +91,7 @@ export function CheckoutScreen() {
89
91
  const [editingAddress, setEditingAddress] = useState<any | null>(null);
90
92
  const [shippingPrice, setShippingPrice] = useState(0);
91
93
  const notification = useNotification();
92
-
94
+ const { appliedCoupon, calculateCouponDiscount } = useDiscounts();
93
95
 
94
96
  // Use the addresses hook
95
97
  const {
@@ -183,19 +185,19 @@ export function CheckoutScreen() {
183
185
  name: user ? `${user.firstname} ${user.lastname}` : '',
184
186
  phone: user?.phoneNumber || '',
185
187
  country: 'United States',
186
- // street1: '',
187
- // city: '',
188
- // state: '',
189
- // zip: '',
188
+ street1: '',
189
+ city: '',
190
+ state: '',
191
+ zip: '',
190
192
  },
191
193
  billing: {
192
194
  name: user ? `${user.firstname} ${user.lastname}` : '',
193
195
  phone: user?.phoneNumber || '',
194
196
  country: 'United States',
195
- // street1: '',
196
- // city: '',
197
- // state: '',
198
- // zip: '',
197
+ street1: '',
198
+ city: '',
199
+ state: '',
200
+ zip: '',
199
201
  },
200
202
  },
201
203
  });
@@ -443,6 +445,7 @@ export function CheckoutScreen() {
443
445
  orderRemindingDates: [],
444
446
  shippingAddress: data.shipping,
445
447
  billingAddress: sameAsShipping ? data.shipping : data.billing,
448
+ ...(appliedCoupon && { discountId: appliedCoupon._id || appliedCoupon.id }),
446
449
  ...(manualShipping && { manualShipping }),
447
450
  };
448
451
 
@@ -521,13 +524,23 @@ export function CheckoutScreen() {
521
524
  }
522
525
  };
523
526
 
527
+ // Redirect if cart is empty - use useEffect to avoid rendering during render
528
+ React.useEffect(() => {
529
+ if (!cart || cart?.cartBody?.items?.length === 0 || !cart?.cartBody?.items) {
530
+ router.push(buildPath('/cart'));
531
+ }
532
+ }, [cart, router]);
533
+
524
534
  if (!cart || cart?.cartBody?.items?.length === 0 || !cart?.cartBody?.items) {
525
- router.push(buildPath('/cart'));
526
535
  return null;
527
536
  }
528
- const subtotal = cart.cartBody.items.reduce((total, item) => total + item.productVariantData.finalPrice * item.quantity, 0);
537
+
538
+ const subtotal = Math.round(cart.cartBody.items.reduce((total, item) => total + item.productVariantData.finalPrice * item.quantity, 0) * 100) / 100;
539
+ const discountAmount = appliedCoupon ? calculateCouponDiscount(subtotal) : 0;
540
+ const subtotalAfterDiscount = subtotal - discountAmount;
529
541
  const tax = 0;
530
- const total = subtotal + shippingPrice + tax;
542
+ const total = subtotalAfterDiscount + shippingPrice + tax;
543
+
531
544
 
532
545
  return (
533
546
  <div className="min-h-screen bg-white pb-16">
@@ -993,6 +1006,14 @@ export function CheckoutScreen() {
993
1006
 
994
1007
  <div className="h-px bg-[#5B9BD5]/20 my-4" />
995
1008
 
1009
+ {/* Coupon Code Section */}
1010
+ <div className="mb-6">
1011
+ <CouponCodeInput
1012
+ userId={user?.id}
1013
+ className="mb-4"
1014
+ />
1015
+ </div>
1016
+
996
1017
  {/* Totals */}
997
1018
  <div className="text-sm text-slate-600 space-y-3 py-4">
998
1019
  <div className="flex items-center justify-between">
@@ -1001,6 +1022,12 @@ export function CheckoutScreen() {
1001
1022
  {formatPrice(subtotal)}
1002
1023
  </span>
1003
1024
  </div>
1025
+ {discountAmount > 0 && (
1026
+ <div className="flex items-center justify-between text-green-600">
1027
+ <span>Discount ({appliedCoupon?.code})</span>
1028
+ <span className="font-semibold">-{formatPrice(discountAmount)}</span>
1029
+ </div>
1030
+ )}
1004
1031
  {isDelivery && (
1005
1032
  <div className="flex items-center justify-between">
1006
1033
  <span>Shipping</span>