hey-pharmacist-ecommerce 1.1.30 → 1.1.31

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 (76) hide show
  1. package/dist/index.d.mts +1451 -1303
  2. package/dist/index.d.ts +1451 -1303
  3. package/dist/index.js +10502 -5728
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +7817 -3059
  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/CouponCodeInput.tsx +190 -0
  10. package/src/components/Header.tsx +5 -1
  11. package/src/components/Notification.tsx +1 -1
  12. package/src/components/NotificationBell.tsx +33 -0
  13. package/src/components/NotificationCard.tsx +211 -0
  14. package/src/components/NotificationDrawer.tsx +195 -0
  15. package/src/components/OrderCard.tsx +164 -99
  16. package/src/components/ProductReviewsSection.tsx +30 -0
  17. package/src/components/RatingDistribution.tsx +86 -0
  18. package/src/components/ReviewCard.tsx +59 -0
  19. package/src/components/ReviewForm.tsx +207 -0
  20. package/src/components/ReviewPromptBanner.tsx +98 -0
  21. package/src/components/ReviewsList.tsx +151 -0
  22. package/src/components/StarRating.tsx +98 -0
  23. package/src/hooks/useDiscounts.ts +7 -0
  24. package/src/hooks/useOrders.ts +15 -0
  25. package/src/hooks/useReviews.ts +230 -0
  26. package/src/index.ts +25 -0
  27. package/src/lib/Apis/apis/discounts-api.ts +23 -72
  28. package/src/lib/Apis/apis/notifications-api.ts +196 -231
  29. package/src/lib/Apis/apis/products-api.ts +84 -0
  30. package/src/lib/Apis/apis/review-api.ts +283 -4
  31. package/src/lib/Apis/apis/stores-api.ts +180 -0
  32. package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
  33. package/src/lib/Apis/models/cart-body-populated.ts +3 -3
  34. package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
  35. package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
  36. package/src/lib/Apis/models/create-discount-dto.ts +31 -92
  37. package/src/lib/Apis/models/create-review-dto.ts +4 -4
  38. package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
  39. package/src/lib/Apis/models/create-store-dto.ts +6 -0
  40. package/src/lib/Apis/models/discount.ts +37 -98
  41. package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
  42. package/src/lib/Apis/models/index.ts +13 -7
  43. package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
  44. package/src/lib/Apis/models/manual-order-dto.ts +3 -3
  45. package/src/lib/Apis/models/populated-discount.ts +41 -101
  46. package/src/lib/Apis/models/preference-update-item.ts +59 -0
  47. package/src/lib/Apis/models/product-light-dto.ts +40 -0
  48. package/src/lib/Apis/models/{check-notifications-response-dto.ts → review-status-dto.ts} +8 -7
  49. package/src/lib/Apis/models/review.ts +9 -3
  50. package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
  51. package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
  52. package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
  53. package/src/lib/Apis/models/store-entity.ts +6 -0
  54. package/src/lib/Apis/models/store.ts +6 -0
  55. package/src/lib/Apis/models/update-discount-dto.ts +31 -92
  56. package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
  57. package/src/lib/Apis/models/update-review-dto.ts +4 -4
  58. package/src/lib/Apis/models/update-store-dto.ts +6 -0
  59. package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
  60. package/src/lib/utils/discount.ts +155 -0
  61. package/src/lib/validations/discount.ts +11 -0
  62. package/src/providers/CartProvider.tsx +2 -2
  63. package/src/providers/DiscountProvider.tsx +97 -0
  64. package/src/providers/EcommerceProvider.tsx +13 -5
  65. package/src/providers/NotificationCenterProvider.tsx +436 -0
  66. package/src/screens/CartScreen.tsx +1 -1
  67. package/src/screens/CheckoutScreen.tsx +39 -12
  68. package/src/screens/NotificationSettingsScreen.tsx +413 -0
  69. package/src/screens/OrderDetailScreen.tsx +283 -0
  70. package/src/screens/OrderReviewsScreen.tsx +308 -0
  71. package/src/screens/OrdersScreen.tsx +31 -7
  72. package/src/screens/ProductDetailScreen.tsx +24 -11
  73. package/src/screens/ProfileScreen.tsx +5 -0
  74. package/src/lib/Apis/models/create-notification-dto.ts +0 -75
  75. package/src/lib/Apis/models/notification.ts +0 -93
  76. package/src/lib/Apis/models/single-notification-dto.ts +0 -99
@@ -0,0 +1,436 @@
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
+ console.log('Raw API response:', {
104
+ fullResponse: response,
105
+ data: response.data,
106
+ dataType: typeof response.data,
107
+ isArray: Array.isArray(response.data),
108
+ keys: (response.data as any) ? Object.keys(response.data as any) : [],
109
+ });
110
+
111
+ const data = response.data as any;
112
+ let rawNotifications = [];
113
+
114
+ if (Array.isArray(data)) {
115
+ rawNotifications = data;
116
+ } else if (data?.notifications && Array.isArray(data.notifications)) {
117
+ rawNotifications = data.notifications;
118
+ } else if (data?.data && Array.isArray(data.data)) {
119
+ rawNotifications = data.data;
120
+ }
121
+
122
+
123
+ // Map API response to our NotificationData interface
124
+ const newNotifications = rawNotifications.map((n: any) => ({
125
+ _id: n.id || n._id, // API returns 'id', we use '_id' internally
126
+ type: n.type,
127
+ title: n.title,
128
+ body: n.body,
129
+ isRead: n.isRead,
130
+ createdAt: n.createdAt,
131
+ data: n.data,
132
+ }));
133
+
134
+ console.log('Fetched and mapped notifications:', {
135
+ pageNum,
136
+ append,
137
+ rawCount: rawNotifications.length,
138
+ mappedCount: newNotifications.length,
139
+ firstNotification: newNotifications[0],
140
+ });
141
+
142
+ if (append) {
143
+ setNotifications(prev => [...prev, ...newNotifications]);
144
+ } else {
145
+ setNotifications(newNotifications);
146
+ }
147
+
148
+ setHasMore(newNotifications.length === 20);
149
+ setPage(pageNum);
150
+ } catch (error) {
151
+ console.error('Failed to fetch notifications:', error);
152
+ } finally {
153
+ setIsLoading(false);
154
+ }
155
+ }, [isAuthenticated]);
156
+
157
+ // Fetch settings
158
+ const fetchSettings = useCallback(async () => {
159
+ if (!isAuthenticated) return;
160
+
161
+ setIsLoading(true);
162
+ try {
163
+ const response = await notificationsApi.current.getSettings();
164
+ console.log('Settings API response raw:', response);
165
+
166
+ const rawData = response.data as any;
167
+
168
+ // Handle different API response structures robustly
169
+ let finalSettings: NotificationSettings | null = null;
170
+
171
+ if (rawData) {
172
+ if (Array.isArray(rawData)) {
173
+ // Raw array [ { type: '...', settings: { ... } } ]
174
+ finalSettings = { preferences: rawData };
175
+ } else if (rawData.preferences) {
176
+ if (Array.isArray(rawData.preferences)) {
177
+ // Standard nested preferences array
178
+ finalSettings = rawData;
179
+ } else if (typeof rawData.preferences === 'object') {
180
+ // Backend returned preferences as an object { TYPE: { email: true } }
181
+ // Convert to our expected array format
182
+ const preferences = Object.entries(rawData.preferences).map(([type, settings]) => ({
183
+ type: type as any,
184
+ settings: settings as any
185
+ }));
186
+ finalSettings = { ...rawData, preferences };
187
+ }
188
+ } else if (rawData.data) {
189
+ const d = rawData.data;
190
+ if (d.preferences) {
191
+ if (Array.isArray(d.preferences)) {
192
+ finalSettings = d;
193
+ } else if (typeof d.preferences === 'object') {
194
+ const preferences = Object.entries(d.preferences).map(([type, settings]) => ({
195
+ type: type as any,
196
+ settings: settings as any
197
+ }));
198
+ finalSettings = { ...d, preferences };
199
+ }
200
+ } else if (Array.isArray(d)) {
201
+ finalSettings = { preferences: d };
202
+ }
203
+ } else if (typeof rawData === 'object' && !rawData.preferences) {
204
+ // Maybe it's just the preferences object directly { TYPE: { email: true } }
205
+ const preferences = Object.entries(rawData).map(([type, settings]) => ({
206
+ type: type as any,
207
+ settings: settings as any
208
+ }));
209
+ if (preferences.length > 0 && preferences[0].settings && typeof preferences[0].settings === 'object') {
210
+ finalSettings = { preferences };
211
+ } else {
212
+ finalSettings = rawData;
213
+ }
214
+ } else {
215
+ finalSettings = rawData;
216
+ }
217
+ }
218
+
219
+ console.log('Parsed settings:', finalSettings);
220
+ setSettings(finalSettings);
221
+ } catch (error) {
222
+ console.error('Failed to fetch settings:', error);
223
+ } finally {
224
+ setIsLoading(false);
225
+ }
226
+ }, [isAuthenticated]);
227
+
228
+ // SSE Connection for real-time notifications
229
+ useEffect(() => {
230
+ if (!isAuthenticated) {
231
+ // Close existing connection if user logs out
232
+ if (eventSourceRef.current) {
233
+ eventSourceRef.current.close();
234
+ eventSourceRef.current = null;
235
+ }
236
+ return;
237
+ }
238
+
239
+ const token = getAuthToken();
240
+ if (!token) return;
241
+
242
+ try {
243
+ const config = getCurrentConfig();
244
+ const sseUrl = `${config.apiBaseUrl}/notifications/stream?token=${encodeURIComponent(token)}`;
245
+
246
+ const eventSource = new EventSource(sseUrl);
247
+ eventSourceRef.current = eventSource;
248
+
249
+ eventSource.onmessage = (event) => {
250
+ try {
251
+ const rawNotification = JSON.parse(event.data);
252
+
253
+ // Map API response to our NotificationData interface
254
+ const notification: NotificationData = {
255
+ _id: rawNotification.id || rawNotification._id,
256
+ type: rawNotification.type,
257
+ title: rawNotification.title,
258
+ body: rawNotification.body,
259
+ isRead: rawNotification.isRead || false,
260
+ createdAt: rawNotification.createdAt,
261
+ data: rawNotification.data,
262
+ };
263
+
264
+ // Add to top of list
265
+ setNotifications(prev => [notification, ...prev]);
266
+
267
+ // Increment unread count
268
+ setUnreadCount(prev => prev + 1);
269
+
270
+ // Show browser notification if supported and drawer is closed
271
+ if ('Notification' in window && Notification.permission === 'granted' && !isDrawerOpen) {
272
+ new Notification(notification.title, {
273
+ body: notification.body,
274
+ icon: '/icon.png', // Add your app icon
275
+ });
276
+ }
277
+ } catch (error) {
278
+ console.error('Failed to parse SSE notification:', error);
279
+ }
280
+ };
281
+
282
+ eventSource.onerror = (error) => {
283
+ console.error('SSE connection error:', error);
284
+ eventSource.close();
285
+
286
+ // Attempt to reconnect after 5 seconds
287
+ setTimeout(() => {
288
+ if (isAuthenticated && getAuthToken()) {
289
+ // Trigger re-render to recreate connection
290
+ fetchUnreadCount();
291
+ }
292
+ }, 5000);
293
+ };
294
+
295
+ return () => {
296
+ eventSource.close();
297
+ };
298
+ } catch (error) {
299
+ console.error('Failed to establish SSE connection:', error);
300
+ }
301
+ }, [isAuthenticated, isDrawerOpen, fetchUnreadCount]);
302
+
303
+ // Initial data fetch
304
+ useEffect(() => {
305
+ if (isAuthenticated) {
306
+ fetchUnreadCount();
307
+ fetchNotifications(1);
308
+ fetchSettings();
309
+ } else {
310
+ // Reset state when logged out
311
+ setNotifications([]);
312
+ setUnreadCount(0);
313
+ setSettings(null);
314
+ setPage(1);
315
+ setHasMore(true);
316
+ }
317
+ }, [isAuthenticated, fetchUnreadCount, fetchNotifications, fetchSettings]);
318
+
319
+ // Request notification permission
320
+ useEffect(() => {
321
+ if ('Notification' in window && Notification.permission === 'default') {
322
+ Notification.requestPermission();
323
+ }
324
+ }, []);
325
+
326
+ const openDrawer = useCallback(() => {
327
+ setIsDrawerOpen(true);
328
+ }, []);
329
+
330
+ const closeDrawer = useCallback(() => {
331
+ setIsDrawerOpen(false);
332
+ }, []);
333
+
334
+ const markAsRead = useCallback(async (id: string) => {
335
+ try {
336
+ await notificationsApi.current.markAsRead(id);
337
+
338
+ // Update local state
339
+ setNotifications(prev =>
340
+ prev.map(n => (n._id === id ? { ...n, isRead: true } : n))
341
+ );
342
+
343
+ // Decrement unread count
344
+ setUnreadCount(prev => Math.max(0, prev - 1));
345
+ } catch (error) {
346
+ console.error('Failed to mark notification as read:', error);
347
+ }
348
+ }, []);
349
+
350
+ const markAllAsRead = useCallback(async () => {
351
+ try {
352
+ await notificationsApi.current.markAllAsRead();
353
+
354
+ // Update local state
355
+ setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
356
+ setUnreadCount(0);
357
+ } catch (error) {
358
+ console.error('Failed to mark all as read:', error);
359
+ }
360
+ }, []);
361
+
362
+ const deleteNotification = useCallback(async (id: string) => {
363
+ try {
364
+ await notificationsApi.current.deleteNotification(id);
365
+
366
+ // Update local state
367
+ const notification = notifications.find(n => n._id === id);
368
+ setNotifications(prev => prev.filter(n => n._id !== id));
369
+
370
+ // Decrement unread count if notification was unread
371
+ if (notification && !notification.isRead) {
372
+ setUnreadCount(prev => Math.max(0, prev - 1));
373
+ }
374
+ } catch (error) {
375
+ console.error('Failed to delete notification:', error);
376
+ }
377
+ }, [notifications]);
378
+
379
+ const loadMore = useCallback(async () => {
380
+ if (!hasMore || isLoading) return;
381
+ await fetchNotifications(page + 1, true);
382
+ }, [hasMore, isLoading, page, fetchNotifications]);
383
+
384
+ const refreshNotifications = useCallback(async () => {
385
+ await fetchNotifications(1, false);
386
+ await fetchUnreadCount();
387
+ }, [fetchNotifications, fetchUnreadCount]);
388
+
389
+ const updateSettings = useCallback(async (newSettings: NotificationSettings) => {
390
+ try {
391
+ // Clean the settings object to match UpdateNotificationSettingsDto
392
+ // and remove any frontend-only fields that might cause validation errors
393
+ const payload: UpdateNotificationSettingsDto = {
394
+ _id: newSettings._id,
395
+ preferences: newSettings.preferences.map(p => ({
396
+ type: p.type,
397
+ settings: {
398
+ email: p.settings?.email,
399
+ push: p.settings?.push,
400
+ inApp: p.settings?.inApp,
401
+ }
402
+ }))
403
+ };
404
+
405
+ console.log('Updating settings with payload:', payload);
406
+ await notificationsApi.current.updateSettings(payload);
407
+ setSettings(newSettings);
408
+ } catch (error) {
409
+ console.error('Failed to update settings:', error);
410
+ throw error;
411
+ }
412
+ }, []);
413
+
414
+ const value: NotificationCenterContextValue = {
415
+ notifications,
416
+ unreadCount,
417
+ isLoading,
418
+ isDrawerOpen,
419
+ settings,
420
+ openDrawer,
421
+ closeDrawer,
422
+ markAsRead,
423
+ markAllAsRead,
424
+ deleteNotification,
425
+ loadMore,
426
+ hasMore,
427
+ refreshNotifications,
428
+ updateSettings,
429
+ };
430
+
431
+ return (
432
+ <NotificationCenterContext.Provider value={value}>
433
+ {children}
434
+ </NotificationCenterContext.Provider>
435
+ );
436
+ }
@@ -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>