@springmicro/cart 0.2.0-alpha.4 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@springmicro/cart",
3
3
  "private": false,
4
- "version": "0.2.0-alpha.4",
4
+ "version": "0.2.1",
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-alpha.4",
26
+ "@springmicro/utils": "0.2.1",
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": "235b44295dc41168e78c6bc31f286ae7c4587a6c"
50
+ "gitHead": "5060bfe477b5be4d15de32991fb8ebc839143430"
50
51
  }
@@ -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,
@@ -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
@@ -0,0 +1,5 @@
1
+ @media print {
2
+ #paymentMethodForm {
3
+ display: none !important;
4
+ }
5
+ }
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,
@@ -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
+ }