@evenicanpm/storefront-core 2.2.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 +7 -3
- 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 +10 -3
- package/src/api-manager/datasources/e4/e4.datasource.ts +9 -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 +11 -5
- 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 +4 -10
- 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 +1 -1
- 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/components/user.tsx +37 -12
- package/src/components/mini-cart/mini-cart.tsx +3 -3
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +1 -0
- 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/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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import NoRecords from "@evenicanpm/storefront-core/src/components/no-records";
|
|
2
|
+
import { Box, CircularProgress } from "@mui/material";
|
|
3
|
+
import { cloneElement, Fragment } from "react";
|
|
4
|
+
import OrderHistoryHeader from "./order-history-header";
|
|
5
|
+
import OrdersPagination from "./order-history-pagination";
|
|
6
|
+
import { useOrderHistory } from "./order-history-root";
|
|
7
|
+
import OrderHistoryRow, {
|
|
8
|
+
OrderHistoryTableRowContext,
|
|
9
|
+
} from "./order-history-row";
|
|
10
|
+
|
|
11
|
+
const OrderHistoryTable = ({ children }: { children: React.ReactNode }) => {
|
|
12
|
+
return <>{children}</>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
OrderHistoryTable.List = ({ children }: { children: React.ReactElement }) => {
|
|
16
|
+
const { orders, isLoading } = useOrderHistory();
|
|
17
|
+
|
|
18
|
+
if (isLoading) {
|
|
19
|
+
return (
|
|
20
|
+
<Box display="flex" justifyContent="center" my={4}>
|
|
21
|
+
<CircularProgress />
|
|
22
|
+
</Box>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!orders || orders.length === 0) {
|
|
27
|
+
return <NoRecords message="Account.Orders.noOrders" />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Fragment>
|
|
32
|
+
{orders.map((order, index) => (
|
|
33
|
+
<OrderHistoryTableRowContext.Provider
|
|
34
|
+
key={order.RecordId ?? index}
|
|
35
|
+
value={{ order }}
|
|
36
|
+
>
|
|
37
|
+
{cloneElement(children)}
|
|
38
|
+
</OrderHistoryTableRowContext.Provider>
|
|
39
|
+
))}
|
|
40
|
+
<Box mt={4}>
|
|
41
|
+
<OrderHistoryTable.Pagination>
|
|
42
|
+
<OrderHistoryTable.Pagination.ResultNumber />
|
|
43
|
+
<OrderHistoryTable.Pagination.PaginationControl />
|
|
44
|
+
</OrderHistoryTable.Pagination>
|
|
45
|
+
</Box>
|
|
46
|
+
</Fragment>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
OrderHistoryTable.Header = OrderHistoryHeader;
|
|
51
|
+
OrderHistoryTable.Row = OrderHistoryRow;
|
|
52
|
+
OrderHistoryTable.Pagination = OrdersPagination;
|
|
53
|
+
|
|
54
|
+
export default OrderHistoryTable;
|
|
@@ -39,7 +39,7 @@ const OrderRow = ({ order, children }: React.PropsWithChildren<Props>) => {
|
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
41
|
<OrderRowContext.Provider value={{ order, formatCurrency }}>
|
|
42
|
-
<Link href={`/account/orders/${order.
|
|
42
|
+
<Link href={`/account/orders/${order.SalesId}`}>
|
|
43
43
|
<TableRow sx={{ gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr" }}>
|
|
44
44
|
{children}
|
|
45
45
|
</TableRow>
|
|
@@ -73,7 +73,7 @@ OrderRow.Date = () => {
|
|
|
73
73
|
const { order } = context;
|
|
74
74
|
return (
|
|
75
75
|
<Paragraph textAlign={{ sm: "center", xs: "left" }}>
|
|
76
|
-
{format(new Date(order.
|
|
76
|
+
{format(new Date(order.CreatedDateTime ?? ""), "MMM dd, yyyy")}
|
|
77
77
|
</Paragraph>
|
|
78
78
|
);
|
|
79
79
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Box from "@mui/material/Box";
|
|
4
|
+
import styled from "@mui/material/styles/styled";
|
|
5
|
+
|
|
6
|
+
export const OrdersDropDownHandler = styled(Box)(({ theme }) => ({
|
|
7
|
+
width: 150,
|
|
8
|
+
minWidth: 150,
|
|
9
|
+
height: "100%",
|
|
10
|
+
display: "flex",
|
|
11
|
+
whiteSpace: "pre",
|
|
12
|
+
alignItems: "center",
|
|
13
|
+
gap: theme.spacing(0.5),
|
|
14
|
+
justifyContent: "flex-start",
|
|
15
|
+
color: theme.palette.grey[700],
|
|
16
|
+
paddingInline: theme.spacing(2),
|
|
17
|
+
borderRight: `1px solid ${theme.palette.grey[400]}`,
|
|
18
|
+
[theme.breakpoints.down("xs")]: { display: "none" },
|
|
19
|
+
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Card from "@mui/material/Card";
|
|
2
|
+
import styled from "@mui/material/styles/styled";
|
|
3
|
+
|
|
4
|
+
const TableHeader = styled(Card)(({ theme }) => ({
|
|
5
|
+
gap: 16,
|
|
6
|
+
marginBlock: 16,
|
|
7
|
+
display: "grid",
|
|
8
|
+
borderRadius: 10,
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
padding: ".6rem 1.2rem",
|
|
11
|
+
gridTemplateColumns: "1.5fr 2fr 1.5fr auto",
|
|
12
|
+
[theme.breakpoints.down("sm")]: {
|
|
13
|
+
gap: 8,
|
|
14
|
+
gridTemplateColumns: "repeat(2, 1fr)",
|
|
15
|
+
},
|
|
16
|
+
backgroundColor: theme.palette.primary.main,
|
|
17
|
+
color: theme.palette.primary.contrastText,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
export default TableHeader;
|
|
@@ -14,7 +14,7 @@ import { format } from "date-fns";
|
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
16
|
title: string;
|
|
17
|
-
imageUrl: string;
|
|
17
|
+
imageUrl: string | undefined;
|
|
18
18
|
description: string;
|
|
19
19
|
createdAt: Date;
|
|
20
20
|
id: string;
|
|
@@ -31,8 +31,14 @@ export default function BlogCard({
|
|
|
31
31
|
}: Readonly<Props>) {
|
|
32
32
|
return (
|
|
33
33
|
<Box overflow="hidden">
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
{imageUrl && (
|
|
35
|
+
<ZoomLazyImage
|
|
36
|
+
width={588}
|
|
37
|
+
height={272}
|
|
38
|
+
alt="blog-image"
|
|
39
|
+
src={imageUrl}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
36
42
|
<Box py={3}>
|
|
37
43
|
<H3 lineHeight={1.3} color="secondary.900">
|
|
38
44
|
{title}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import ArrowBack from "@mui/icons-material/ArrowBack";
|
|
3
|
+
import { Box, Container, Typography } from "@mui/material";
|
|
4
|
+
import Grid from "@mui/material/Grid2";
|
|
5
|
+
import { BlocksRenderer } from "@strapi/blocks-react-renderer";
|
|
6
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
7
|
+
import Link from "next/link";
|
|
8
|
+
import { cmsQueryOptions } from "../../cms/queries";
|
|
9
|
+
import { BorderShadowCard } from "../../components/BoxShadowCard";
|
|
10
|
+
import { Display, Subtitle } from "../../components/Typography";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
id: string;
|
|
14
|
+
locale: string;
|
|
15
|
+
cmsLocale?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const BlogDetailView = ({ id, locale, cmsLocale }: Props) => {
|
|
19
|
+
const { data: blog } = useSuspenseQuery(cmsQueryOptions.blog(id, cmsLocale));
|
|
20
|
+
|
|
21
|
+
if (!blog) return null;
|
|
22
|
+
|
|
23
|
+
const blogData = blog?.data;
|
|
24
|
+
const imageUrl = blogData?.image?.formats?.large?.url;
|
|
25
|
+
const imageAlt = blogData?.image?.alternativeText || "Image";
|
|
26
|
+
const publishDate = new Date(blogData?.publishedAt).toLocaleDateString(
|
|
27
|
+
locale,
|
|
28
|
+
);
|
|
29
|
+
const title = blogData?.title;
|
|
30
|
+
const shortDescription = blogData?.shortDescription;
|
|
31
|
+
const content = blogData?.content;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Container className="mt-2 mb-2" maxWidth="xl">
|
|
35
|
+
<BorderShadowCard style={{ padding: 0 }}>
|
|
36
|
+
{/* Hero header — full-width image with dark overlay and text */}
|
|
37
|
+
<Box
|
|
38
|
+
sx={{
|
|
39
|
+
position: "relative",
|
|
40
|
+
width: "100%",
|
|
41
|
+
minHeight: { xs: 280, md: 420 },
|
|
42
|
+
borderTopRightRadius: "inherit",
|
|
43
|
+
borderTopLeftRadius: "inherit",
|
|
44
|
+
overflow: "hidden",
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{imageUrl && (
|
|
48
|
+
<Box
|
|
49
|
+
component="img"
|
|
50
|
+
src={imageUrl}
|
|
51
|
+
alt={imageAlt}
|
|
52
|
+
sx={{
|
|
53
|
+
position: "absolute",
|
|
54
|
+
inset: 0,
|
|
55
|
+
width: "100%",
|
|
56
|
+
height: "100%",
|
|
57
|
+
objectFit: "cover",
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
{/* Dark gradient overlay */}
|
|
62
|
+
<Box
|
|
63
|
+
sx={{
|
|
64
|
+
position: "absolute",
|
|
65
|
+
inset: 0,
|
|
66
|
+
background:
|
|
67
|
+
"linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.35) 60%, rgba(0,0,0,0.15) 100%)",
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
{/* Text content on top of overlay */}
|
|
71
|
+
<Box
|
|
72
|
+
sx={{
|
|
73
|
+
position: "relative",
|
|
74
|
+
zIndex: 1,
|
|
75
|
+
display: "flex",
|
|
76
|
+
flexDirection: "column",
|
|
77
|
+
justifyContent: "flex-end",
|
|
78
|
+
height: "100%",
|
|
79
|
+
minHeight: { xs: 280, md: 420 },
|
|
80
|
+
padding: { xs: "24px 20px", md: "40px 40px" },
|
|
81
|
+
color: "#fff",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Link
|
|
85
|
+
href="/blog"
|
|
86
|
+
style={{
|
|
87
|
+
color: "rgba(255,255,255,0.85)",
|
|
88
|
+
marginBottom: "16px",
|
|
89
|
+
display: "inline-flex",
|
|
90
|
+
alignItems: "center",
|
|
91
|
+
gap: 4,
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<ArrowBack fontSize="small" />
|
|
95
|
+
</Link>
|
|
96
|
+
<Display sx={{ color: "#fff", mb: 1 }}>{title}</Display>
|
|
97
|
+
<Typography
|
|
98
|
+
sx={{
|
|
99
|
+
color: "rgba(255,255,255,0.85)",
|
|
100
|
+
lineHeight: "1.7",
|
|
101
|
+
maxWidth: 640,
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{shortDescription}
|
|
105
|
+
</Typography>
|
|
106
|
+
</Box>
|
|
107
|
+
</Box>
|
|
108
|
+
<Box>
|
|
109
|
+
<Grid
|
|
110
|
+
container
|
|
111
|
+
sx={{
|
|
112
|
+
backgroundColor: "background.paper",
|
|
113
|
+
padding: "25px",
|
|
114
|
+
boxSizing: "border-box",
|
|
115
|
+
color: "grey.900",
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<Grid size={{ xs: 12, md: 9 }} order={{ xs: 2, md: 1 }}>
|
|
119
|
+
<Box sx={blogSx}>
|
|
120
|
+
<BlocksRenderer
|
|
121
|
+
content={
|
|
122
|
+
content as React.ComponentProps<
|
|
123
|
+
typeof BlocksRenderer
|
|
124
|
+
>["content"]
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
</Box>
|
|
128
|
+
</Grid>
|
|
129
|
+
<Grid size={{ xs: 12, md: 3 }} order={{ xs: 1, md: 2 }}>
|
|
130
|
+
<Box sx={{ paddingBottom: "15px" }}>
|
|
131
|
+
<Subtitle sx={noSpaceSx}>Published on: {publishDate}</Subtitle>
|
|
132
|
+
</Box>
|
|
133
|
+
</Grid>
|
|
134
|
+
</Grid>
|
|
135
|
+
</Box>
|
|
136
|
+
</BorderShadowCard>
|
|
137
|
+
</Container>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const noSpaceSx = { margin: 0, padding: 0 };
|
|
142
|
+
const blogSx = {
|
|
143
|
+
maxWidth: "650px",
|
|
144
|
+
"& img": {
|
|
145
|
+
width: "100%",
|
|
146
|
+
},
|
|
147
|
+
"& p": {
|
|
148
|
+
lineHeight: "1.8",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Box, Container, Grid } from "@mui/material";
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import { cmsQueryOptions } from "../../cms/queries";
|
|
5
|
+
import { BorderShadowCard } from "../../components/BoxShadowCard";
|
|
6
|
+
import { FlexBox } from "../../components/flex-box";
|
|
7
|
+
import { H1, H3 } from "../../components/Typography";
|
|
8
|
+
import BlogCard from "./blog-card";
|
|
9
|
+
import Pagination from "./pagination";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
page: number;
|
|
13
|
+
pageSize: number;
|
|
14
|
+
cmsLocale?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const BlogListView = ({ page, pageSize, cmsLocale }: Props) => {
|
|
18
|
+
const { data: blogs } = useSuspenseQuery(
|
|
19
|
+
cmsQueryOptions.blogs(page, pageSize, cmsLocale),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const pageCount = blogs?.meta.pagination?.pageCount ?? 0;
|
|
23
|
+
|
|
24
|
+
if (blogs.data.length === 0) {
|
|
25
|
+
return (
|
|
26
|
+
<BorderShadowCard>
|
|
27
|
+
<FlexBox justifyContent="center">
|
|
28
|
+
<H3 color={"grey.700"}>No posts yet, visit again soon!</H3>
|
|
29
|
+
</FlexBox>
|
|
30
|
+
</BorderShadowCard>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Container className="mt-2 mb-2" maxWidth="xl">
|
|
36
|
+
<H1>Posts</H1>
|
|
37
|
+
<Grid container spacing={3}>
|
|
38
|
+
{blogs.data.map((blog) => {
|
|
39
|
+
const imageUrl = blog?.image?.formats?.small?.url;
|
|
40
|
+
return (
|
|
41
|
+
<Grid item md={6} xs={12} key={blog.id}>
|
|
42
|
+
<BlogCard
|
|
43
|
+
key={blog?.id}
|
|
44
|
+
title={blog?.title}
|
|
45
|
+
imageUrl={imageUrl || undefined}
|
|
46
|
+
description={blog?.shortDescription}
|
|
47
|
+
createdAt={blog?.createdAt}
|
|
48
|
+
id={blog.documentId}
|
|
49
|
+
/>
|
|
50
|
+
</Grid>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</Grid>
|
|
54
|
+
<Box display="flex" justifyContent="center" sx={{ my: 2 }}>
|
|
55
|
+
<Pagination count={pageCount} page={page} pageSize={pageSize} />
|
|
56
|
+
</Box>
|
|
57
|
+
</Container>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -162,11 +162,12 @@ EstimateShipping.State = () => {
|
|
|
162
162
|
onChange={(e) => setSelectedState(e.target.value)}
|
|
163
163
|
>
|
|
164
164
|
<MenuItem value="">Select a state</MenuItem>
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
{Array.isArray(states) &&
|
|
166
|
+
states.map(({ StateName, StateId }) => (
|
|
167
|
+
<MenuItem value={StateId} key={StateId}>
|
|
168
|
+
{StateName}
|
|
169
|
+
</MenuItem>
|
|
170
|
+
))}
|
|
170
171
|
</TextField>
|
|
171
172
|
);
|
|
172
173
|
};
|
|
@@ -70,29 +70,51 @@ const CheckoutForm = ({ cart, children }: CheckoutFormProps) => {
|
|
|
70
70
|
const placeOrder = async () => {
|
|
71
71
|
setIsSubmitting(true);
|
|
72
72
|
const cache = queryClient.getQueryCache();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!paymentAcceptResult?.TokenizedPaymentCard) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
73
|
+
|
|
74
|
+
const selectedPaymentMethod =
|
|
75
|
+
(queryClient.getQueryData<string>(["selectedPaymentMethod", cart.Id]) as
|
|
76
|
+
| "card"
|
|
77
|
+
| "on-account") ?? "card";
|
|
82
78
|
|
|
83
79
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
80
|
+
if (selectedPaymentMethod === "on-account") {
|
|
81
|
+
const data = await placeOrderMutation({
|
|
82
|
+
id: cart.Id,
|
|
83
|
+
receiptEmail: cart.ReceiptEmail ?? "",
|
|
84
|
+
currency: channelConfigurationData?.Currency ?? "",
|
|
85
|
+
amount: cart.TotalAmount || 0,
|
|
86
|
+
paymentMethod: "on-account",
|
|
87
|
+
});
|
|
88
|
+
await cleanupCart(data.Id);
|
|
89
|
+
setNotification({
|
|
90
|
+
message: t("placeOrderSuccessMessage"),
|
|
91
|
+
severity: "success",
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
const acceptResultCache = cache.get(
|
|
95
|
+
JSON.stringify(["paymentAcceptResult", cart.Id]),
|
|
96
|
+
);
|
|
97
|
+
const paymentAcceptResult = acceptResultCache?.state
|
|
98
|
+
.data as CardPaymentAcceptResult;
|
|
99
|
+
|
|
100
|
+
if (!paymentAcceptResult?.TokenizedPaymentCard) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await placeOrderMutation({
|
|
105
|
+
id: cart.Id,
|
|
106
|
+
receiptEmail: cart.ReceiptEmail ?? "",
|
|
107
|
+
tokenizedPaymentCard: paymentAcceptResult.TokenizedPaymentCard,
|
|
108
|
+
currency: channelConfigurationData?.Currency ?? "",
|
|
109
|
+
amount: cart.TotalAmount || 0,
|
|
110
|
+
paymentMethod: "card",
|
|
111
|
+
});
|
|
112
|
+
await cleanupCart(data.Id);
|
|
113
|
+
setNotification({
|
|
114
|
+
message: t("placeOrderSuccessMessage"),
|
|
115
|
+
severity: "success",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
96
118
|
} catch (error) {
|
|
97
119
|
console.error(error);
|
|
98
120
|
setNotification({
|
|
@@ -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 (
|