@springmicro/cart 0.2.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5855 -5429
- package/dist/index.umd.cjs +68 -68
- package/package.json +3 -3
- package/springmicro-cart-0.2.3.tgz +0 -0
- package/src/AddToCartForm.tsx +2 -1
- package/src/CartButton.tsx +119 -48
- package/src/ProductCard.css +106 -0
- package/src/ProductCard.tsx +165 -0
- package/src/checkout/CheckoutList.css +93 -0
- package/src/checkout/CheckoutList.tsx +264 -0
- package/src/checkout/{Address.tsx → components/Address.tsx} +2 -2
- package/src/checkout/{Billing.tsx → components/Billing.tsx} +19 -12
- package/src/checkout/components/CartProductCard.css +67 -0
- package/src/checkout/components/CartProductCard.tsx +80 -0
- package/src/checkout/{index.tsx → components/index.tsx} +10 -3
- package/src/index.ts +11 -5
- package/src/types.d.ts +29 -4
- package/src/utils/api.ts +0 -1
- package/src/utils/cartAuthHandler.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/utils/storage.ts +44 -9
- /package/src/checkout/{Order.tsx → components/Order.tsx} +0 -0
- /package/src/checkout/{ProviderLogos.tsx → components/ProviderLogos.tsx} +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import "./CheckoutList.css";
|
|
2
|
+
import { useStore } from "@nanostores/react";
|
|
3
|
+
import { CartProductCard } from "./components/CartProductCard";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Button, Tooltip, Typography } from "@mui/material";
|
|
6
|
+
import {
|
|
7
|
+
cartStore,
|
|
8
|
+
clearCart,
|
|
9
|
+
setOrder as cartSetOrder,
|
|
10
|
+
} from "../utils/storage";
|
|
11
|
+
import Checkout from "./components";
|
|
12
|
+
|
|
13
|
+
export default function CheckoutList({
|
|
14
|
+
apiBaseUrl,
|
|
15
|
+
emptyCartLink = "",
|
|
16
|
+
disableProductLink = false,
|
|
17
|
+
}: {
|
|
18
|
+
apiBaseUrl: string;
|
|
19
|
+
emptyCartLink?: string;
|
|
20
|
+
disableProductLink?: boolean;
|
|
21
|
+
}) {
|
|
22
|
+
const cart = JSON.parse(useStore(cartStore));
|
|
23
|
+
|
|
24
|
+
const [prices, setPrices] = React.useState({});
|
|
25
|
+
const [total, setTotal] = React.useState("Loading prices...");
|
|
26
|
+
|
|
27
|
+
const [status, setStatus] = React.useState(0);
|
|
28
|
+
const [order, setOrder] = React.useState(undefined);
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
const params = new URLSearchParams(window.location.search);
|
|
32
|
+
|
|
33
|
+
const sr = params.get("showReceipt");
|
|
34
|
+
const oid = params.get("orderId");
|
|
35
|
+
const or = params.get("orderRef");
|
|
36
|
+
if (oid && or) {
|
|
37
|
+
fetch(`${apiBaseUrl}/api/ecommerce/orders/${oid}/reference/${or}`).then(
|
|
38
|
+
async (res) =>
|
|
39
|
+
await res.json().then((order) => {
|
|
40
|
+
setOrder(order);
|
|
41
|
+
setStatus(
|
|
42
|
+
order.charge_id ||
|
|
43
|
+
!["pending", "awaiting_payment"].includes(order.status)
|
|
44
|
+
? 2
|
|
45
|
+
: 1
|
|
46
|
+
);
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
// build pricing list
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
// filter out prices that have already been queried
|
|
55
|
+
const pricesToGet = cart.items
|
|
56
|
+
.map((c) => c.price_id)
|
|
57
|
+
.filter(
|
|
58
|
+
(pId) => Object.keys(prices).findIndex((pKey) => pKey == pId) === -1
|
|
59
|
+
);
|
|
60
|
+
if (pricesToGet.length === 0) return;
|
|
61
|
+
|
|
62
|
+
const url = `${apiBaseUrl}/api/ecommerce/price?filter={'ids':[${pricesToGet.join(
|
|
63
|
+
","
|
|
64
|
+
)}]}`;
|
|
65
|
+
fetch(url, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
.then((res) =>
|
|
72
|
+
res.json().then((data) => {
|
|
73
|
+
const pricingData = { ...prices };
|
|
74
|
+
|
|
75
|
+
data.forEach((p) => {
|
|
76
|
+
pricingData[p.id] = p;
|
|
77
|
+
});
|
|
78
|
+
setPrices(pricingData);
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
.catch(() => {});
|
|
82
|
+
}, [cart]);
|
|
83
|
+
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
setTotal(
|
|
86
|
+
`$${(
|
|
87
|
+
cart.items
|
|
88
|
+
.map((product) => prices[product.price_id]?.unit_amount)
|
|
89
|
+
.reduce((p, c) => (c ? p + c : p), 0) / 100
|
|
90
|
+
).toFixed(2)}`
|
|
91
|
+
);
|
|
92
|
+
}, [cart, prices]);
|
|
93
|
+
|
|
94
|
+
if (status === 0 && cart.items.length === 0)
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
style={{
|
|
98
|
+
width: "100%",
|
|
99
|
+
display: "flex",
|
|
100
|
+
alignItems: "center",
|
|
101
|
+
flexDirection: "column",
|
|
102
|
+
gap: 8,
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<Typography style={{ fontSize: 32, fontWeight: 600 }}>
|
|
106
|
+
Cart is empty
|
|
107
|
+
</Typography>
|
|
108
|
+
<a className="shopping-button" href={`/${emptyCartLink}`}>
|
|
109
|
+
<Typography>BACK</Typography>
|
|
110
|
+
</a>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div>
|
|
116
|
+
<StatusBar status={status} />
|
|
117
|
+
<div>
|
|
118
|
+
{status === 0 && (
|
|
119
|
+
<CartSection
|
|
120
|
+
total={total}
|
|
121
|
+
setStatus={setStatus}
|
|
122
|
+
cart={cart}
|
|
123
|
+
prices={prices}
|
|
124
|
+
apiBaseUrl={apiBaseUrl}
|
|
125
|
+
setOrder={setOrder}
|
|
126
|
+
disableProductLink={disableProductLink}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
{status > 0 && order && (
|
|
130
|
+
<Checkout
|
|
131
|
+
apiBaseUrl={apiBaseUrl}
|
|
132
|
+
order={order}
|
|
133
|
+
onPlacement={() => {
|
|
134
|
+
clearCart();
|
|
135
|
+
setStatus(2);
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function CartSection({
|
|
145
|
+
total,
|
|
146
|
+
setStatus,
|
|
147
|
+
cart,
|
|
148
|
+
prices,
|
|
149
|
+
apiBaseUrl,
|
|
150
|
+
setOrder,
|
|
151
|
+
disableProductLink,
|
|
152
|
+
}) {
|
|
153
|
+
function createOrder() {
|
|
154
|
+
// If an order has already been created and hasn't been changed, get the previous order.
|
|
155
|
+
if (cart.order && cart.order.id && cart.order.reference) {
|
|
156
|
+
fetch(
|
|
157
|
+
`${apiBaseUrl}/api/ecommerce/orders/${cart.order.id}/reference/${cart.order.reference}`
|
|
158
|
+
).then((res) =>
|
|
159
|
+
res.json().then((order) => {
|
|
160
|
+
setOrder(order);
|
|
161
|
+
const currentUrl = new URL(window.location.href);
|
|
162
|
+
currentUrl.searchParams.set("orderId", order.id);
|
|
163
|
+
currentUrl.searchParams.set("orderRef", order.reference);
|
|
164
|
+
window.history.pushState({}, "", currentUrl);
|
|
165
|
+
setStatus(1);
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Otherwise, create a new order.
|
|
172
|
+
fetch(apiBaseUrl + "/api/ecommerce/orders", {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
customer_id: 1,
|
|
179
|
+
cart_data: {
|
|
180
|
+
items: cart.items.map((li) => ({
|
|
181
|
+
...li,
|
|
182
|
+
name: undefined,
|
|
183
|
+
image: undefined,
|
|
184
|
+
})),
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
}).then((res) =>
|
|
188
|
+
res.json().then((order) => {
|
|
189
|
+
if (!order) throw "Missing order";
|
|
190
|
+
setOrder(order);
|
|
191
|
+
cartSetOrder({ id: order.id, reference: order.reference });
|
|
192
|
+
const currentUrl = new URL(window.location.href);
|
|
193
|
+
currentUrl.searchParams.set("orderId", order.id);
|
|
194
|
+
currentUrl.searchParams.set("orderRef", order.reference);
|
|
195
|
+
window.history.pushState({}, "", currentUrl);
|
|
196
|
+
setStatus(1);
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div style={{ display: "flex", justifyContent: "center" }}>
|
|
203
|
+
<div
|
|
204
|
+
style={{
|
|
205
|
+
display: "flex",
|
|
206
|
+
flexDirection: "column",
|
|
207
|
+
width: 600,
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
boxShadow: "0px 2px 5px 2px #dfdfdfff",
|
|
210
|
+
margin: "4px",
|
|
211
|
+
padding: "2rem 0",
|
|
212
|
+
borderRadius: "8px",
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<Typography style={{ fontSize: "30px", fontWeight: "bold" }}>
|
|
216
|
+
Checkout
|
|
217
|
+
</Typography>
|
|
218
|
+
<Tooltip title="Total is pre-tax and includes the first payment for recurring payments.">
|
|
219
|
+
<Typography style={{ fontSize: "20px" }}>Total: {total}</Typography>
|
|
220
|
+
</Tooltip>
|
|
221
|
+
{/* <Button variant="contained" sx={{ mt: 2 }} onClick={createOrder}>
|
|
222
|
+
Confirm Order
|
|
223
|
+
</Button> */}
|
|
224
|
+
<div className="checkout-list">
|
|
225
|
+
{cart.items.map((p, i) => (
|
|
226
|
+
<CartProductCard
|
|
227
|
+
product={p}
|
|
228
|
+
i={i}
|
|
229
|
+
price={prices[p.price_id]}
|
|
230
|
+
disableProductLink={disableProductLink}
|
|
231
|
+
/>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
<Button variant="contained" sx={{ mt: 2 }} onClick={createOrder}>
|
|
235
|
+
Confirm Order
|
|
236
|
+
</Button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function StatusBar({ status }) {
|
|
243
|
+
return (
|
|
244
|
+
<div
|
|
245
|
+
style={{
|
|
246
|
+
display: "flex",
|
|
247
|
+
alignItems: "center",
|
|
248
|
+
flexDirection: "column",
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
<div id="status-bar">
|
|
252
|
+
<Typography className="status-text active">REVIEW</Typography>
|
|
253
|
+
<div className={"status-bar" + (status > 0 ? " active" : "")}></div>
|
|
254
|
+
<Typography className={"status-text" + (status > 0 ? " active" : "")}>
|
|
255
|
+
PAYMENT
|
|
256
|
+
</Typography>
|
|
257
|
+
<div className={"status-bar" + (status > 1 ? " active" : "")}></div>
|
|
258
|
+
<Typography className={"status-text" + (status > 1 ? " active" : "")}>
|
|
259
|
+
ORDER PLACED
|
|
260
|
+
</Typography>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -238,7 +238,7 @@ export function AddressPostalCodeField({
|
|
|
238
238
|
] as Alpha2Code;
|
|
239
239
|
|
|
240
240
|
React.useEffect(() => {
|
|
241
|
-
if (
|
|
241
|
+
if (getPostalCodeDefault(country2code)) {
|
|
242
242
|
formik
|
|
243
243
|
.setFieldValue(name, getPostalCodeDefault(country2code))
|
|
244
244
|
.then(() => formik.setFieldTouched(name, true));
|
|
@@ -256,7 +256,7 @@ export function AddressPostalCodeField({
|
|
|
256
256
|
onBlur={formik.handleBlur}
|
|
257
257
|
required={true}
|
|
258
258
|
helperText={
|
|
259
|
-
|
|
259
|
+
getPostalCodeDefault(country2code)
|
|
260
260
|
? 'Our records indicate that your country does not have a postal code system, so "00000" will be used by default.'
|
|
261
261
|
: ""
|
|
262
262
|
}
|
|
@@ -126,30 +126,33 @@ export const AddCard = ({
|
|
|
126
126
|
}
|
|
127
127
|
: validationSchemaBase
|
|
128
128
|
),
|
|
129
|
-
onSubmit: async (
|
|
129
|
+
onSubmit: async (v, helpers) => {
|
|
130
|
+
setDisabled(true);
|
|
131
|
+
const values = { ...v };
|
|
130
132
|
if (disabled) {
|
|
133
|
+
setDisabled(false);
|
|
131
134
|
throw new Error("Attempted to submit an invalid form.");
|
|
132
135
|
}
|
|
133
136
|
if (onSubmit) {
|
|
134
137
|
if (address) {
|
|
135
138
|
// match backend
|
|
136
|
-
// @ts-
|
|
139
|
+
// @ts-expect-error value exists
|
|
137
140
|
values["street_address"] = values.line2
|
|
138
|
-
? // @ts-
|
|
141
|
+
? // @ts-expect-error value exists
|
|
139
142
|
`${values.line1}\n${values.line2}`
|
|
140
|
-
: // @ts-
|
|
143
|
+
: // @ts-expect-error value exists
|
|
141
144
|
values.line1;
|
|
142
|
-
// @ts-
|
|
145
|
+
// @ts-expect-error value exists
|
|
143
146
|
values["locality"] = values.city;
|
|
144
|
-
// @ts-
|
|
147
|
+
// @ts-expect-error value exists
|
|
145
148
|
delete values.line1;
|
|
146
|
-
// @ts-
|
|
149
|
+
// @ts-expect-error value exists
|
|
147
150
|
delete values.line2;
|
|
148
|
-
// @ts-
|
|
151
|
+
// @ts-expect-error value exists
|
|
149
152
|
delete values.city;
|
|
150
|
-
// @ts-
|
|
153
|
+
// @ts-expect-error value exists
|
|
151
154
|
if (Object.keys(allCountriesReverse).includes(values.country)) {
|
|
152
|
-
// @ts-
|
|
155
|
+
// @ts-expect-error value exists
|
|
153
156
|
values.country = allCountriesReverse[values.country];
|
|
154
157
|
}
|
|
155
158
|
}
|
|
@@ -160,6 +163,9 @@ export const AddCard = ({
|
|
|
160
163
|
delete values.number;
|
|
161
164
|
|
|
162
165
|
const res = await onSubmit(values);
|
|
166
|
+
setDisabled(false);
|
|
167
|
+
} else {
|
|
168
|
+
setDisabled(false);
|
|
163
169
|
}
|
|
164
170
|
},
|
|
165
171
|
});
|
|
@@ -194,7 +200,7 @@ export const AddCard = ({
|
|
|
194
200
|
<form
|
|
195
201
|
onSubmit={(e) => {
|
|
196
202
|
e.preventDefault();
|
|
197
|
-
formik.handleSubmit();
|
|
203
|
+
formik.handleSubmit(e);
|
|
198
204
|
}}
|
|
199
205
|
>
|
|
200
206
|
<Box
|
|
@@ -332,10 +338,11 @@ export const AddCard = ({
|
|
|
332
338
|
size="large"
|
|
333
339
|
sx={{ mt: 3 }}
|
|
334
340
|
variant="contained"
|
|
341
|
+
// onClick={formik.handleSubmit}
|
|
335
342
|
type="submit"
|
|
336
343
|
disabled={disabled}
|
|
337
344
|
>
|
|
338
|
-
|
|
345
|
+
Complete and pay
|
|
339
346
|
</Button>
|
|
340
347
|
<Box sx={{ mt: 1 }}>
|
|
341
348
|
<StripeLogoLink />
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.product-card {
|
|
2
|
+
width: 500px;
|
|
3
|
+
/* display: flex;
|
|
4
|
+
flex-direction: row;
|
|
5
|
+
justify-content: space-between; */
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: 1fr 40px;
|
|
8
|
+
gap: 0.5rem;
|
|
9
|
+
border-radius: 8px;
|
|
10
|
+
padding: 1rem;
|
|
11
|
+
box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0);
|
|
12
|
+
transition: all 0.3s 0.1s;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
min-height: 48px;
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.product-card:hover {
|
|
19
|
+
box-shadow: 0px 2px 5px 2px #dfdfdfff;
|
|
20
|
+
transition: all 0.3s 0s;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.left-section {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: row;
|
|
26
|
+
gap: 1rem;
|
|
27
|
+
flex-grow: 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.left-section > img,
|
|
31
|
+
.missing-image {
|
|
32
|
+
min-height: 100%;
|
|
33
|
+
max-height: 100px;
|
|
34
|
+
aspect-ratio: 1;
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.missing-image {
|
|
40
|
+
background-color: rgb(222, 222, 222);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.two-row {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.two-row :nth-child(1) {
|
|
49
|
+
font-size: 20px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.two-row :nth-child(2) {
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.two-row > * {
|
|
57
|
+
margin: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.remove-button {
|
|
61
|
+
color: transparent !important;
|
|
62
|
+
transition: all 0.2s !important;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.product-card:hover .remove-button {
|
|
66
|
+
color: red !important;
|
|
67
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import "./CartProductCard.css";
|
|
2
|
+
import { Tooltip, IconButton, Typography } from "@mui/material";
|
|
3
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { CartProduct } from "../../types";
|
|
6
|
+
import { removeFromCart } from "../../utils/storage";
|
|
7
|
+
|
|
8
|
+
export function CartProductCard({
|
|
9
|
+
product,
|
|
10
|
+
i,
|
|
11
|
+
price,
|
|
12
|
+
disableProductLink = false,
|
|
13
|
+
}: {
|
|
14
|
+
product: CartProduct;
|
|
15
|
+
i: number;
|
|
16
|
+
price: any;
|
|
17
|
+
disableProductLink?: boolean;
|
|
18
|
+
}) {
|
|
19
|
+
const [hoverDelete, setHoverDelete] = React.useState(false);
|
|
20
|
+
console.log(product);
|
|
21
|
+
return (
|
|
22
|
+
<a
|
|
23
|
+
className="product-card"
|
|
24
|
+
href={
|
|
25
|
+
disableProductLink || hoverDelete
|
|
26
|
+
? undefined
|
|
27
|
+
: `/product/${product.product_id}`
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
<div className="left-section">
|
|
31
|
+
{product.image ? <img /> : <div className="missing-image"></div>}
|
|
32
|
+
<div className="two-row">
|
|
33
|
+
<Typography>{product.name}</Typography>
|
|
34
|
+
<Typography>
|
|
35
|
+
{price
|
|
36
|
+
? `$${(price.unit_amount / 100).toFixed(2)}${
|
|
37
|
+
price.recurring
|
|
38
|
+
? `/${
|
|
39
|
+
price.recurring.interval_count > 1
|
|
40
|
+
? `${price.recurring.interval_count} `
|
|
41
|
+
: ""
|
|
42
|
+
}${price.recurring.interval}${
|
|
43
|
+
price.recurring.interval_count > 1 ? "s" : ""
|
|
44
|
+
}`
|
|
45
|
+
: ""
|
|
46
|
+
}`
|
|
47
|
+
: "Loading price..."}
|
|
48
|
+
</Typography>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div
|
|
52
|
+
style={{
|
|
53
|
+
display: "flex",
|
|
54
|
+
flexGrow: 1,
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<Tooltip
|
|
60
|
+
title="Remove from cart"
|
|
61
|
+
onMouseEnter={() => {
|
|
62
|
+
setHoverDelete(true);
|
|
63
|
+
}}
|
|
64
|
+
onMouseLeave={() => {
|
|
65
|
+
setHoverDelete(false);
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<IconButton
|
|
69
|
+
className="remove-button"
|
|
70
|
+
onClick={() => {
|
|
71
|
+
removeFromCart(i);
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<CloseIcon />
|
|
75
|
+
</IconButton>
|
|
76
|
+
</Tooltip>
|
|
77
|
+
</div>
|
|
78
|
+
</a>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AddCard, type AddCardProps } from "./Billing";
|
|
2
2
|
import Order from "./Order";
|
|
3
|
-
import { postCheckout } from "
|
|
3
|
+
import { postCheckout } from "../../utils/api";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { Alert, Container, Typography, CircularProgress } from "@mui/material";
|
|
6
6
|
|
|
@@ -8,12 +8,14 @@ type CheckoutProps = AddCardProps & {
|
|
|
8
8
|
order: any;
|
|
9
9
|
apiBaseUrl: string;
|
|
10
10
|
invoiceId?: string;
|
|
11
|
+
onPlacement?: () => void;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export default function Checkout({
|
|
14
15
|
order,
|
|
15
16
|
apiBaseUrl,
|
|
16
17
|
invoiceId,
|
|
18
|
+
onPlacement,
|
|
17
19
|
}: CheckoutProps) {
|
|
18
20
|
const currentUrl = new URL(window.location.href);
|
|
19
21
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
@@ -34,10 +36,13 @@ export default function Checkout({
|
|
|
34
36
|
} else {
|
|
35
37
|
// success
|
|
36
38
|
setSuccessData(data);
|
|
39
|
+
onPlacement && onPlacement();
|
|
37
40
|
// Get the current URL
|
|
38
41
|
const currentUrl = new URL(window.location.href);
|
|
39
42
|
// Set the query parameter 'showReceipt' to '1'
|
|
40
43
|
currentUrl.searchParams.set("showReceipt", "1");
|
|
44
|
+
currentUrl.searchParams.set("orderId", order.id);
|
|
45
|
+
currentUrl.searchParams.set("orderRef", order.reference);
|
|
41
46
|
|
|
42
47
|
// Update the browser's URL and history without refreshing the page
|
|
43
48
|
window.history.pushState({}, "", currentUrl);
|
|
@@ -47,7 +52,8 @@ export default function Checkout({
|
|
|
47
52
|
|
|
48
53
|
if (
|
|
49
54
|
successData !== null ||
|
|
50
|
-
currentUrl.searchParams.get("showReceipt")
|
|
55
|
+
// currentUrl.searchParams.get("showReceipt")
|
|
56
|
+
order.charge_id ||
|
|
51
57
|
!["pending", "awaiting_payment"].includes(order.status)
|
|
52
58
|
) {
|
|
53
59
|
const print = (
|
|
@@ -57,6 +63,7 @@ export default function Checkout({
|
|
|
57
63
|
window.print();
|
|
58
64
|
return false;
|
|
59
65
|
}}
|
|
66
|
+
style={{ textDecoration: "underline" }}
|
|
60
67
|
>
|
|
61
68
|
print
|
|
62
69
|
</a>
|
|
@@ -73,10 +80,10 @@ export default function Checkout({
|
|
|
73
80
|
);
|
|
74
81
|
return (
|
|
75
82
|
<>
|
|
76
|
-
<Order order={order} invoiceId={invoiceId} />
|
|
77
83
|
<Container>
|
|
78
84
|
<Alert severity="success">{text}</Alert>
|
|
79
85
|
</Container>
|
|
86
|
+
<Order order={order} invoiceId={invoiceId} />
|
|
80
87
|
</>
|
|
81
88
|
);
|
|
82
89
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,14 +4,17 @@ import {
|
|
|
4
4
|
addToCart,
|
|
5
5
|
removeFromCart,
|
|
6
6
|
clearCart,
|
|
7
|
+
setOrder,
|
|
7
8
|
} from "./utils/storage";
|
|
8
9
|
import type { Cart, CartProduct } from "./types";
|
|
9
10
|
import AddToCartForm from "./AddToCartForm";
|
|
10
|
-
import { AddCard, AddCardProps } from "./checkout/Billing";
|
|
11
|
-
import Order from "./checkout/Order";
|
|
12
|
-
import Checkout from "./checkout";
|
|
11
|
+
import { AddCard, AddCardProps } from "./checkout/components/Billing";
|
|
12
|
+
import Order from "./checkout/components/Order";
|
|
13
|
+
import Checkout from "./checkout/components";
|
|
13
14
|
import "react-credit-cards-2/dist/es/styles-compiled.css";
|
|
14
15
|
import "./index.css";
|
|
16
|
+
import ProductCard from "./ProductCard";
|
|
17
|
+
import CheckoutList from "./checkout/CheckoutList";
|
|
15
18
|
|
|
16
19
|
export {
|
|
17
20
|
Checkout,
|
|
@@ -20,10 +23,13 @@ export {
|
|
|
20
23
|
type AddCardProps,
|
|
21
24
|
cartStore,
|
|
22
25
|
CartButton,
|
|
23
|
-
Cart,
|
|
24
|
-
CartProduct,
|
|
26
|
+
type Cart,
|
|
27
|
+
type CartProduct,
|
|
25
28
|
addToCart,
|
|
26
29
|
removeFromCart,
|
|
27
30
|
clearCart,
|
|
28
31
|
AddToCartForm,
|
|
32
|
+
setOrder,
|
|
33
|
+
ProductCard,
|
|
34
|
+
CheckoutList,
|
|
29
35
|
};
|
package/src/types.d.ts
CHANGED
|
@@ -3,13 +3,19 @@ export interface Cart {
|
|
|
3
3
|
loggedIn: boolean;
|
|
4
4
|
user_id?: number | string;
|
|
5
5
|
};
|
|
6
|
-
|
|
6
|
+
items: CartProduct[];
|
|
7
|
+
order?: {
|
|
8
|
+
id: number;
|
|
9
|
+
reference: string;
|
|
10
|
+
};
|
|
7
11
|
}
|
|
8
12
|
export type CartProduct = {
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
product_id: number | string;
|
|
14
|
+
price_id: number | string;
|
|
11
15
|
quantity?: number;
|
|
12
|
-
|
|
16
|
+
// Used in local cart, delete when sent to api
|
|
17
|
+
name: string;
|
|
18
|
+
image?: string;
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
type CartContextType = {
|
|
@@ -29,3 +35,22 @@ export type PathDetailsType = {
|
|
|
29
35
|
baseUrl?: string;
|
|
30
36
|
userId?: string | number;
|
|
31
37
|
};
|
|
38
|
+
|
|
39
|
+
export type Product = {
|
|
40
|
+
id: string | number;
|
|
41
|
+
name: string;
|
|
42
|
+
/** v Parses to @type {ProductPricing} v */
|
|
43
|
+
pricing: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
} & unknown;
|
|
46
|
+
|
|
47
|
+
export type ProductPricing = {
|
|
48
|
+
id: number | string;
|
|
49
|
+
unit_amount: number; // in cents
|
|
50
|
+
recurring?: {
|
|
51
|
+
interval: "month" | "week" | "day";
|
|
52
|
+
interval_count: number;
|
|
53
|
+
};
|
|
54
|
+
tier_label?: string;
|
|
55
|
+
tier_description?: string;
|
|
56
|
+
};
|
package/src/utils/api.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function cartAuthHandler(
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// login
|
|
26
|
-
if (cart.
|
|
26
|
+
if (cart.items.length > 0) {
|
|
27
27
|
setCart((c) => ({
|
|
28
28
|
...c,
|
|
29
29
|
authentication: {
|
|
@@ -39,7 +39,7 @@ export function cartAuthHandler(
|
|
|
39
39
|
if (!c2) return;
|
|
40
40
|
|
|
41
41
|
setCart({
|
|
42
|
-
|
|
42
|
+
items: JSON.parse(c2.cart),
|
|
43
43
|
authentication: {
|
|
44
44
|
loggedIn: true,
|
|
45
45
|
user_id: c2.user_id,
|