@springmicro/cart 0.2.0-alpha.4 → 0.2.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/dist/index.js +11868 -10591
- package/dist/index.umd.cjs +91 -77
- package/package.json +5 -4
- package/src/checkout/Address.tsx +19 -0
- package/src/checkout/Billing.tsx +24 -5
- package/src/checkout/Order.tsx +93 -0
- package/src/checkout/index.tsx +97 -0
- package/src/index.css +5 -0
- package/src/index.ts +5 -0
- package/src/utils/api.ts +68 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springmicro/cart",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.0
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
"@nanostores/persistent": "^0.10.1",
|
|
24
24
|
"@nanostores/query": "^0.3.3",
|
|
25
25
|
"@nanostores/react": "^0.7.2",
|
|
26
|
-
"@springmicro/utils": "0.2.0
|
|
26
|
+
"@springmicro/utils": "0.2.0",
|
|
27
27
|
"dotenv": "^16.4.5",
|
|
28
28
|
"nanostores": "^0.10.3",
|
|
29
29
|
"react": "^18.2.0",
|
|
30
30
|
"react-credit-cards-2": "^1.0.2",
|
|
31
31
|
"react-dom": "^18.2.0",
|
|
32
|
-
"unstorage": "^1.10.2"
|
|
32
|
+
"unstorage": "^1.10.2",
|
|
33
|
+
"vite-plugin-dts": "^3.9.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@types/react": "^18.2.66",
|
|
@@ -46,5 +47,5 @@
|
|
|
46
47
|
"vite-plugin-css-injected-by-js": "^3.5.1",
|
|
47
48
|
"yup": "^1.4.0"
|
|
48
49
|
},
|
|
49
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "bada90b1f5c1efcd122b4a69436098408162677c"
|
|
50
51
|
}
|
package/src/checkout/Address.tsx
CHANGED
|
@@ -23,6 +23,7 @@ export type AddressValues = {
|
|
|
23
23
|
line1: string;
|
|
24
24
|
line2?: string;
|
|
25
25
|
organization?: string;
|
|
26
|
+
email: string;
|
|
26
27
|
city: string;
|
|
27
28
|
postal_code: string;
|
|
28
29
|
};
|
|
@@ -174,6 +175,24 @@ export function AddressStreetField({
|
|
|
174
175
|
);
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
export function AddressEmailField({ formik, name, sx }: AddressFieldProps) {
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<TextField
|
|
182
|
+
label={"Email"}
|
|
183
|
+
sx={{ width: 400, display: "block", ...sx }}
|
|
184
|
+
name={name as string}
|
|
185
|
+
id={name as string}
|
|
186
|
+
value={formik.values[name]}
|
|
187
|
+
onChange={formik.handleChange}
|
|
188
|
+
onBlur={formik.handleBlur}
|
|
189
|
+
type="email"
|
|
190
|
+
required={true}
|
|
191
|
+
/>
|
|
192
|
+
</>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
177
196
|
export function AddressOrganizationNameField({
|
|
178
197
|
formik,
|
|
179
198
|
name,
|
package/src/checkout/Billing.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
AddressPostalCodeField,
|
|
10
10
|
AddressRegionField,
|
|
11
11
|
AddressStreetField,
|
|
12
|
+
AddressEmailField,
|
|
12
13
|
AddressValues,
|
|
13
14
|
} from "./Address";
|
|
14
15
|
import {
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
allCountriesReverse,
|
|
17
18
|
getPostalCodeDefault,
|
|
18
19
|
} from "@springmicro/utils/address";
|
|
19
|
-
import { Box, Button, TextField, Typography } from "@mui/material";
|
|
20
|
+
import { Box, Button, Container, TextField, Typography } from "@mui/material";
|
|
20
21
|
import Cards from "react-credit-cards-2";
|
|
21
22
|
import { FocusEventHandler } from "react";
|
|
22
23
|
import {
|
|
@@ -42,6 +43,12 @@ export type CreditCardValues = {
|
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export type AddCardProps = {
|
|
46
|
+
contact?: {
|
|
47
|
+
organization?: string;
|
|
48
|
+
email: string;
|
|
49
|
+
first_name: string;
|
|
50
|
+
last_name: string;
|
|
51
|
+
};
|
|
45
52
|
stacked?: boolean;
|
|
46
53
|
error?: string;
|
|
47
54
|
address?: boolean;
|
|
@@ -51,6 +58,7 @@ export type AddCardProps = {
|
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
export const AddCard = ({
|
|
61
|
+
contact,
|
|
54
62
|
error,
|
|
55
63
|
stacked = false,
|
|
56
64
|
address = true,
|
|
@@ -62,16 +70,17 @@ export const AddCard = ({
|
|
|
62
70
|
cvc: "",
|
|
63
71
|
expiry: "",
|
|
64
72
|
focus: "number",
|
|
65
|
-
name: "",
|
|
73
|
+
name: contact ? `${contact.first_name} ${contact.last_name}` : "",
|
|
66
74
|
number: "",
|
|
67
75
|
} as CreditCardValues;
|
|
68
76
|
const initialCountry = "US";
|
|
69
77
|
const initialValuesAddress = {
|
|
70
78
|
country: initialCountry ? allCountries[initialCountry] : "",
|
|
79
|
+
email: contact?.email ?? "",
|
|
71
80
|
region: "",
|
|
72
81
|
line1: "",
|
|
73
82
|
line2: "",
|
|
74
|
-
organization: "",
|
|
83
|
+
organization: contact?.organization ?? "",
|
|
75
84
|
city: "",
|
|
76
85
|
postal_code: getPostalCodeDefault(initialCountry),
|
|
77
86
|
} as AddressValues;
|
|
@@ -101,6 +110,7 @@ export const AddCard = ({
|
|
|
101
110
|
|
|
102
111
|
const validationSchemaAddress = {
|
|
103
112
|
country: Yup.string().required("Country is required."),
|
|
113
|
+
email: Yup.string().email().required("Email is required."),
|
|
104
114
|
line1: Yup.string().required("Street Address 1 is required."),
|
|
105
115
|
city: Yup.string().required("City is required."),
|
|
106
116
|
postal_code: Yup.string().required("Postal Code is required."),
|
|
@@ -137,6 +147,11 @@ export const AddCard = ({
|
|
|
137
147
|
delete values.line2;
|
|
138
148
|
// @ts-ignore
|
|
139
149
|
delete values.city;
|
|
150
|
+
// @ts-ignore
|
|
151
|
+
if (Object.keys(allCountriesReverse).includes(values.country)) {
|
|
152
|
+
// @ts-ignore
|
|
153
|
+
values.country = allCountriesReverse[values.country];
|
|
154
|
+
}
|
|
140
155
|
}
|
|
141
156
|
values["card_number"] = values.number;
|
|
142
157
|
values["expiry_date"] = values.expiry;
|
|
@@ -175,7 +190,7 @@ export const AddCard = ({
|
|
|
175
190
|
}, [formik.errors]);
|
|
176
191
|
|
|
177
192
|
return (
|
|
178
|
-
|
|
193
|
+
<Container id="paymentMethodForm">
|
|
179
194
|
<form
|
|
180
195
|
onSubmit={(e) => {
|
|
181
196
|
e.preventDefault();
|
|
@@ -205,6 +220,7 @@ export const AddCard = ({
|
|
|
205
220
|
type="tel"
|
|
206
221
|
name="number"
|
|
207
222
|
label="Card Number"
|
|
223
|
+
required
|
|
208
224
|
onBlur={formik.handleBlur}
|
|
209
225
|
onChange={handleInputChange}
|
|
210
226
|
onFocus={handleInputFocus}
|
|
@@ -219,6 +235,7 @@ export const AddCard = ({
|
|
|
219
235
|
type="text"
|
|
220
236
|
name="name"
|
|
221
237
|
label="Name on Card"
|
|
238
|
+
required
|
|
222
239
|
onBlur={formik.handleBlur}
|
|
223
240
|
onChange={handleInputChange}
|
|
224
241
|
onFocus={handleInputFocus}
|
|
@@ -250,6 +267,7 @@ export const AddCard = ({
|
|
|
250
267
|
type="tel"
|
|
251
268
|
name="cvc"
|
|
252
269
|
label="CVC"
|
|
270
|
+
required
|
|
253
271
|
onBlur={formik.handleBlur}
|
|
254
272
|
onChange={handleInputChange}
|
|
255
273
|
onFocus={handleInputFocus}
|
|
@@ -272,6 +290,7 @@ export const AddCard = ({
|
|
|
272
290
|
Billing Address
|
|
273
291
|
</Typography>
|
|
274
292
|
<AddressCountryField formik={formik} name="country" />
|
|
293
|
+
<AddressEmailField formik={formik} name="email" sx={{ mt: 2 }} />
|
|
275
294
|
<AddressOrganizationNameField
|
|
276
295
|
sx={{ mt: 2 }}
|
|
277
296
|
formik={formik}
|
|
@@ -322,6 +341,6 @@ export const AddCard = ({
|
|
|
322
341
|
<StripeLogoLink />
|
|
323
342
|
</Box>
|
|
324
343
|
</form>
|
|
325
|
-
|
|
344
|
+
</Container>
|
|
326
345
|
);
|
|
327
346
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Container,
|
|
4
|
+
Typography,
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableContainer,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableRow,
|
|
11
|
+
Paper,
|
|
12
|
+
} from "@mui/material";
|
|
13
|
+
|
|
14
|
+
const Order = ({ order, invoiceId }) => {
|
|
15
|
+
// Formatter for currency in USD
|
|
16
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
17
|
+
style: "currency",
|
|
18
|
+
currency: "USD",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Container>
|
|
23
|
+
<Typography variant="h4" gutterBottom>
|
|
24
|
+
{invoiceId ? `Invoice #${invoiceId}` : "Order"}
|
|
25
|
+
</Typography>
|
|
26
|
+
<Typography variant="subtitle1">Reference: {order.reference}</Typography>
|
|
27
|
+
<Typography variant="subtitle1">
|
|
28
|
+
Date: {new Date(order.date).toLocaleDateString()}
|
|
29
|
+
</Typography>
|
|
30
|
+
<Typography variant="subtitle1">Status: {order.status}</Typography>
|
|
31
|
+
{order.customer ? (
|
|
32
|
+
<Typography variant="subtitle1">
|
|
33
|
+
{order.customer.first_name} {order.customer.last_name}
|
|
34
|
+
</Typography>
|
|
35
|
+
) : (
|
|
36
|
+
<Typography variant="subtitle1">
|
|
37
|
+
Customer ID: {order.customer_id}
|
|
38
|
+
</Typography>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
<TableContainer component={Paper} sx={{ my: 2 }}>
|
|
42
|
+
<Table>
|
|
43
|
+
<TableHead>
|
|
44
|
+
<TableRow>
|
|
45
|
+
<TableCell>Item ID</TableCell>
|
|
46
|
+
<TableCell>Name</TableCell>
|
|
47
|
+
<TableCell>Description</TableCell>
|
|
48
|
+
<TableCell>Quantity</TableCell>
|
|
49
|
+
<TableCell>Unit Price</TableCell>
|
|
50
|
+
<TableCell>Total Price</TableCell>
|
|
51
|
+
</TableRow>
|
|
52
|
+
</TableHead>
|
|
53
|
+
<TableBody>
|
|
54
|
+
{order.basket.map((item) => (
|
|
55
|
+
<TableRow key={item.item_id}>
|
|
56
|
+
<TableCell>{item.item_id}</TableCell>
|
|
57
|
+
<TableCell>{item.name}</TableCell>
|
|
58
|
+
<TableCell>{item.description || "-"}</TableCell>
|
|
59
|
+
<TableCell>{item.quantity}</TableCell>
|
|
60
|
+
<TableCell>
|
|
61
|
+
{formatter.format(item.unit_price_cents / 100)}
|
|
62
|
+
</TableCell>
|
|
63
|
+
<TableCell>
|
|
64
|
+
{formatter.format(
|
|
65
|
+
(item.quantity * item.unit_price_cents) / 100
|
|
66
|
+
)}
|
|
67
|
+
</TableCell>
|
|
68
|
+
</TableRow>
|
|
69
|
+
))}
|
|
70
|
+
</TableBody>
|
|
71
|
+
</Table>
|
|
72
|
+
</TableContainer>
|
|
73
|
+
|
|
74
|
+
<Typography variant="h6" gutterBottom>
|
|
75
|
+
Subtotal (excl. taxes): {formatter.format(order.total_ex_taxes / 100)}
|
|
76
|
+
</Typography>
|
|
77
|
+
<Typography variant="h6" gutterBottom>
|
|
78
|
+
Taxes: {formatter.format(order.taxes / 100)}
|
|
79
|
+
</Typography>
|
|
80
|
+
<Typography variant="h6" gutterBottom>
|
|
81
|
+
Delivery Fees:{" "}
|
|
82
|
+
{formatter.format(
|
|
83
|
+
order.delivery_fees ? order.delivery_fees / 100 : 0.0
|
|
84
|
+
)}
|
|
85
|
+
</Typography>
|
|
86
|
+
<Typography variant="h5" gutterBottom sx={{ mb: 4 }}>
|
|
87
|
+
Total: {formatter.format(order.total / 100)}
|
|
88
|
+
</Typography>
|
|
89
|
+
</Container>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default Order;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AddCard, type AddCardProps } from "./Billing";
|
|
2
|
+
import Order from "./Order";
|
|
3
|
+
import { postCheckout } from "../utils/api";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Alert, Container, Typography, CircularProgress } from "@mui/material";
|
|
6
|
+
|
|
7
|
+
type CheckoutProps = AddCardProps & {
|
|
8
|
+
order: any;
|
|
9
|
+
apiBaseUrl: string;
|
|
10
|
+
invoiceId?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function Checkout({
|
|
14
|
+
order,
|
|
15
|
+
apiBaseUrl,
|
|
16
|
+
invoiceId,
|
|
17
|
+
}: CheckoutProps) {
|
|
18
|
+
const currentUrl = new URL(window.location.href);
|
|
19
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
20
|
+
const [successData, setSuccessData] = React.useState<any | null>(null);
|
|
21
|
+
const onSubmit = async (values: any) => {
|
|
22
|
+
setIsSubmitting(true);
|
|
23
|
+
const data = await postCheckout(
|
|
24
|
+
apiBaseUrl,
|
|
25
|
+
order.id,
|
|
26
|
+
order.reference,
|
|
27
|
+
values,
|
|
28
|
+
"stripe",
|
|
29
|
+
invoiceId
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (data instanceof Response) {
|
|
33
|
+
alert(JSON.stringify(data));
|
|
34
|
+
} else {
|
|
35
|
+
// success
|
|
36
|
+
setSuccessData(data);
|
|
37
|
+
// Get the current URL
|
|
38
|
+
const currentUrl = new URL(window.location.href);
|
|
39
|
+
// Set the query parameter 'showReceipt' to '1'
|
|
40
|
+
currentUrl.searchParams.set("showReceipt", "1");
|
|
41
|
+
|
|
42
|
+
// Update the browser's URL and history without refreshing the page
|
|
43
|
+
window.history.pushState({}, "", currentUrl);
|
|
44
|
+
}
|
|
45
|
+
setIsSubmitting(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
successData !== null ||
|
|
50
|
+
currentUrl.searchParams.get("showReceipt") ||
|
|
51
|
+
!["pending", "awaiting_payment"].includes(order.status)
|
|
52
|
+
) {
|
|
53
|
+
const print = (
|
|
54
|
+
<a
|
|
55
|
+
href="#"
|
|
56
|
+
onClick={() => {
|
|
57
|
+
window.print();
|
|
58
|
+
return false;
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
print
|
|
62
|
+
</a>
|
|
63
|
+
);
|
|
64
|
+
const text =
|
|
65
|
+
successData !== null ? (
|
|
66
|
+
<>
|
|
67
|
+
Payment received! An email was sent to{" "}
|
|
68
|
+
<strong>{successData.billing_information.email}</strong>. You can also{" "}
|
|
69
|
+
{print} this page for your records.
|
|
70
|
+
</>
|
|
71
|
+
) : (
|
|
72
|
+
<>Payment received! You can {print} this page for your records.</>
|
|
73
|
+
);
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<Order order={order} invoiceId={invoiceId} />
|
|
77
|
+
<Container>
|
|
78
|
+
<Alert severity="success">{text}</Alert>
|
|
79
|
+
</Container>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<Order order={order} invoiceId={invoiceId} />
|
|
86
|
+
{isSubmitting ? (
|
|
87
|
+
<Container>
|
|
88
|
+
<Typography>
|
|
89
|
+
<CircularProgress color="primary" /> Submitting...
|
|
90
|
+
</Typography>
|
|
91
|
+
</Container>
|
|
92
|
+
) : (
|
|
93
|
+
<AddCard contact={order.customer} onSubmit={onSubmit} />
|
|
94
|
+
)}
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
}
|
package/src/index.css
ADDED
package/src/index.ts
CHANGED
|
@@ -8,9 +8,14 @@ import {
|
|
|
8
8
|
import type { Cart, CartProduct } from "./types";
|
|
9
9
|
import AddToCartForm from "./AddToCartForm";
|
|
10
10
|
import { AddCard, AddCardProps } from "./checkout/Billing";
|
|
11
|
+
import Order from "./checkout/Order";
|
|
12
|
+
import Checkout from "./checkout";
|
|
11
13
|
import "react-credit-cards-2/dist/es/styles-compiled.css";
|
|
14
|
+
import "./index.css";
|
|
12
15
|
|
|
13
16
|
export {
|
|
17
|
+
Checkout,
|
|
18
|
+
Order,
|
|
14
19
|
AddCard,
|
|
15
20
|
type AddCardProps,
|
|
16
21
|
cartStore,
|
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function postInit(body: any): RequestInit {
|
|
2
|
+
const init: RequestInit = {
|
|
3
|
+
method: "POST",
|
|
4
|
+
headers: { "Content-Type": "application/json" },
|
|
5
|
+
};
|
|
6
|
+
if (typeof body === "string") {
|
|
7
|
+
return { ...init, body };
|
|
8
|
+
}
|
|
9
|
+
return { ...init, body: JSON.stringify(body) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function dataOrResponse(res: Response) {
|
|
13
|
+
if (res.status === 200) {
|
|
14
|
+
return await res.json();
|
|
15
|
+
} else {
|
|
16
|
+
return res;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function dataOr404(res: Response) {
|
|
21
|
+
if (res.status === 200) {
|
|
22
|
+
return await res.json();
|
|
23
|
+
} else {
|
|
24
|
+
return new Response(null, { status: 404 });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getOrder(
|
|
29
|
+
apiBaseUrl: string,
|
|
30
|
+
id: string,
|
|
31
|
+
orderReference: string
|
|
32
|
+
) {
|
|
33
|
+
const res = await fetch(
|
|
34
|
+
`${apiBaseUrl}/api/ecommerce/orders/${id}/reference/${orderReference}`
|
|
35
|
+
);
|
|
36
|
+
return await dataOr404(res);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getInvoice(
|
|
40
|
+
apiBaseUrl: string,
|
|
41
|
+
id: string,
|
|
42
|
+
orderReference: string
|
|
43
|
+
) {
|
|
44
|
+
const res = await fetch(
|
|
45
|
+
`${apiBaseUrl}/api/ecommerce/invoice/${id}/reference/${orderReference}`
|
|
46
|
+
);
|
|
47
|
+
return await dataOr404(res);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function postCheckout(
|
|
51
|
+
apiBaseUrl: string,
|
|
52
|
+
id: string,
|
|
53
|
+
orderReference: string,
|
|
54
|
+
body: object,
|
|
55
|
+
paymentProvider: string,
|
|
56
|
+
invoiceId?: string
|
|
57
|
+
): Promise<object | Response> {
|
|
58
|
+
const url = new URL(
|
|
59
|
+
`${apiBaseUrl}/api/ecommerce/checkout/${id}/${orderReference}`
|
|
60
|
+
);
|
|
61
|
+
url.searchParams.set("payment_provider", paymentProvider);
|
|
62
|
+
if (invoiceId !== undefined) {
|
|
63
|
+
url.searchParams.set("invoice_id", invoiceId);
|
|
64
|
+
}
|
|
65
|
+
console.log(body);
|
|
66
|
+
const res = await fetch(url, postInit(body));
|
|
67
|
+
return await dataOrResponse(res);
|
|
68
|
+
}
|