@evenicanpm/storefront-core 2.1.0 → 2.3.0
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/package.json +9 -4
- package/src/api-manager/README.md +52 -27
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
- package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +96 -22
- package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +14 -5
- package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
- package/src/api-manager/datasources/e4/middleware.ts +35 -0
- package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
- package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
- package/src/api-manager/index.ts +54 -3
- package/src/api-manager/lib/get-graphql-client.ts +13 -6
- package/src/api-manager/schemas/cart.schema.ts +10 -1
- package/src/api-manager/schemas/invoice.schema.ts +55 -0
- package/src/api-manager/schemas/order.schema.ts +13 -1
- package/src/api-manager/schemas/user.schema.ts +8 -0
- package/src/api-manager/services/create-query.ts +5 -56
- package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
- package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
- package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
- package/src/api-manager/services/order/queries/get-orders.ts +6 -3
- package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
- package/src/api-manager/types/Datasource.ts +21 -2
- package/src/auth/better-auth.ts +89 -0
- package/src/auth/signout.ts +5 -34
- package/src/cms/blocks/block-manager.tsx +18 -3
- package/src/cms/blocks/components/cta-banner.tsx +65 -0
- package/src/cms/blocks/components/hero-image.tsx +83 -0
- package/src/cms/blocks/index.tsx +2 -0
- package/src/cms/blocks/interfaces.ts +20 -0
- package/src/cms/draft-mode-badge.tsx +26 -0
- package/src/cms/queries.ts +81 -0
- package/src/components/flex-box/flex-box.tsx +2 -0
- package/src/components/header/__tests__/header.test.tsx +9 -2
- package/src/components/header/__tests__/user.test.tsx +34 -19
- package/src/components/header/components/user.tsx +70 -29
- package/src/components/mini-cart/mini-cart.tsx +3 -3
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +3 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +4 -1
- package/src/hooks/use-nextauth-session.ts +10 -14
- package/src/pages/account/account-navigation.tsx +88 -60
- package/src/pages/account/account-routes.ts +52 -0
- package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
- package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
- package/src/pages/account/orders/order-history-header.tsx +54 -0
- package/src/pages/account/orders/order-history-pagination.tsx +55 -0
- package/src/pages/account/orders/order-history-root.tsx +125 -0
- package/src/pages/account/orders/order-history-row.tsx +110 -0
- package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
- package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
- package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
- package/src/pages/account/orders/order-history-sort.tsx +90 -0
- package/src/pages/account/orders/order-history-table.tsx +54 -0
- package/src/pages/account/orders/order-row.tsx +2 -2
- package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
- package/src/pages/account/table-header.tsx +20 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +1 -1
- package/src/pages/blog/blog-card.tsx +9 -3
- package/src/pages/blog/blog-detail-view.tsx +150 -0
- package/src/pages/blog/blog-list-view.tsx +59 -0
- package/src/pages/cart/__tests__/checkout-form.test.tsx +4 -2
- package/src/pages/cart/estimate-shipping.tsx +6 -5
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
- package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
- package/src/pages/cms-page-view.tsx +53 -0
- package/src/pages/quickorder/order-upload.tsx +12 -6
- package/src/pages/quickorder/quick-order.tsx +25 -20
- package/src/api-manager/datasources/d365/utils/decode-jwt.ts +0 -10
- package/src/auth/auth-options.test.ts +0 -70
- package/src/auth/auth-options.ts +0 -101
- package/src/auth/next-auth-cookie-manager.ts +0 -87
- package/src/auth/providers/aadb2c-provider.ts +0 -32
- package/src/auth/providers/authentik-provider.ts +0 -24
- package/src/auth/providers/entra-external-id-provider.ts +0 -50
- package/src/auth/providers/index.ts +0 -16
- package/src/auth/providers/keycloak-provider.ts +0 -22
- package/src/auth/refresh-token.ts +0 -132
- package/src/auth/types/next-auth.d.ts +0 -38
|
@@ -4,6 +4,14 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
4
4
|
import { NextIntlClientProvider } from "next-intl";
|
|
5
5
|
import { vi } from "vitest";
|
|
6
6
|
|
|
7
|
+
const { mockMutate, mockHandleRedirectPromise, mockUseSession } = vi.hoisted(
|
|
8
|
+
() => ({
|
|
9
|
+
mockMutate: vi.fn().mockResolvedValue({}),
|
|
10
|
+
mockHandleRedirectPromise: vi.fn().mockResolvedValue(null),
|
|
11
|
+
mockUseSession: vi.fn(),
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
|
|
7
15
|
vi.mock(
|
|
8
16
|
"@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-init",
|
|
9
17
|
() => ({
|
|
@@ -11,10 +19,6 @@ vi.mock(
|
|
|
11
19
|
}),
|
|
12
20
|
);
|
|
13
21
|
|
|
14
|
-
const mockMutate = vi.fn().mockResolvedValue({});
|
|
15
|
-
const mockHandleRedirectPromise = vi.fn().mockResolvedValue(null);
|
|
16
|
-
const mockUseSession = vi.fn();
|
|
17
|
-
|
|
18
22
|
vi.mock(
|
|
19
23
|
"@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user",
|
|
20
24
|
() => ({
|
|
@@ -36,9 +40,17 @@ vi.mock("next/navigation", () => ({
|
|
|
36
40
|
usePathname: () => "/mock-path",
|
|
37
41
|
}));
|
|
38
42
|
|
|
39
|
-
vi.
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
vi.mock("better-auth/react", () => {
|
|
44
|
+
return {
|
|
45
|
+
createAuthClient: () => ({
|
|
46
|
+
useSession: mockUseSession,
|
|
47
|
+
signIn: { oauth2: vi.fn() },
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
vi.mock("better-auth/client/plugins", () => ({
|
|
53
|
+
genericOAuthClient: () => ({}),
|
|
42
54
|
}));
|
|
43
55
|
|
|
44
56
|
vi.mock("@evenicanpm/storefront-core/src/auth/msal", () => ({
|
|
@@ -58,7 +70,7 @@ describe.skip("User Component", async () => {
|
|
|
58
70
|
it("renders login button when user is not authenticated", () => {
|
|
59
71
|
mockUseSession.mockReturnValue({
|
|
60
72
|
data: null,
|
|
61
|
-
|
|
73
|
+
isPending: false,
|
|
62
74
|
});
|
|
63
75
|
render(
|
|
64
76
|
<NextIntlClientProvider locale="en" messages={messages}>
|
|
@@ -75,9 +87,9 @@ describe.skip("User Component", async () => {
|
|
|
75
87
|
mockUseSession.mockReturnValue({
|
|
76
88
|
data: {
|
|
77
89
|
user: { email: "test@example.com" },
|
|
78
|
-
accessToken: "mockAccessToken",
|
|
90
|
+
session: { accessToken: "mockAccessToken" },
|
|
79
91
|
},
|
|
80
|
-
|
|
92
|
+
isPending: false,
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
render(
|
|
@@ -94,10 +106,10 @@ describe.skip("User Component", async () => {
|
|
|
94
106
|
expect(userMenuButton).toBeInTheDocument();
|
|
95
107
|
});
|
|
96
108
|
|
|
97
|
-
it("calls signOut when session
|
|
109
|
+
it("calls signOut when session is null after pending", () => {
|
|
98
110
|
mockUseSession.mockReturnValue({
|
|
99
|
-
data:
|
|
100
|
-
|
|
111
|
+
data: null,
|
|
112
|
+
isPending: false,
|
|
101
113
|
});
|
|
102
114
|
|
|
103
115
|
render(
|
|
@@ -113,8 +125,11 @@ describe.skip("User Component", async () => {
|
|
|
113
125
|
const mockSignOut = vi.mocked(signOut);
|
|
114
126
|
|
|
115
127
|
mockUseSession.mockReturnValue({
|
|
116
|
-
data: {
|
|
117
|
-
|
|
128
|
+
data: {
|
|
129
|
+
user: { email: "test@example.com" },
|
|
130
|
+
session: { accessToken: "token" },
|
|
131
|
+
},
|
|
132
|
+
isPending: false,
|
|
118
133
|
});
|
|
119
134
|
|
|
120
135
|
render(
|
|
@@ -139,9 +154,9 @@ describe.skip("User Component", async () => {
|
|
|
139
154
|
mockUseSession.mockReturnValue({
|
|
140
155
|
data: {
|
|
141
156
|
user: { email: "test@example.com" },
|
|
142
|
-
accessToken: "mockAccessToken",
|
|
157
|
+
session: { accessToken: "mockAccessToken" },
|
|
143
158
|
},
|
|
144
|
-
|
|
159
|
+
isPending: false,
|
|
145
160
|
});
|
|
146
161
|
|
|
147
162
|
vi.mocked(mockHandleRedirectPromise).mockResolvedValue({
|
|
@@ -193,9 +208,9 @@ describe.skip("User Component", async () => {
|
|
|
193
208
|
mockUseSession.mockReturnValue({
|
|
194
209
|
data: {
|
|
195
210
|
user: { email: "test@example.com" },
|
|
196
|
-
accessToken: "mockAccessToken",
|
|
211
|
+
session: { accessToken: "mockAccessToken" },
|
|
197
212
|
},
|
|
198
|
-
|
|
213
|
+
isPending: false,
|
|
199
214
|
});
|
|
200
215
|
|
|
201
216
|
vi.mocked(mockHandleRedirectPromise).mockRejectedValue(
|
|
@@ -4,6 +4,7 @@ import useSessionInit from "@evenicanpm/storefront-core/src/api-manager/services
|
|
|
4
4
|
import useSessionLogout from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-logout";
|
|
5
5
|
// API hooks
|
|
6
6
|
import useUpdateUser from "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user";
|
|
7
|
+
import getUser from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-user";
|
|
7
8
|
import {
|
|
8
9
|
getMsalInstance,
|
|
9
10
|
initializeMsal,
|
|
@@ -12,6 +13,11 @@ import signOutCustom from "@evenicanpm/storefront-core/src/auth/signout";
|
|
|
12
13
|
import MobileUserIcon from "@evenicanpm/storefront-core/src/components/icons/user";
|
|
13
14
|
import { NavLink } from "@evenicanpm/storefront-core/src/components/nav-link";
|
|
14
15
|
import T from "@evenicanpm/storefront-core/src/components/T";
|
|
16
|
+
import {
|
|
17
|
+
type AccountRoute,
|
|
18
|
+
type AccountRouteOverrides,
|
|
19
|
+
createAccountRoutes,
|
|
20
|
+
} from "@evenicanpm/storefront-core/src/pages/account/account-routes";
|
|
15
21
|
// UI Components
|
|
16
22
|
import {
|
|
17
23
|
Box,
|
|
@@ -21,9 +27,16 @@ import {
|
|
|
21
27
|
ListItemButton,
|
|
22
28
|
Popover,
|
|
23
29
|
} from "@mui/material";
|
|
24
|
-
import {
|
|
30
|
+
import { genericOAuthClient } from "better-auth/client/plugins";
|
|
25
31
|
// Auth components
|
|
26
|
-
import {
|
|
32
|
+
import { createAuthClient } from "better-auth/react";
|
|
33
|
+
import { useRouter } from "next/navigation";
|
|
34
|
+
|
|
35
|
+
const authClient = createAuthClient({
|
|
36
|
+
plugins: [genericOAuthClient()],
|
|
37
|
+
});
|
|
38
|
+
const { useSession: useBetterAuthSession } = authClient;
|
|
39
|
+
|
|
27
40
|
import type React from "react";
|
|
28
41
|
import {
|
|
29
42
|
createContext,
|
|
@@ -40,19 +53,32 @@ interface IdTokenClaims {
|
|
|
40
53
|
family_name?: string;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
const routeToMenuItem = (route: AccountRoute) => ({
|
|
57
|
+
href: route.href,
|
|
58
|
+
label: `Account.Menu.${route.titleKey}`,
|
|
59
|
+
Icon: route.Icon,
|
|
60
|
+
count: route.count,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const buildMenuItems = (overrides?: AccountRouteOverrides) => {
|
|
64
|
+
const routes = createAccountRoutes(overrides);
|
|
65
|
+
return [
|
|
66
|
+
routeToMenuItem(routes.profile),
|
|
67
|
+
routeToMenuItem(routes.orders),
|
|
68
|
+
routeToMenuItem(routes.wishlists),
|
|
69
|
+
routeToMenuItem(routes.invoices),
|
|
70
|
+
routeToMenuItem(routes.addresses),
|
|
71
|
+
];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type MenuItem = { href: string; label: string };
|
|
49
75
|
|
|
50
76
|
interface UserContextValue {
|
|
51
77
|
isAuthenticated: boolean;
|
|
52
78
|
status: string;
|
|
53
|
-
menuItems:
|
|
79
|
+
menuItems: MenuItem[];
|
|
54
80
|
handleLogout: () => Promise<void>;
|
|
55
|
-
|
|
81
|
+
handleSignIn: () => void;
|
|
56
82
|
signOut: () => Promise<void>;
|
|
57
83
|
anchorEl: HTMLButtonElement | null;
|
|
58
84
|
setAnchorEl: (el: HTMLButtonElement | null) => void;
|
|
@@ -63,30 +89,36 @@ const UserContext = createContext<UserContextValue | undefined>(undefined);
|
|
|
63
89
|
|
|
64
90
|
interface Props {
|
|
65
91
|
children?: ReactNode;
|
|
92
|
+
routeOverrides?: AccountRouteOverrides;
|
|
66
93
|
}
|
|
67
94
|
|
|
68
|
-
const User = ({ children }: Props) => {
|
|
69
|
-
const { data: session,
|
|
95
|
+
const User = ({ children, routeOverrides }: Props) => {
|
|
96
|
+
const { data: session, isPending } = useBetterAuthSession();
|
|
97
|
+
const status = isPending
|
|
98
|
+
? "loading"
|
|
99
|
+
: session
|
|
100
|
+
? "authenticated"
|
|
101
|
+
: "unauthenticated";
|
|
70
102
|
|
|
71
|
-
if (session
|
|
72
|
-
//
|
|
73
|
-
console.warn("Auth Error", session?.error);
|
|
74
|
-
signOutCustom();
|
|
103
|
+
if (session === null && !isPending) {
|
|
104
|
+
// No error handling needed - session is simply null when not authenticated
|
|
75
105
|
}
|
|
76
106
|
|
|
77
107
|
const isAuthenticated = !!session;
|
|
78
108
|
const { mutateAsync: sessionInit } = useSessionInit();
|
|
79
109
|
const { mutateAsync: updateUser } = useUpdateUser();
|
|
80
110
|
const { mutateAsync: sessionLogout } = useSessionLogout();
|
|
111
|
+
// Register the "me" query on the client so the hydrated cache entry has a queryFn for refetches
|
|
112
|
+
getUser.useData();
|
|
81
113
|
|
|
82
114
|
// session init
|
|
83
115
|
useEffect(() => {
|
|
84
|
-
if (isAuthenticated) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
if (isAuthenticated && session?.user?.email) {
|
|
117
|
+
const accessToken =
|
|
118
|
+
(session.session as { accessToken?: string })?.accessToken || "";
|
|
119
|
+
sessionInit({ accessToken, email: session.user.email }).catch(
|
|
120
|
+
console.error,
|
|
121
|
+
);
|
|
90
122
|
}
|
|
91
123
|
}, [isAuthenticated, session, sessionInit]);
|
|
92
124
|
|
|
@@ -124,15 +156,24 @@ const User = ({ children }: Props) => {
|
|
|
124
156
|
}
|
|
125
157
|
};
|
|
126
158
|
|
|
159
|
+
const handleSignIn = () => {
|
|
160
|
+
authClient.signIn.oauth2({
|
|
161
|
+
providerId: "entra-external-id",
|
|
162
|
+
callbackURL: "/",
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
127
166
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
|
128
167
|
const open = Boolean(anchorEl);
|
|
129
168
|
|
|
169
|
+
const menuItems = buildMenuItems(routeOverrides);
|
|
170
|
+
|
|
130
171
|
const value: UserContextValue = {
|
|
131
172
|
isAuthenticated,
|
|
132
173
|
status,
|
|
133
|
-
menuItems
|
|
174
|
+
menuItems,
|
|
134
175
|
handleLogout,
|
|
135
|
-
|
|
176
|
+
handleSignIn,
|
|
136
177
|
signOut: signOutCustom,
|
|
137
178
|
anchorEl,
|
|
138
179
|
setAnchorEl,
|
|
@@ -149,13 +190,13 @@ User.MobileTrigger = function MobileTrigger() {
|
|
|
149
190
|
const ctx = useContext(UserContext);
|
|
150
191
|
const router = useRouter();
|
|
151
192
|
if (!ctx) return null;
|
|
152
|
-
const { isAuthenticated,
|
|
193
|
+
const { isAuthenticated, handleSignIn } = ctx;
|
|
153
194
|
return (
|
|
154
195
|
<IconButton
|
|
155
196
|
aria-label="User Menu"
|
|
156
197
|
onClick={() => {
|
|
157
198
|
if (isAuthenticated) router.push("/account/profile");
|
|
158
|
-
else
|
|
199
|
+
else handleSignIn();
|
|
159
200
|
}}
|
|
160
201
|
>
|
|
161
202
|
<MobileUserIcon style={{ fontSize: "1.25rem" }} />
|
|
@@ -182,7 +223,7 @@ User.DesktopTrigger = function DesktopTrigger() {
|
|
|
182
223
|
User.Menu = function Menu({
|
|
183
224
|
children,
|
|
184
225
|
}: {
|
|
185
|
-
children?: (items:
|
|
226
|
+
children?: (items: MenuItem[]) => React.ReactNode;
|
|
186
227
|
}) {
|
|
187
228
|
const ctx = useContext(UserContext);
|
|
188
229
|
if (!ctx) return null;
|
|
@@ -239,13 +280,13 @@ User.MenuItem = function MenuItem({
|
|
|
239
280
|
User.LoginButton = function LoginButton() {
|
|
240
281
|
const ctx = useContext(UserContext);
|
|
241
282
|
if (!ctx) return null;
|
|
242
|
-
const {
|
|
283
|
+
const { handleSignIn } = ctx;
|
|
243
284
|
return (
|
|
244
285
|
<Button
|
|
245
286
|
size="small"
|
|
246
287
|
variant="contained"
|
|
247
288
|
color="primary"
|
|
248
|
-
onClick={() =>
|
|
289
|
+
onClick={() => handleSignIn()}
|
|
249
290
|
>
|
|
250
291
|
<T path="Account.Menu.btnLogin" />
|
|
251
292
|
</Button>
|
|
@@ -121,8 +121,8 @@ MiniCart.Items = () => {
|
|
|
121
121
|
} = ctx;
|
|
122
122
|
|
|
123
123
|
return (
|
|
124
|
-
<Box height={`calc(100vh - ${cart?.CartLines
|
|
125
|
-
{cart
|
|
124
|
+
<Box height={`calc(100vh - ${cart?.CartLines?.length ? "207px" : "75px"})`}>
|
|
125
|
+
{cart?.CartLines?.length > 0 ? (
|
|
126
126
|
<Scrollbar>
|
|
127
127
|
{cart?.CartLines?.map((item) => (
|
|
128
128
|
<MiniCartItem
|
|
@@ -153,7 +153,7 @@ MiniCart.Actions = () => {
|
|
|
153
153
|
const ctx = useContext(MiniCartContext);
|
|
154
154
|
if (!ctx) return null;
|
|
155
155
|
const { cart, currency, handleNavigate } = ctx;
|
|
156
|
-
return cart?.CartLines
|
|
156
|
+
return cart?.CartLines?.length > 0 ? (
|
|
157
157
|
<BottomActions
|
|
158
158
|
total={currency(cart?.TotalAmount || 0)}
|
|
159
159
|
handleNavigate={handleNavigate}
|
|
@@ -8,9 +8,11 @@ import { AddToWishListDialogContext } from "@evenicanpm/storefront-core/src/comp
|
|
|
8
8
|
// LOCAL COMPONENTS
|
|
9
9
|
import DialogHeader from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header";
|
|
10
10
|
import { Dialog, DialogContent } from "@mui/material";
|
|
11
|
-
import {
|
|
11
|
+
import { createAuthClient } from "better-auth/react";
|
|
12
12
|
import type React from "react";
|
|
13
13
|
|
|
14
|
+
const { useSession } = createAuthClient();
|
|
15
|
+
|
|
14
16
|
export interface AddToWishlistDialogProps {
|
|
15
17
|
open: boolean;
|
|
16
18
|
handleClose: () => void;
|
|
@@ -9,8 +9,11 @@ import { H2 } from "@evenicanpm/storefront-core/src/components/Typography";
|
|
|
9
9
|
import CreateWishlistButton from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/create-wishlist/create-wishlist-button";
|
|
10
10
|
import { IconButton, styled, type Theme } from "@mui/material";
|
|
11
11
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
12
|
-
import {
|
|
12
|
+
import { createAuthClient } from "better-auth/react";
|
|
13
13
|
import { useTranslations } from "next-intl";
|
|
14
|
+
|
|
15
|
+
const { useSession } = createAuthClient();
|
|
16
|
+
|
|
14
17
|
// ICONS
|
|
15
18
|
import { MdFavorite } from "react-icons/md";
|
|
16
19
|
|
|
@@ -1,36 +1,31 @@
|
|
|
1
1
|
// hooks/useAppSession.ts
|
|
2
2
|
|
|
3
3
|
import useSessionInit from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-init";
|
|
4
|
-
import useSessionLogout from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-logout";
|
|
5
4
|
// API hooks
|
|
6
5
|
import useUpdateUser from "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user";
|
|
7
6
|
import {
|
|
8
7
|
getMsalInstance,
|
|
9
8
|
initializeMsal,
|
|
10
9
|
} from "@evenicanpm/storefront-core/src/auth/msal";
|
|
11
|
-
import {
|
|
10
|
+
import { createAuthClient } from "better-auth/react";
|
|
12
11
|
import { useEffect } from "react";
|
|
13
12
|
|
|
13
|
+
const authClient = createAuthClient();
|
|
14
|
+
const { useSession: useBetterAuthSession } = authClient;
|
|
15
|
+
|
|
14
16
|
export function useAppSession() {
|
|
15
|
-
const { data: session } =
|
|
17
|
+
const { data: session, isPending } = useBetterAuthSession();
|
|
16
18
|
const { mutateAsync: sessionInit } = useSessionInit();
|
|
17
19
|
const { mutateAsync: updateUser } = useUpdateUser();
|
|
18
|
-
const { mutateAsync: sessionLogout } = useSessionLogout();
|
|
19
|
-
|
|
20
|
-
// Handle custom auth errors
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (session?.error) {
|
|
23
|
-
console.warn("Auth Error", session.error);
|
|
24
|
-
sessionLogout({}); // Or your custom signOut
|
|
25
|
-
}
|
|
26
|
-
}, [session, sessionLogout]);
|
|
27
20
|
|
|
28
21
|
// Initialize session in API
|
|
29
22
|
useEffect(() => {
|
|
30
23
|
if (!session) return;
|
|
31
24
|
|
|
32
|
-
const {
|
|
33
|
-
|
|
25
|
+
const { user } = session;
|
|
26
|
+
const accessToken =
|
|
27
|
+
(session.session as { accessToken?: string })?.accessToken || "";
|
|
28
|
+
if (user?.email) {
|
|
34
29
|
sessionInit({ accessToken, email: user.email }).catch(console.error);
|
|
35
30
|
}
|
|
36
31
|
}, [session, sessionInit]);
|
|
@@ -64,5 +59,6 @@ export function useAppSession() {
|
|
|
64
59
|
return {
|
|
65
60
|
session,
|
|
66
61
|
isAuthenticated: !!session,
|
|
62
|
+
isPending,
|
|
67
63
|
};
|
|
68
64
|
}
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
Paragraph,
|
|
10
10
|
Span,
|
|
11
11
|
} from "@evenicanpm/storefront-core/src/components/Typography";
|
|
12
|
+
import {
|
|
13
|
+
type AccountRoute,
|
|
14
|
+
type AccountRouteOverrides,
|
|
15
|
+
createAccountRoutes,
|
|
16
|
+
} from "@evenicanpm/storefront-core/src/pages/account/account-routes";
|
|
12
17
|
// STYLED COMPONENTS
|
|
13
18
|
import {
|
|
14
19
|
MainContainer,
|
|
@@ -17,88 +22,114 @@ import {
|
|
|
17
22
|
import { usePathname } from "next/navigation";
|
|
18
23
|
import { useTranslations } from "next-intl";
|
|
19
24
|
import { createContext, Fragment, type ReactNode } from "react";
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
MdFavoriteBorder as FavoriteBorder,
|
|
23
|
-
MdPerson as Person,
|
|
24
|
-
MdPlace as Place,
|
|
25
|
-
MdOutlineShoppingBag as ShoppingBagOutlined,
|
|
26
|
-
} from "react-icons/md";
|
|
25
|
+
import type { IconType } from "react-icons";
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
*
|
|
30
29
|
*** Extend menu:
|
|
31
30
|
<AccountNavigation
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
items={[
|
|
32
|
+
...DEFAULT_MENU_ITEMS,
|
|
33
|
+
{
|
|
34
|
+
title: "EXTRAS",
|
|
35
|
+
list: [
|
|
36
|
+
{
|
|
37
|
+
href: "/account/rewards",
|
|
38
|
+
title: "rewards",
|
|
39
|
+
Icon: RewardsIcon,
|
|
40
|
+
count: 5,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
]}
|
|
46
45
|
/>;
|
|
47
46
|
|
|
48
47
|
*** Override completely:
|
|
49
48
|
<AccountNavigation>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
<AccountNavigation.Section title="Custom Section">
|
|
50
|
+
<AccountNavigation.Item
|
|
51
|
+
href="/custom"
|
|
52
|
+
Icon={CustomIcon}
|
|
53
|
+
label="Custom Page"
|
|
54
|
+
/>
|
|
55
|
+
</AccountNavigation.Section>
|
|
57
56
|
</AccountNavigation>;
|
|
58
57
|
*/
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
interface NavItem {
|
|
60
|
+
href: string;
|
|
61
|
+
title: string;
|
|
62
|
+
Icon: IconType;
|
|
63
|
+
count?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface NavSection {
|
|
67
|
+
title: string;
|
|
68
|
+
list: NavItem[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* @param route
|
|
74
|
+
* @returns
|
|
75
|
+
*/
|
|
76
|
+
const routeToNavItem = (route: AccountRoute): NavItem => {
|
|
77
|
+
return {
|
|
78
|
+
href: route.href,
|
|
79
|
+
title: route.titleKey,
|
|
80
|
+
Icon: route.Icon,
|
|
81
|
+
count: route.count,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const DEFAULT_MENU_ITEMS = (
|
|
86
|
+
overrides?: AccountRouteOverrides,
|
|
87
|
+
): NavSection[] => {
|
|
88
|
+
const routes = createAccountRoutes(overrides);
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
title: "DASHBOARD",
|
|
92
|
+
list: [
|
|
93
|
+
routeToNavItem(routes.orders),
|
|
94
|
+
routeToNavItem(routes.wishlists),
|
|
95
|
+
routeToNavItem(routes.invoices),
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
title: "ACCOUNT SETTINGS",
|
|
100
|
+
list: [routeToNavItem(routes.profile), routeToNavItem(routes.addresses)],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
};
|
|
81
104
|
|
|
82
105
|
interface AccountNavigationContextValue {
|
|
83
|
-
items:
|
|
106
|
+
items: NavSection[];
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
const AccountNavigationContext =
|
|
87
110
|
createContext<AccountNavigationContextValue | null>(null);
|
|
111
|
+
|
|
88
112
|
interface AccountNavigationProps {
|
|
89
|
-
items?:
|
|
113
|
+
items?: NavSection[];
|
|
90
114
|
children?: ReactNode;
|
|
115
|
+
routeOverrides?: AccountRouteOverrides;
|
|
91
116
|
}
|
|
92
117
|
|
|
93
|
-
const AccountNavigation = ({
|
|
118
|
+
const AccountNavigation = ({
|
|
119
|
+
items,
|
|
120
|
+
children,
|
|
121
|
+
routeOverrides,
|
|
122
|
+
}: AccountNavigationProps) => {
|
|
94
123
|
const pathname = usePathname();
|
|
95
124
|
const t = useTranslations("Account.Menu");
|
|
96
125
|
|
|
97
126
|
const { data: user } = getUser.useData();
|
|
98
127
|
const { data: counts } = getUserCounts.useData(String(user?.RecordId) || "");
|
|
99
128
|
|
|
100
|
-
|
|
101
|
-
|
|
129
|
+
const defaultItems = DEFAULT_MENU_ITEMS(routeOverrides);
|
|
130
|
+
|
|
131
|
+
// Render either the props items or the default items
|
|
132
|
+
const itemsWCounts = (items || defaultItems).map((section) => ({
|
|
102
133
|
...section,
|
|
103
134
|
list: section.list.map((item) => {
|
|
104
135
|
if (item.href === "/account/orders") {
|
|
@@ -112,10 +143,10 @@ const AccountNavigation = ({ items, children }: AccountNavigationProps) => {
|
|
|
112
143
|
}));
|
|
113
144
|
|
|
114
145
|
return (
|
|
115
|
-
<AccountNavigationContext.Provider value={{ items:
|
|
146
|
+
<AccountNavigationContext.Provider value={{ items: itemsWCounts }}>
|
|
116
147
|
<MainContainer>
|
|
117
148
|
{children ||
|
|
118
|
-
|
|
149
|
+
itemsWCounts.map((section) => (
|
|
119
150
|
<AccountNavigation.Section
|
|
120
151
|
key={section.title}
|
|
121
152
|
title={section.title}
|
|
@@ -151,10 +182,7 @@ AccountNavigation.Section = ({ title, children }: SectionProps) => {
|
|
|
151
182
|
);
|
|
152
183
|
};
|
|
153
184
|
|
|
154
|
-
interface ItemProps {
|
|
155
|
-
href: string;
|
|
156
|
-
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
157
|
-
count?: number;
|
|
185
|
+
interface ItemProps extends Pick<NavItem, "href" | "Icon" | "count"> {
|
|
158
186
|
label: string;
|
|
159
187
|
isActive?: boolean;
|
|
160
188
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { IconType } from "react-icons";
|
|
2
|
+
import {
|
|
3
|
+
MdFavoriteBorder as FavoriteBorder,
|
|
4
|
+
MdDocumentScanner as InvoicesIcon,
|
|
5
|
+
MdPerson as Person,
|
|
6
|
+
MdPlace as Place,
|
|
7
|
+
MdOutlineShoppingBag as ShoppingBagOutlined,
|
|
8
|
+
} from "react-icons/md";
|
|
9
|
+
|
|
10
|
+
export interface AccountRoute {
|
|
11
|
+
href: string;
|
|
12
|
+
titleKey: string;
|
|
13
|
+
Icon: IconType;
|
|
14
|
+
count?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AccountRouteOverrides {
|
|
18
|
+
wishlists?: { href?: string; titleKey?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaults = {
|
|
22
|
+
orders: {
|
|
23
|
+
href: "/account/orders",
|
|
24
|
+
titleKey: "orders",
|
|
25
|
+
Icon: ShoppingBagOutlined,
|
|
26
|
+
count: 0,
|
|
27
|
+
},
|
|
28
|
+
wishlists: {
|
|
29
|
+
href: "/account/wish-lists",
|
|
30
|
+
titleKey: "wishlists",
|
|
31
|
+
Icon: FavoriteBorder,
|
|
32
|
+
},
|
|
33
|
+
invoices: {
|
|
34
|
+
href: "/account/invoices",
|
|
35
|
+
titleKey: "invoices",
|
|
36
|
+
Icon: InvoicesIcon,
|
|
37
|
+
},
|
|
38
|
+
profile: { href: "/account/profile", titleKey: "profile", Icon: Person },
|
|
39
|
+
addresses: {
|
|
40
|
+
href: "/account/addresses",
|
|
41
|
+
titleKey: "addresses",
|
|
42
|
+
Icon: Place,
|
|
43
|
+
count: 0,
|
|
44
|
+
},
|
|
45
|
+
} as const satisfies Record<string, AccountRoute>;
|
|
46
|
+
|
|
47
|
+
export type AccountRouteKey = keyof typeof defaults;
|
|
48
|
+
|
|
49
|
+
export const createAccountRoutes = (overrides?: AccountRouteOverrides) => ({
|
|
50
|
+
...defaults,
|
|
51
|
+
wishlists: { ...defaults.wishlists, ...overrides?.wishlists },
|
|
52
|
+
});
|