@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RetrieveCardPaymentAcceptResultInput } from "@evenicanpm/storefront-core/src/api-manager/schemas/cart.schema";
|
|
2
2
|
import usePaymentAcceptResult from "@evenicanpm/storefront-core/src/api-manager/services/cart/mutations/get-payment-accept-result";
|
|
3
3
|
import getPaymentAcceptPoint from "@evenicanpm/storefront-core/src/api-manager/services/cart/queries/get-payment-accept-point";
|
|
4
|
+
import getCustomerBalance from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-customer-balance";
|
|
5
|
+
import getUser from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-user";
|
|
4
6
|
import { FlexBox } from "@evenicanpm/storefront-core/src/components/flex-box";
|
|
5
7
|
import {
|
|
6
8
|
H6,
|
|
@@ -12,10 +14,20 @@ import type {
|
|
|
12
14
|
CardPaymentAcceptPoint,
|
|
13
15
|
CardPaymentAcceptResult,
|
|
14
16
|
} from "@msdyn365-commerce/retail-proxy";
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Alert,
|
|
19
|
+
Box,
|
|
20
|
+
Card,
|
|
21
|
+
FormControlLabel,
|
|
22
|
+
Radio,
|
|
23
|
+
RadioGroup,
|
|
24
|
+
} from "@mui/material";
|
|
25
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
16
26
|
import { useTranslations } from "next-intl";
|
|
17
27
|
import React from "react";
|
|
18
28
|
|
|
29
|
+
export type PaymentMethod = "card" | "on-account";
|
|
30
|
+
|
|
19
31
|
enum PaymentConnectorPostMessageType {
|
|
20
32
|
CardPrefix = "msax-cc-cardprefix",
|
|
21
33
|
Error = "msax-cc-error",
|
|
@@ -41,7 +53,10 @@ type PaymentError = {
|
|
|
41
53
|
};
|
|
42
54
|
|
|
43
55
|
type PaymentDetailsCompound = React.FC<CheckoutStepComponentProps> & {
|
|
44
|
-
Preview: React.FC<{
|
|
56
|
+
Preview: React.FC<{
|
|
57
|
+
paymentAcceptResult: CardPaymentAcceptResult | null;
|
|
58
|
+
selectedPaymentMethod: PaymentMethod;
|
|
59
|
+
}>;
|
|
45
60
|
Form: React.FC<{
|
|
46
61
|
iframeReference: React.RefObject<HTMLIFrameElement>;
|
|
47
62
|
iframeHeight: number;
|
|
@@ -62,14 +77,37 @@ const PaymentDetails: PaymentDetailsCompound = (props) => {
|
|
|
62
77
|
|
|
63
78
|
const [paymentAcceptResult, setPaymentAcceptResult] =
|
|
64
79
|
React.useState<CardPaymentAcceptResult | null>(null);
|
|
80
|
+
const [selectedPaymentMethod, setSelectedPaymentMethod] =
|
|
81
|
+
React.useState<PaymentMethod>("card");
|
|
65
82
|
|
|
66
83
|
const t = useTranslations("Checkout.Payment");
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
const queryClient = useQueryClient();
|
|
85
|
+
const { data: user } = getUser.useData();
|
|
86
|
+
const { data: customerBalance } = getCustomerBalance.useData(
|
|
87
|
+
user?.AccountNumber
|
|
88
|
+
? {
|
|
89
|
+
accountNumber: user.AccountNumber,
|
|
90
|
+
invoiceAccountNumber: user.InvoiceAccount,
|
|
91
|
+
}
|
|
92
|
+
: undefined,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const availableCredit = customerBalance
|
|
96
|
+
? customerBalance.CreditLimit -
|
|
97
|
+
customerBalance.Balance -
|
|
98
|
+
customerBalance.PendingBalance
|
|
99
|
+
: 0;
|
|
100
|
+
const canPayOnAccount = availableCredit >= (cart.TotalAmount ?? 0);
|
|
101
|
+
|
|
102
|
+
const { data: paymentAcceptPointData } = getPaymentAcceptPoint.useData(
|
|
103
|
+
{
|
|
104
|
+
cartId: cart.Id,
|
|
105
|
+
totalAmount: cart.TotalAmount ?? 0,
|
|
106
|
+
origin: globalThis.window?.location?.origin,
|
|
107
|
+
isClient: !!globalThis.window,
|
|
108
|
+
},
|
|
109
|
+
{ enabled: selectedPaymentMethod === "card" },
|
|
110
|
+
);
|
|
73
111
|
|
|
74
112
|
const { mutateAsync: retrievePaymentAcceptResult } = usePaymentAcceptResult();
|
|
75
113
|
const iframeReference = React.useRef<HTMLIFrameElement>(null);
|
|
@@ -219,10 +257,29 @@ const PaymentDetails: PaymentDetailsCompound = (props) => {
|
|
|
219
257
|
|
|
220
258
|
React.useEffect(() => {
|
|
221
259
|
if (stepSubmissionState === "submitting") {
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
if (selectedPaymentMethod === "on-account") {
|
|
261
|
+
// Store on-account selection in query cache for checkout-form to read
|
|
262
|
+
queryClient.setQueryData(
|
|
263
|
+
["selectedPaymentMethod", cart.Id],
|
|
264
|
+
"on-account",
|
|
265
|
+
);
|
|
266
|
+
setStepSubmissionState("idle");
|
|
267
|
+
handleStepCompletion();
|
|
268
|
+
} else {
|
|
269
|
+
// INFO: This listens for `submitting` state, then posts submit message to iframe
|
|
270
|
+
queryClient.setQueryData(["selectedPaymentMethod", cart.Id], "card");
|
|
271
|
+
postSubmitMessage();
|
|
272
|
+
}
|
|
224
273
|
}
|
|
225
|
-
}, [
|
|
274
|
+
}, [
|
|
275
|
+
postSubmitMessage,
|
|
276
|
+
stepSubmissionState,
|
|
277
|
+
selectedPaymentMethod,
|
|
278
|
+
queryClient,
|
|
279
|
+
cart.Id,
|
|
280
|
+
setStepSubmissionState,
|
|
281
|
+
handleStepCompletion,
|
|
282
|
+
]);
|
|
226
283
|
|
|
227
284
|
React.useEffect(() => {
|
|
228
285
|
if (!paymentAcceptPointData || stepViewState !== "active") return;
|
|
@@ -246,21 +303,89 @@ const PaymentDetails: PaymentDetailsCompound = (props) => {
|
|
|
246
303
|
<>
|
|
247
304
|
{children ||
|
|
248
305
|
(stepViewState === "preview" ? (
|
|
249
|
-
<PaymentDetails.Preview
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
iframeReference={iframeReference}
|
|
253
|
-
iframeHeight={iframeHeight}
|
|
254
|
-
errorState={errorState}
|
|
255
|
-
paymentAcceptPointData={paymentAcceptPointData}
|
|
306
|
+
<PaymentDetails.Preview
|
|
307
|
+
paymentAcceptResult={paymentAcceptResult}
|
|
308
|
+
selectedPaymentMethod={selectedPaymentMethod}
|
|
256
309
|
/>
|
|
310
|
+
) : (
|
|
311
|
+
<Box sx={{ m: 2 }}>
|
|
312
|
+
{canPayOnAccount && (
|
|
313
|
+
<>
|
|
314
|
+
<H6 sx={{ mb: 2 }}>{t("selectPaymentMethod")}</H6>
|
|
315
|
+
<RadioGroup
|
|
316
|
+
value={selectedPaymentMethod}
|
|
317
|
+
onChange={(e) =>
|
|
318
|
+
setSelectedPaymentMethod(e.target.value as PaymentMethod)
|
|
319
|
+
}
|
|
320
|
+
sx={{ mb: 3 }}
|
|
321
|
+
>
|
|
322
|
+
<FormControlLabel
|
|
323
|
+
value="card"
|
|
324
|
+
control={<Radio />}
|
|
325
|
+
label={t("payWithCard")}
|
|
326
|
+
/>
|
|
327
|
+
<FormControlLabel
|
|
328
|
+
value="on-account"
|
|
329
|
+
control={<Radio />}
|
|
330
|
+
label={`${t("payOnAccount")} (${t("availableCredit")}: ${availableCredit.toFixed(2)})`}
|
|
331
|
+
/>
|
|
332
|
+
</RadioGroup>
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
{selectedPaymentMethod === "card" ? (
|
|
336
|
+
<PaymentDetails.Form
|
|
337
|
+
iframeReference={iframeReference}
|
|
338
|
+
iframeHeight={iframeHeight}
|
|
339
|
+
errorState={errorState}
|
|
340
|
+
paymentAcceptPointData={paymentAcceptPointData}
|
|
341
|
+
/>
|
|
342
|
+
) : (
|
|
343
|
+
<Card
|
|
344
|
+
sx={{
|
|
345
|
+
p: 3,
|
|
346
|
+
border: "1px solid",
|
|
347
|
+
borderColor: "success.main",
|
|
348
|
+
backgroundColor: "success.light",
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<Paragraph fontWeight="bold">{t("onAccountSummary")}</Paragraph>
|
|
352
|
+
<Paragraph>
|
|
353
|
+
{t("chargeAmount")}: {cart.TotalAmount?.toFixed(2)}
|
|
354
|
+
</Paragraph>
|
|
355
|
+
<Paragraph>
|
|
356
|
+
{t("availableCredit")}: {availableCredit.toFixed(2)}
|
|
357
|
+
</Paragraph>
|
|
358
|
+
</Card>
|
|
359
|
+
)}
|
|
360
|
+
</Box>
|
|
257
361
|
))}
|
|
258
362
|
</>
|
|
259
363
|
);
|
|
260
364
|
};
|
|
261
365
|
|
|
262
|
-
PaymentDetails.Preview = ({ paymentAcceptResult }) => {
|
|
366
|
+
PaymentDetails.Preview = ({ paymentAcceptResult, selectedPaymentMethod }) => {
|
|
263
367
|
const t = useTranslations("Checkout.Payment");
|
|
368
|
+
|
|
369
|
+
if (selectedPaymentMethod === "on-account") {
|
|
370
|
+
return (
|
|
371
|
+
<FlexBox flexDirection={"column"} sx={{ m: 2 }}>
|
|
372
|
+
<H6 sx={{ mb: 2 }}>{t("cardPaymentDetails")}</H6>
|
|
373
|
+
<Card
|
|
374
|
+
sx={{
|
|
375
|
+
padding: 2,
|
|
376
|
+
boxShadow: "none",
|
|
377
|
+
border: "1px solid",
|
|
378
|
+
position: "relative",
|
|
379
|
+
backgroundColor: "grey.100",
|
|
380
|
+
borderColor: "secondary.main",
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<Paragraph fontWeight="bold">{t("payOnAccount")}</Paragraph>
|
|
384
|
+
</Card>
|
|
385
|
+
</FlexBox>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
264
389
|
if (!paymentAcceptResult) return null;
|
|
265
390
|
|
|
266
391
|
return (
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Box, Container, Paper } from "@mui/material";
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import BlockManager from "../cms/blocks/block-manager";
|
|
5
|
+
import type { BlockManagerType } from "../cms/blocks/interfaces";
|
|
6
|
+
import { DraftModeBadge } from "../cms/draft-mode-badge";
|
|
7
|
+
import { cmsQueryOptions } from "../cms/queries";
|
|
8
|
+
import { H1, H4 } from "../components/Typography";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
slug: string;
|
|
12
|
+
cmsLocale?: string;
|
|
13
|
+
isDraftMode: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CmsPageView = ({ slug, cmsLocale, isDraftMode }: Props) => {
|
|
17
|
+
const { data } = useSuspenseQuery(
|
|
18
|
+
cmsQueryOptions.page(slug, isDraftMode ? "draft" : "published", cmsLocale),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const pageData = data?.data[0];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{isDraftMode && <DraftModeBadge />}
|
|
26
|
+
{pageData ? (
|
|
27
|
+
<Box
|
|
28
|
+
sx={{
|
|
29
|
+
width: "100%",
|
|
30
|
+
maxWidth: "100vw",
|
|
31
|
+
overflowX: "hidden",
|
|
32
|
+
wordWrap: "break-word",
|
|
33
|
+
marginTop: "25px",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<BlockManager
|
|
37
|
+
blocks={pageData?.block as unknown as BlockManagerType}
|
|
38
|
+
/>
|
|
39
|
+
</Box>
|
|
40
|
+
) : (
|
|
41
|
+
<Container>
|
|
42
|
+
<Paper sx={{ marginTop: "25px", padding: "50px" }} elevation={1}>
|
|
43
|
+
<H1>E4 Storefront</H1>
|
|
44
|
+
<H4>
|
|
45
|
+
Add a page to the "Page" content-type in CMS to add
|
|
46
|
+
content to this page.
|
|
47
|
+
</H4>
|
|
48
|
+
</Paper>
|
|
49
|
+
</Container>
|
|
50
|
+
)}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import DropZone from "@evenicanpm/storefront-core/src/components/DropZone";
|
|
4
4
|
import { Alert } from "@mui/material";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import type { Accept, FileRejection } from "react-dropzone";
|
|
8
8
|
|
|
9
9
|
export interface OrderUploadItem {
|
|
@@ -42,10 +42,16 @@ export default function OrderUpload({
|
|
|
42
42
|
const t = useTranslations("QuickOrder");
|
|
43
43
|
|
|
44
44
|
const [items, setItems] = useState<OrderUploadItem[]>([]);
|
|
45
|
-
const [
|
|
45
|
+
const [_errCount, setErrCount] = useState<number>(0);
|
|
46
46
|
const [files, setFiles] = useState<File[]>();
|
|
47
47
|
const [fileRejections, setFileRejections] = useState<string[]>([]);
|
|
48
48
|
|
|
49
|
+
const itemsRef = useRef(items);
|
|
50
|
+
itemsRef.current = items;
|
|
51
|
+
|
|
52
|
+
const onUploadItemsRef = useRef(onUploadItems);
|
|
53
|
+
onUploadItemsRef.current = onUploadItems;
|
|
54
|
+
|
|
49
55
|
const accept: Accept = { "text/*": [".xlsx", ".csv", ".xls"] };
|
|
50
56
|
|
|
51
57
|
useEffect(loadXlsxScript, []);
|
|
@@ -60,16 +66,16 @@ export default function OrderUpload({
|
|
|
60
66
|
const sheet = readFirstSheet(buffer);
|
|
61
67
|
const parsed = extractItemsFromSheet(sheet);
|
|
62
68
|
|
|
63
|
-
const { validItems, errors } = validateItems(parsed,
|
|
69
|
+
const { validItems, errors } = validateItems(parsed, itemsRef.current);
|
|
64
70
|
|
|
65
71
|
setItems(validItems);
|
|
66
|
-
setErrCount(
|
|
67
|
-
|
|
72
|
+
setErrCount((prev) => prev + errors);
|
|
73
|
+
onUploadItemsRef.current(validItems);
|
|
68
74
|
}
|
|
69
75
|
};
|
|
70
76
|
|
|
71
77
|
processFiles();
|
|
72
|
-
}, [files
|
|
78
|
+
}, [files]);
|
|
73
79
|
|
|
74
80
|
const handleReject = (rejs: FileRejection[]) => {
|
|
75
81
|
const msgs = rejs.map((r) => `${r.file.name} ${t("fileRejectionMsg")}`);
|
|
@@ -61,6 +61,7 @@ export interface QuickOrderRowData {
|
|
|
61
61
|
|
|
62
62
|
type SearchableProduct = {
|
|
63
63
|
ItemId?: string;
|
|
64
|
+
RecordId?: number;
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
type Ctx = {
|
|
@@ -93,17 +94,22 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
93
94
|
|
|
94
95
|
const tableInit = useCallback(() => {
|
|
95
96
|
const next: QuickOrderRowData[] = [];
|
|
96
|
-
for (let i = 0; i <
|
|
97
|
+
for (let i = 0; i < 2; i++) next.push(generateEmptyRow(i + 1));
|
|
97
98
|
setRows(next);
|
|
98
99
|
}, []);
|
|
99
100
|
|
|
100
101
|
const deleteRow = (id: number) => {
|
|
102
|
+
if (rows.length <= 1) return;
|
|
101
103
|
const current = [...rows];
|
|
102
104
|
const idx = current.findIndex((r) => r.id === id);
|
|
103
105
|
if (idx > -1) {
|
|
104
106
|
current.splice(idx, 1);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
// Ensure there's always a trailing empty row for input
|
|
108
|
+
const lastRow = current[current.length - 1];
|
|
109
|
+
if (lastRow.value) {
|
|
110
|
+
const newRowId = current.reduce((m, r) => Math.max(m, r.id), 0) + 1;
|
|
111
|
+
current.push(generateEmptyRow(newRowId));
|
|
112
|
+
}
|
|
107
113
|
setRows(current);
|
|
108
114
|
}
|
|
109
115
|
};
|
|
@@ -176,6 +182,14 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
current[idx] = generateRowData(product, id, value, found, quantity);
|
|
185
|
+
|
|
186
|
+
// Auto-add an empty row when the last row has data entered
|
|
187
|
+
const lastRow = current[current.length - 1];
|
|
188
|
+
if (lastRow.value) {
|
|
189
|
+
const newRowId = current.reduce((m, r) => Math.max(m, r.id), 0) + 1;
|
|
190
|
+
current.push(generateEmptyRow(newRowId));
|
|
191
|
+
}
|
|
192
|
+
|
|
179
193
|
setRows(current);
|
|
180
194
|
};
|
|
181
195
|
|
|
@@ -234,9 +248,11 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
234
248
|
let rowId: number = curr.reduce((m, r) => Math.max(m, r.id), 0) + 1;
|
|
235
249
|
|
|
236
250
|
items.forEach((it: OrderUploadItem): void => {
|
|
251
|
+
const itemIdStr = it.itemId.toString().toLowerCase();
|
|
237
252
|
const product: ProductSearchResult | undefined = response?.find(
|
|
238
253
|
(r: SearchableProduct) =>
|
|
239
|
-
|
|
254
|
+
itemIdStr === r.ItemId?.toLowerCase() ||
|
|
255
|
+
itemIdStr === String(r.RecordId),
|
|
240
256
|
);
|
|
241
257
|
const qty: number = it.qty || 1;
|
|
242
258
|
curr.push(
|
|
@@ -250,7 +266,8 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
250
266
|
);
|
|
251
267
|
});
|
|
252
268
|
|
|
253
|
-
|
|
269
|
+
// Add one trailing empty row for the next entry
|
|
270
|
+
curr.push(generateEmptyRow(rowId++));
|
|
254
271
|
setRows(curr);
|
|
255
272
|
},
|
|
256
273
|
[rows, searchProductsMultiple, generateRowData],
|
|
@@ -316,20 +333,14 @@ QuickOrder.Body = () => {
|
|
|
316
333
|
const { rows } = ctx;
|
|
317
334
|
return (
|
|
318
335
|
<TableBody>
|
|
319
|
-
{rows.map((r
|
|
320
|
-
<QuickOrder.Row key={r.id} row={r}
|
|
336
|
+
{rows.map((r) => (
|
|
337
|
+
<QuickOrder.Row key={r.id} row={r} />
|
|
321
338
|
))}
|
|
322
339
|
</TableBody>
|
|
323
340
|
);
|
|
324
341
|
};
|
|
325
342
|
|
|
326
|
-
QuickOrder.Row = ({
|
|
327
|
-
row,
|
|
328
|
-
isLast,
|
|
329
|
-
}: {
|
|
330
|
-
row: QuickOrderRowData;
|
|
331
|
-
isLast: boolean;
|
|
332
|
-
}) => {
|
|
343
|
+
QuickOrder.Row = ({ row }: { row: QuickOrderRowData }) => {
|
|
333
344
|
const ctx = useQuickOrder();
|
|
334
345
|
const theme = useTheme();
|
|
335
346
|
const t = useTranslations("QuickOrder");
|
|
@@ -378,10 +389,6 @@ QuickOrder.Row = ({
|
|
|
378
389
|
ctx.updateRow(row.id, row.value, e.target.value);
|
|
379
390
|
};
|
|
380
391
|
|
|
381
|
-
const onFocus = () => {
|
|
382
|
-
if (isLast) ctx.addRows();
|
|
383
|
-
};
|
|
384
|
-
|
|
385
392
|
return (
|
|
386
393
|
<TableRow
|
|
387
394
|
sx={{
|
|
@@ -394,7 +401,6 @@ QuickOrder.Row = ({
|
|
|
394
401
|
<QuickOrder.SkuInput
|
|
395
402
|
value={inputValue}
|
|
396
403
|
onChange={onValueChange}
|
|
397
|
-
onFocus={onFocus}
|
|
398
404
|
placeholder={t("Table.itemNo")}
|
|
399
405
|
/>
|
|
400
406
|
</TableCell>
|
|
@@ -403,7 +409,6 @@ QuickOrder.Row = ({
|
|
|
403
409
|
<QuickOrder.QtyInput
|
|
404
410
|
value={row.quantity}
|
|
405
411
|
onChange={onQtyChange}
|
|
406
|
-
onFocus={onFocus}
|
|
407
412
|
placeholder={t("Table.quantity")}
|
|
408
413
|
/>
|
|
409
414
|
</TableCell>
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"use server";
|
|
2
|
-
import { decode } from "next-auth/jwt";
|
|
3
|
-
|
|
4
|
-
export async function decodeToken(token: string) {
|
|
5
|
-
const secret = process.env.NEXTAUTH_SECRET; //this shouldn't be from key vault.
|
|
6
|
-
if (!secret) {
|
|
7
|
-
throw new Error("Environment variable NEXTAUTH_SECRET is not set.");
|
|
8
|
-
}
|
|
9
|
-
return decode({ token: token, secret });
|
|
10
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { Account, Session } from "next-auth";
|
|
2
|
-
import type { JWT } from "next-auth/jwt";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import authOptions, { type ExtendedJwtToken } from "@/auth/auth-options";
|
|
5
|
-
|
|
6
|
-
describe("next-auth option callbacks", () => {
|
|
7
|
-
describe("jwt callback", () => {
|
|
8
|
-
it("should store accessToken and refreshToken when account and profile are available", async () => {
|
|
9
|
-
const token: ExtendedJwtToken = {
|
|
10
|
-
accessToken: "",
|
|
11
|
-
accessTokenExpiresAt: 0,
|
|
12
|
-
provider: "azure-ad-b2c",
|
|
13
|
-
refreshTokenExpiresAt: 0,
|
|
14
|
-
exp: 0,
|
|
15
|
-
};
|
|
16
|
-
const account: Account = {
|
|
17
|
-
access_token: "testAccessToken",
|
|
18
|
-
refresh_token: "testRefreshToken",
|
|
19
|
-
expires_on: Math.floor(Date.now() / 1000) + 3600,
|
|
20
|
-
refresh_token_expires_in: 86400,
|
|
21
|
-
providerAccountId: "test-provider-account-id",
|
|
22
|
-
provider: "azure-ad-b2c",
|
|
23
|
-
type: "oauth",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
let newToken: ExtendedJwtToken | undefined;
|
|
27
|
-
if (authOptions()?.callbacks?.jwt) {
|
|
28
|
-
newToken = await authOptions().callbacks.jwt({
|
|
29
|
-
token,
|
|
30
|
-
user: { id: "123" },
|
|
31
|
-
account,
|
|
32
|
-
profile: {},
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
expect(newToken?.accessToken).toBe("testAccessToken");
|
|
37
|
-
expect(newToken?.refreshToken).toBe("testRefreshToken");
|
|
38
|
-
expect(newToken?.accessTokenExpiresAt).toBe(token.accessTokenExpiresAt); // We're not setting this or using it anymore
|
|
39
|
-
expect(newToken?.refreshTokenExpiresAt).toBe(token.refreshTokenExpiresAt); // We're not setting this or using it anymore);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("session callback", () => {
|
|
44
|
-
it("should pass accessToken and error to session", async () => {
|
|
45
|
-
const session = { expires: "", user: { name: "123" } };
|
|
46
|
-
const token: JWT = {
|
|
47
|
-
accessToken: "sessionToken",
|
|
48
|
-
accessTokenExpiresAt: 0,
|
|
49
|
-
refreshToken: "",
|
|
50
|
-
refreshTokenExpiresAt: 0,
|
|
51
|
-
error: "SomeError",
|
|
52
|
-
};
|
|
53
|
-
let newSession: Session | null;
|
|
54
|
-
if (authOptions()?.callbacks?.session) {
|
|
55
|
-
newSession = await authOptions().callbacks.session({
|
|
56
|
-
session,
|
|
57
|
-
token,
|
|
58
|
-
user: { id: "123", email: "", emailVerified: new Date(Date.now()) },
|
|
59
|
-
newSession: {},
|
|
60
|
-
trigger: "update",
|
|
61
|
-
});
|
|
62
|
-
} else {
|
|
63
|
-
newSession = null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
expect(newSession?.accessToken).toBe("sessionToken");
|
|
67
|
-
expect(newSession?.error).toBe("SomeError");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
});
|
package/src/auth/auth-options.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { deleteCartCookie } from "@evenicanpm/storefront-core/src/lib/cart-cookie-handler";
|
|
2
|
-
import type {
|
|
3
|
-
GetServerSidePropsContext,
|
|
4
|
-
NextApiRequest,
|
|
5
|
-
NextApiResponse,
|
|
6
|
-
} from "next";
|
|
7
|
-
import type { NextAuthOptions, Session } from "next-auth";
|
|
8
|
-
import type { JWT } from "next-auth/jwt";
|
|
9
|
-
import { getServerSession } from "next-auth/next";
|
|
10
|
-
import { getProviders } from "@/auth/providers";
|
|
11
|
-
|
|
12
|
-
// Split providers into separate array so we can iterate them and match the provider ID
|
|
13
|
-
export type ExtendedJwtToken = JWT & {
|
|
14
|
-
provider: string;
|
|
15
|
-
exp: number;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type ExtendedSession = Session & { token?: JWT };
|
|
19
|
-
|
|
20
|
-
const createAuthOptions = (): NextAuthOptions => {
|
|
21
|
-
const providers = getProviders();
|
|
22
|
-
const secret = process.env.NEXTAUTH_SECRET; // internally used by next-auth, shouldn't be from key vault.
|
|
23
|
-
return {
|
|
24
|
-
providers,
|
|
25
|
-
secret,
|
|
26
|
-
cookies: {
|
|
27
|
-
csrfToken: {
|
|
28
|
-
name: "next-auth.csrf-token",
|
|
29
|
-
options: {
|
|
30
|
-
httpOnly: true,
|
|
31
|
-
sameSite: "none",
|
|
32
|
-
path: "/",
|
|
33
|
-
secure: true,
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
pkceCodeVerifier: {
|
|
37
|
-
name: "next-auth.pkce.code_verifier",
|
|
38
|
-
options: {
|
|
39
|
-
httpOnly: true,
|
|
40
|
-
sameSite: "none",
|
|
41
|
-
path: "/",
|
|
42
|
-
secure: true,
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
nonce: {
|
|
46
|
-
name: "next-auth.nonce",
|
|
47
|
-
options: {
|
|
48
|
-
httpOnly: true,
|
|
49
|
-
sameSite: "none",
|
|
50
|
-
path: "/",
|
|
51
|
-
secure: true,
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
// Order in which callbacks are called = signin > redirect > jwt > session. Thats why session callback has token info.
|
|
56
|
-
callbacks: {
|
|
57
|
-
// By default, NextAuth uses "session" with strategy: "jwt"
|
|
58
|
-
async jwt({ token, account, profile }) {
|
|
59
|
-
const _token = token as ExtendedJwtToken;
|
|
60
|
-
// account and profile is available only in first callback after signin
|
|
61
|
-
if (account && profile) {
|
|
62
|
-
_token.provider = account.provider;
|
|
63
|
-
// use id_token for entra external id and access_token for aadb2c
|
|
64
|
-
_token.accessToken =
|
|
65
|
-
_token.provider === "entra-external-id"
|
|
66
|
-
? account.id_token
|
|
67
|
-
: account.access_token;
|
|
68
|
-
_token.refreshToken = account.refresh_token;
|
|
69
|
-
}
|
|
70
|
-
return _token;
|
|
71
|
-
// return await checkAccessTokenExpiry(_token)
|
|
72
|
-
},
|
|
73
|
-
async session({ session, token }) {
|
|
74
|
-
const _session = { ...session } as ExtendedSession;
|
|
75
|
-
// Way to pass data to the browser
|
|
76
|
-
// Use this error to force logout in UI. No proper way to signout user from server.
|
|
77
|
-
_session.error = token?.error;
|
|
78
|
-
_session.accessToken = token.accessToken;
|
|
79
|
-
|
|
80
|
-
return _session;
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
events: {
|
|
84
|
-
signOut: async () => {
|
|
85
|
-
await deleteCartCookie();
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export function auth(
|
|
92
|
-
// <-- use this function to access the jwt from React components
|
|
93
|
-
...args:
|
|
94
|
-
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
|
|
95
|
-
| [NextApiRequest, NextApiResponse]
|
|
96
|
-
| []
|
|
97
|
-
) {
|
|
98
|
-
return getServerSession(...args, createAuthOptions());
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export default createAuthOptions;
|