@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.
Files changed (63) hide show
  1. package/package.json +7 -3
  2. package/src/api-manager/README.md +52 -27
  3. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
  4. package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
  6. package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
  7. package/src/api-manager/datasources/d365/d365-user.datasource.ts +96 -22
  8. package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
  9. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +10 -3
  10. package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
  11. package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
  12. package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
  13. package/src/api-manager/index.ts +54 -3
  14. package/src/api-manager/lib/get-graphql-client.ts +11 -5
  15. package/src/api-manager/schemas/cart.schema.ts +10 -1
  16. package/src/api-manager/schemas/invoice.schema.ts +55 -0
  17. package/src/api-manager/schemas/order.schema.ts +13 -1
  18. package/src/api-manager/schemas/user.schema.ts +8 -0
  19. package/src/api-manager/services/create-query.ts +4 -10
  20. package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
  21. package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
  22. package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
  23. package/src/api-manager/services/order/queries/get-orders.ts +6 -3
  24. package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
  25. package/src/api-manager/types/Datasource.ts +21 -2
  26. package/src/auth/better-auth.ts +1 -1
  27. package/src/cms/blocks/block-manager.tsx +18 -3
  28. package/src/cms/blocks/components/cta-banner.tsx +65 -0
  29. package/src/cms/blocks/components/hero-image.tsx +83 -0
  30. package/src/cms/blocks/index.tsx +2 -0
  31. package/src/cms/blocks/interfaces.ts +20 -0
  32. package/src/cms/draft-mode-badge.tsx +26 -0
  33. package/src/cms/queries.ts +81 -0
  34. package/src/components/flex-box/flex-box.tsx +2 -0
  35. package/src/components/header/components/user.tsx +37 -12
  36. package/src/components/mini-cart/mini-cart.tsx +3 -3
  37. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +1 -0
  38. package/src/pages/account/account-navigation.tsx +88 -60
  39. package/src/pages/account/account-routes.ts +52 -0
  40. package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
  41. package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
  42. package/src/pages/account/orders/order-history-header.tsx +54 -0
  43. package/src/pages/account/orders/order-history-pagination.tsx +55 -0
  44. package/src/pages/account/orders/order-history-root.tsx +125 -0
  45. package/src/pages/account/orders/order-history-row.tsx +110 -0
  46. package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
  47. package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
  48. package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
  49. package/src/pages/account/orders/order-history-sort.tsx +90 -0
  50. package/src/pages/account/orders/order-history-table.tsx +54 -0
  51. package/src/pages/account/orders/order-row.tsx +2 -2
  52. package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
  53. package/src/pages/account/table-header.tsx +20 -0
  54. package/src/pages/account/wishlist/wishlist-item.tsx +1 -1
  55. package/src/pages/blog/blog-card.tsx +9 -3
  56. package/src/pages/blog/blog-detail-view.tsx +150 -0
  57. package/src/pages/blog/blog-list-view.tsx +59 -0
  58. package/src/pages/cart/estimate-shipping.tsx +6 -5
  59. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
  60. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
  61. package/src/pages/cms-page-view.tsx +53 -0
  62. package/src/pages/quickorder/order-upload.tsx +12 -6
  63. 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.Id}`}>
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.TransactionDate ?? ""), "MMM dd, yyyy")}
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;
@@ -79,7 +79,7 @@ WishlistItem.Name = () => {
79
79
  return (
80
80
  <Paragraph ellipsis>
81
81
  <NavLink3
82
- href={`/account/wish-lists/${wishlist?.Id}`}
82
+ href={`/account/order-templates/${wishlist?.Id}`}
83
83
  text={wishlist?.Name ?? ""}
84
84
  />
85
85
  </Paragraph>
@@ -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
- <ZoomLazyImage width={588} height={272} alt="blog-image" src={imageUrl} />
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
- {states.map(({ StateName, StateId }) => (
166
- <MenuItem value={StateId} key={StateId}>
167
- {StateName}
168
- </MenuItem>
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
- const acceptResultCache = cache.get(
74
- JSON.stringify(["paymentAcceptResult", cart.Id]),
75
- );
76
- const paymentAcceptResult = acceptResultCache?.state
77
- .data as CardPaymentAcceptResult;
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
- const data = await placeOrderMutation({
85
- id: cart.Id,
86
- receiptEmail: cart.ReceiptEmail ?? "",
87
- tokenizedPaymentCard: paymentAcceptResult.TokenizedPaymentCard,
88
- currency: channelConfigurationData?.Currency ?? "",
89
- amount: cart.TotalAmount || 0,
90
- });
91
- await cleanupCart(data.Id);
92
- setNotification({
93
- message: t("placeOrderSuccessMessage"),
94
- severity: "success",
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 { Alert, Box, Card } from "@mui/material";
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<{ paymentAcceptResult: CardPaymentAcceptResult | null }>;
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 { data: paymentAcceptPointData } = getPaymentAcceptPoint.useData({
68
- cartId: cart.Id,
69
- totalAmount: cart.TotalAmount ?? 0,
70
- origin: globalThis.window?.location?.origin,
71
- isClient: !!globalThis.window,
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
- // INFO: This listens for `submitting` state, then posts submit message to iframe
223
- postSubmitMessage();
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
- }, [postSubmitMessage, stepSubmissionState]);
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 paymentAcceptResult={paymentAcceptResult} />
250
- ) : (
251
- <PaymentDetails.Form
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 (