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.
- package/dist/index.d.mts +10957 -1331
- package/dist/index.d.ts +10957 -1331
- package/dist/index.js +12364 -5144
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9353 -2205
- 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/hooks/useStoreCapabilities.ts +87 -0
- package/src/index.ts +29 -0
- package/src/lib/Apis/apis/auth-api.ts +19 -7
- package/src/lib/Apis/apis/categories-api.ts +97 -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 +181 -0
- package/src/lib/Apis/apis/review-api.ts +283 -4
- package/src/lib/Apis/apis/shipping-api.ts +105 -0
- package/src/lib/Apis/apis/stores-api.ts +536 -0
- package/src/lib/Apis/apis/sub-categories-api.ts +97 -0
- package/src/lib/Apis/apis/users-api.ts +8 -8
- package/src/lib/Apis/models/address-created-request.ts +0 -12
- package/src/lib/Apis/models/address.ts +0 -12
- package/src/lib/Apis/models/api-key-info-dto.ts +49 -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-address-dto.ts +0 -12
- package/src/lib/Apis/models/create-discount-dto.ts +31 -100
- 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-address-dto.ts +0 -12
- package/src/lib/Apis/models/create-store-dto-settings.ts +51 -0
- package/src/lib/Apis/models/create-store-dto.ts +13 -0
- package/src/lib/Apis/models/create-variant-dto.ts +0 -6
- package/src/lib/Apis/models/discount.ts +37 -106
- package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
- package/src/lib/Apis/models/index.ts +24 -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 -109
- 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/product-variant.ts +0 -6
- package/src/lib/Apis/models/reorder-categories-dto.ts +27 -0
- package/src/lib/Apis/models/reorder-products-dto.ts +49 -0
- package/src/lib/Apis/models/{check-notifications-response-dto.ts → reorder-products-success-response-dto.ts} +7 -7
- package/src/lib/Apis/models/reorder-subcategories-dto.ts +33 -0
- package/src/lib/Apis/models/reorder-success-response-dto.ts +33 -0
- package/src/lib/Apis/models/review-status-dto.ts +34 -0
- 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/shipment-with-order.ts +18 -0
- package/src/lib/Apis/models/shipment.ts +18 -0
- package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
- package/src/lib/Apis/models/store-api-keys-response-dto.ts +34 -0
- package/src/lib/Apis/models/store-capabilities-dto.ts +63 -0
- package/src/lib/Apis/models/store-entity.ts +13 -0
- package/src/lib/Apis/models/store.ts +13 -0
- package/src/lib/Apis/models/update-address-dto.ts +0 -12
- package/src/lib/Apis/models/update-api-keys-dto.ts +39 -0
- package/src/lib/Apis/models/update-discount-dto.ts +31 -100
- package/src/lib/Apis/models/update-manual-shipment-status-dto.ts +47 -0
- 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 +13 -0
- package/src/lib/Apis/models/update-variant-dto.ts +0 -6
- 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 +402 -290
- 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/screens/ResetPasswordScreen.tsx +10 -4
- 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,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
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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;
|