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.
- package/dist/index.d.mts +1451 -1303
- package/dist/index.d.ts +1451 -1303
- package/dist/index.js +10502 -5728
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7817 -3059
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/components/AccountReviewsTab.tsx +97 -0
- package/src/components/CouponCodeInput.tsx +190 -0
- package/src/components/Header.tsx +5 -1
- package/src/components/Notification.tsx +1 -1
- package/src/components/NotificationBell.tsx +33 -0
- package/src/components/NotificationCard.tsx +211 -0
- package/src/components/NotificationDrawer.tsx +195 -0
- package/src/components/OrderCard.tsx +164 -99
- package/src/components/ProductReviewsSection.tsx +30 -0
- package/src/components/RatingDistribution.tsx +86 -0
- package/src/components/ReviewCard.tsx +59 -0
- package/src/components/ReviewForm.tsx +207 -0
- package/src/components/ReviewPromptBanner.tsx +98 -0
- package/src/components/ReviewsList.tsx +151 -0
- package/src/components/StarRating.tsx +98 -0
- package/src/hooks/useDiscounts.ts +7 -0
- package/src/hooks/useOrders.ts +15 -0
- package/src/hooks/useReviews.ts +230 -0
- package/src/index.ts +25 -0
- package/src/lib/Apis/apis/discounts-api.ts +23 -72
- package/src/lib/Apis/apis/notifications-api.ts +196 -231
- package/src/lib/Apis/apis/products-api.ts +84 -0
- package/src/lib/Apis/apis/review-api.ts +283 -4
- package/src/lib/Apis/apis/stores-api.ts +180 -0
- package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
- package/src/lib/Apis/models/cart-body-populated.ts +3 -3
- package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
- package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
- package/src/lib/Apis/models/create-discount-dto.ts +31 -92
- package/src/lib/Apis/models/create-review-dto.ts +4 -4
- package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
- package/src/lib/Apis/models/create-store-dto.ts +6 -0
- package/src/lib/Apis/models/discount.ts +37 -98
- package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
- package/src/lib/Apis/models/index.ts +13 -7
- package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
- package/src/lib/Apis/models/manual-order-dto.ts +3 -3
- package/src/lib/Apis/models/populated-discount.ts +41 -101
- package/src/lib/Apis/models/preference-update-item.ts +59 -0
- package/src/lib/Apis/models/product-light-dto.ts +40 -0
- package/src/lib/Apis/models/{check-notifications-response-dto.ts → review-status-dto.ts} +8 -7
- package/src/lib/Apis/models/review.ts +9 -3
- package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
- package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
- package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
- package/src/lib/Apis/models/store-entity.ts +6 -0
- package/src/lib/Apis/models/store.ts +6 -0
- package/src/lib/Apis/models/update-discount-dto.ts +31 -92
- package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
- package/src/lib/Apis/models/update-review-dto.ts +4 -4
- package/src/lib/Apis/models/update-store-dto.ts +6 -0
- package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
- package/src/lib/utils/discount.ts +155 -0
- package/src/lib/validations/discount.ts +11 -0
- package/src/providers/CartProvider.tsx +2 -2
- package/src/providers/DiscountProvider.tsx +97 -0
- package/src/providers/EcommerceProvider.tsx +13 -5
- package/src/providers/NotificationCenterProvider.tsx +436 -0
- package/src/screens/CartScreen.tsx +1 -1
- package/src/screens/CheckoutScreen.tsx +39 -12
- package/src/screens/NotificationSettingsScreen.tsx +413 -0
- package/src/screens/OrderDetailScreen.tsx +283 -0
- package/src/screens/OrderReviewsScreen.tsx +308 -0
- package/src/screens/OrdersScreen.tsx +31 -7
- package/src/screens/ProductDetailScreen.tsx +24 -11
- package/src/screens/ProfileScreen.tsx +5 -0
- package/src/lib/Apis/models/create-notification-dto.ts +0 -75
- package/src/lib/Apis/models/notification.ts +0 -93
- 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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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 =
|
|
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>
|