hey-pharmacist-ecommerce 1.1.29 → 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 (104) hide show
  1. package/dist/index.d.mts +10957 -1331
  2. package/dist/index.d.ts +10957 -1331
  3. package/dist/index.js +12364 -5144
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +9353 -2205
  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/hooks/useStoreCapabilities.ts +87 -0
  27. package/src/index.ts +29 -0
  28. package/src/lib/Apis/apis/auth-api.ts +19 -7
  29. package/src/lib/Apis/apis/categories-api.ts +97 -0
  30. package/src/lib/Apis/apis/discounts-api.ts +23 -72
  31. package/src/lib/Apis/apis/notifications-api.ts +196 -231
  32. package/src/lib/Apis/apis/products-api.ts +181 -0
  33. package/src/lib/Apis/apis/review-api.ts +283 -4
  34. package/src/lib/Apis/apis/shipping-api.ts +105 -0
  35. package/src/lib/Apis/apis/stores-api.ts +536 -0
  36. package/src/lib/Apis/apis/sub-categories-api.ts +97 -0
  37. package/src/lib/Apis/apis/users-api.ts +8 -8
  38. package/src/lib/Apis/models/address-created-request.ts +0 -12
  39. package/src/lib/Apis/models/address.ts +0 -12
  40. package/src/lib/Apis/models/api-key-info-dto.ts +49 -0
  41. package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
  42. package/src/lib/Apis/models/cart-body-populated.ts +3 -3
  43. package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
  44. package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
  45. package/src/lib/Apis/models/create-address-dto.ts +0 -12
  46. package/src/lib/Apis/models/create-discount-dto.ts +31 -100
  47. package/src/lib/Apis/models/create-review-dto.ts +4 -4
  48. package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
  49. package/src/lib/Apis/models/create-store-address-dto.ts +0 -12
  50. package/src/lib/Apis/models/create-store-dto-settings.ts +51 -0
  51. package/src/lib/Apis/models/create-store-dto.ts +13 -0
  52. package/src/lib/Apis/models/create-variant-dto.ts +0 -6
  53. package/src/lib/Apis/models/discount.ts +37 -106
  54. package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
  55. package/src/lib/Apis/models/index.ts +24 -7
  56. package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
  57. package/src/lib/Apis/models/manual-order-dto.ts +3 -3
  58. package/src/lib/Apis/models/populated-discount.ts +41 -109
  59. package/src/lib/Apis/models/preference-update-item.ts +59 -0
  60. package/src/lib/Apis/models/product-light-dto.ts +40 -0
  61. package/src/lib/Apis/models/product-variant.ts +0 -6
  62. package/src/lib/Apis/models/reorder-categories-dto.ts +27 -0
  63. package/src/lib/Apis/models/reorder-products-dto.ts +49 -0
  64. package/src/lib/Apis/models/{check-notifications-response-dto.ts → reorder-products-success-response-dto.ts} +7 -7
  65. package/src/lib/Apis/models/reorder-subcategories-dto.ts +33 -0
  66. package/src/lib/Apis/models/reorder-success-response-dto.ts +33 -0
  67. package/src/lib/Apis/models/review-status-dto.ts +34 -0
  68. package/src/lib/Apis/models/review.ts +9 -3
  69. package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
  70. package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
  71. package/src/lib/Apis/models/shipment-with-order.ts +18 -0
  72. package/src/lib/Apis/models/shipment.ts +18 -0
  73. package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
  74. package/src/lib/Apis/models/store-api-keys-response-dto.ts +34 -0
  75. package/src/lib/Apis/models/store-capabilities-dto.ts +63 -0
  76. package/src/lib/Apis/models/store-entity.ts +13 -0
  77. package/src/lib/Apis/models/store.ts +13 -0
  78. package/src/lib/Apis/models/update-address-dto.ts +0 -12
  79. package/src/lib/Apis/models/update-api-keys-dto.ts +39 -0
  80. package/src/lib/Apis/models/update-discount-dto.ts +31 -100
  81. package/src/lib/Apis/models/update-manual-shipment-status-dto.ts +47 -0
  82. package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
  83. package/src/lib/Apis/models/update-review-dto.ts +4 -4
  84. package/src/lib/Apis/models/update-store-dto.ts +13 -0
  85. package/src/lib/Apis/models/update-variant-dto.ts +0 -6
  86. package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
  87. package/src/lib/utils/discount.ts +155 -0
  88. package/src/lib/validations/discount.ts +11 -0
  89. package/src/providers/CartProvider.tsx +2 -2
  90. package/src/providers/DiscountProvider.tsx +97 -0
  91. package/src/providers/EcommerceProvider.tsx +13 -5
  92. package/src/providers/NotificationCenterProvider.tsx +436 -0
  93. package/src/screens/CartScreen.tsx +1 -1
  94. package/src/screens/CheckoutScreen.tsx +402 -290
  95. package/src/screens/NotificationSettingsScreen.tsx +413 -0
  96. package/src/screens/OrderDetailScreen.tsx +283 -0
  97. package/src/screens/OrderReviewsScreen.tsx +308 -0
  98. package/src/screens/OrdersScreen.tsx +31 -7
  99. package/src/screens/ProductDetailScreen.tsx +24 -11
  100. package/src/screens/ProfileScreen.tsx +5 -0
  101. package/src/screens/ResetPasswordScreen.tsx +10 -4
  102. package/src/lib/Apis/models/create-notification-dto.ts +0 -75
  103. package/src/lib/Apis/models/notification.ts +0 -93
  104. package/src/lib/Apis/models/single-notification-dto.ts +0 -99
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useState, useCallback } from 'react';
4
+ import { Discount } from '@/lib/Apis/models/discount';
5
+ import { DiscountsApi } from '@/lib/Apis/apis/discounts-api';
6
+ import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
7
+ import { calculateDiscountAmount } from '@/lib/utils/discount';
8
+
9
+ interface DiscountContextType {
10
+ appliedCoupon: Discount | null;
11
+ couponError: string | null;
12
+ isValidatingCoupon: boolean;
13
+ validateAndApplyCoupon: (code: string, userId?: string) => Promise<{ success: boolean; error?: string; discount?: Discount }>;
14
+ removeCoupon: () => void;
15
+ calculateCouponDiscount: (subtotal: number) => number;
16
+ }
17
+
18
+ const DiscountContext = createContext<DiscountContextType | undefined>(undefined);
19
+
20
+ export function DiscountProvider({ children }: { children: React.ReactNode }) {
21
+ const [appliedCoupon, setAppliedCoupon] = useState<Discount | null>(null);
22
+ const [couponError, setCouponError] = useState<string | null>(null);
23
+ const [isValidatingCoupon, setIsValidatingCoupon] = useState(false);
24
+
25
+ const validateAndApplyCoupon = useCallback(
26
+ async (code: string, userId?: string) => {
27
+ setIsValidatingCoupon(true);
28
+ setCouponError(null);
29
+
30
+ try {
31
+ const api = new DiscountsApi(AXIOS_CONFIG);
32
+ const response = await api.validateCoupon(code, userId);
33
+ const discount = response.data;
34
+
35
+ if (!discount) {
36
+ setCouponError('Coupon code not found');
37
+ setAppliedCoupon(null);
38
+ return { success: false, error: 'Coupon code not found' };
39
+ }
40
+
41
+ setAppliedCoupon(discount);
42
+ setCouponError(null);
43
+ return { success: true, discount };
44
+ } catch (error: any) {
45
+ const errorMessage =
46
+ error.response?.data?.message || 'Failed to validate coupon code';
47
+ setCouponError(errorMessage);
48
+ setAppliedCoupon(null);
49
+ return { success: false, error: errorMessage };
50
+ } finally {
51
+ setIsValidatingCoupon(false);
52
+ }
53
+ },
54
+ []
55
+ );
56
+
57
+ const removeCoupon = useCallback(() => {
58
+ setAppliedCoupon(null);
59
+ setCouponError(null);
60
+ }, []);
61
+
62
+ const calculateCouponDiscount = useCallback(
63
+ (subtotal: number): number => {
64
+ if (!appliedCoupon) {
65
+ return 0;
66
+ }
67
+ const discount = calculateDiscountAmount(subtotal, appliedCoupon);
68
+ return discount;
69
+ },
70
+ [appliedCoupon]
71
+ );
72
+
73
+ const value: DiscountContextType = {
74
+ appliedCoupon,
75
+ couponError,
76
+ isValidatingCoupon,
77
+ validateAndApplyCoupon,
78
+ removeCoupon,
79
+ calculateCouponDiscount,
80
+ };
81
+
82
+ return (
83
+ <DiscountContext.Provider value={value}>
84
+ {children}
85
+ </DiscountContext.Provider>
86
+ );
87
+ }
88
+
89
+ export function useDiscountsContext() {
90
+ const context = useContext(DiscountContext);
91
+ if (!context) {
92
+ throw new Error('useDiscountsContext must be used within DiscountProvider');
93
+ }
94
+ return context;
95
+ }
96
+
97
+ export type { DiscountContextType };
@@ -7,10 +7,13 @@ import { AuthProvider } from './AuthProvider';
7
7
  import { CartProvider } from './CartProvider';
8
8
  import { WishlistProvider } from './WishlistProvider';
9
9
  import { BasePathProvider } from './BasePathProvider';
10
+ import { DiscountProvider } from './DiscountProvider';
10
11
  import { initializeApiAdapter } from '@/lib/api-adapter';
11
12
  import { QueryClientProvider } from '@tanstack/react-query';
12
13
  import { QueryClient } from '@tanstack/react-query';
13
14
  import { NotificationProvider } from './NotificationProvider';
15
+ import { NotificationCenterProvider } from './NotificationCenterProvider';
16
+ import { NotificationDrawer } from '@/components/NotificationDrawer';
14
17
 
15
18
  interface EcommerceProviderProps {
16
19
  config: EcommerceConfig;
@@ -35,11 +38,16 @@ export function EcommerceProvider({ config, children, withToaster = true, basePa
35
38
  <BasePathProvider basePath={basePath}>
36
39
  <AuthProvider>
37
40
  <NotificationProvider>
38
- <CartProvider>
39
- <WishlistProvider>
40
- {children}
41
- </WishlistProvider>
42
- </CartProvider>
41
+ <NotificationCenterProvider>
42
+ <CartProvider>
43
+ <DiscountProvider>
44
+ <WishlistProvider>
45
+ {children}
46
+ <NotificationDrawer />
47
+ </WishlistProvider>
48
+ </DiscountProvider>
49
+ </CartProvider>
50
+ </NotificationCenterProvider>
43
51
  </NotificationProvider>
44
52
  </AuthProvider>
45
53
  </BasePathProvider>
@@ -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;