@springmicro/cart 0.2.0-alpha.3 → 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.
@@ -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,93 @@
1
+ import { createSvgIcon } from "@mui/material";
2
+ // Source: https://brandfolder.com/s/99gctvbpwgvzbc7mz3j9g4x
3
+
4
+ export const PoweredByStripe = createSvgIcon(
5
+ <svg
6
+ id="Layer_1"
7
+ data-name="Layer 1"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 150 34"
10
+ >
11
+ <defs>
12
+ <style>{`.cls-1{fill: #635bff}`}</style>
13
+ </defs>
14
+ <title>Powered by Stripe - blurple</title>
15
+ <path
16
+ className="cls-1"
17
+ d="M146,0H3.73A3.73,3.73,0,0,0,0,3.73V30.27A3.73,3.73,0,0,0,3.73,34H146a4,4,0,0,0,4-4V4A4,4,0,0,0,146,0Zm3,30a3,3,0,0,1-3,3H3.73A2.74,2.74,0,0,1,1,30.27V3.73A2.74,2.74,0,0,1,3.73,1H146a3,3,0,0,1,3,3Z"
18
+ />
19
+ <path
20
+ className="cls-1"
21
+ d="M17.07,11.24h-4.3V22h1.92V17.84h2.38c2.4,0,3.9-1.16,3.9-3.3S19.47,11.24,17.07,11.24Zm-.1,5H14.69v-3.3H17c1.38,0,2.11.59,2.11,1.65S18.35,16.19,17,16.19Z"
22
+ />
23
+ <path
24
+ className="cls-1"
25
+ d="M25.1,14a3.77,3.77,0,0,0-3.8,4.09,3.81,3.81,0,1,0,7.59,0A3.76,3.76,0,0,0,25.1,14Zm0,6.67c-1.22,0-2-1-2-2.58s.76-2.58,2-2.58,2,1,2,2.58S26.31,20.66,25.1,20.66Z"
26
+ />
27
+ <polygon
28
+ className="cls-1"
29
+ points="36.78 19.35 35.37 14.13 33.89 14.13 32.49 19.35 31.07 14.13 29.22 14.13 31.59 22.01 33.15 22.01 34.59 16.85 36.03 22.01 37.59 22.01 39.96 14.13 38.18 14.13 36.78 19.35"
30
+ />
31
+ <path
32
+ className="cls-1"
33
+ d="M44,14a3.83,3.83,0,0,0-3.75,4.09,3.79,3.79,0,0,0,3.83,4.09A3.47,3.47,0,0,0,47.49,20L46,19.38a1.78,1.78,0,0,1-1.83,1.26A2.12,2.12,0,0,1,42,18.47h5.52v-.6C47.54,15.71,46.32,14,44,14Zm-1.93,3.13A1.92,1.92,0,0,1,44,15.5a1.56,1.56,0,0,1,1.69,1.62Z"
34
+ />
35
+ <path
36
+ className="cls-1"
37
+ d="M50.69,15.3V14.13h-1.8V22h1.8V17.87a1.89,1.89,0,0,1,2-2,4.68,4.68,0,0,1,.66,0v-1.8c-.14,0-.3,0-.51,0A2.29,2.29,0,0,0,50.69,15.3Z"
38
+ />
39
+ <path
40
+ className="cls-1"
41
+ d="M57.48,14a3.83,3.83,0,0,0-3.75,4.09,3.79,3.79,0,0,0,3.83,4.09A3.47,3.47,0,0,0,60.93,20l-1.54-.59a1.78,1.78,0,0,1-1.83,1.26,2.12,2.12,0,0,1-2.1-2.17H61v-.6C61,15.71,59.76,14,57.48,14Zm-1.93,3.13a1.92,1.92,0,0,1,1.92-1.62,1.56,1.56,0,0,1,1.69,1.62Z"
42
+ />
43
+ <path
44
+ className="cls-1"
45
+ d="M67.56,15a2.85,2.85,0,0,0-2.26-1c-2.21,0-3.47,1.85-3.47,4.09s1.26,4.09,3.47,4.09a2.82,2.82,0,0,0,2.26-1V22h1.8V11.24h-1.8Zm0,3.35a2,2,0,0,1-2,2.28c-1.31,0-2-1-2-2.52s.7-2.52,2-2.52c1.11,0,2,.81,2,2.29Z"
46
+ />
47
+ <path
48
+ className="cls-1"
49
+ d="M79.31,14A2.88,2.88,0,0,0,77,15V11.24h-1.8V22H77v-.83a2.86,2.86,0,0,0,2.27,1c2.2,0,3.46-1.86,3.46-4.09S81.51,14,79.31,14ZM79,20.6a2,2,0,0,1-2-2.28v-.47c0-1.48.84-2.29,2-2.29,1.3,0,2,1,2,2.52S80.25,20.6,79,20.6Z"
50
+ />
51
+ <path
52
+ className="cls-1"
53
+ d="M86.93,19.66,85,14.13H83.1L86,21.72l-.3.74a1,1,0,0,1-1.14.79,4.12,4.12,0,0,1-.6,0v1.51a4.62,4.62,0,0,0,.73.05,2.67,2.67,0,0,0,2.78-2l3.24-8.62H88.82Z"
54
+ />
55
+ <path
56
+ className="cls-1"
57
+ d="M125,12.43a3,3,0,0,0-2.13.87l-.14-.69h-2.39V25.53l2.72-.59V21.81a3,3,0,0,0,1.93.7c1.94,0,3.72-1.59,3.72-5.11C128.71,14.18,126.91,12.43,125,12.43Zm-.65,7.63a1.61,1.61,0,0,1-1.28-.52l0-4.11a1.64,1.64,0,0,1,1.3-.55c1,0,1.68,1.13,1.68,2.58S125.36,20.06,124.35,20.06Z"
58
+ />
59
+ <path
60
+ className="cls-1"
61
+ d="M133.73,12.43c-2.62,0-4.21,2.26-4.21,5.11,0,3.37,1.88,5.08,4.56,5.08a6.12,6.12,0,0,0,3-.73V19.64a5.79,5.79,0,0,1-2.7.62c-1.08,0-2-.39-2.14-1.7h5.38c0-.15,0-.74,0-1C137.71,14.69,136.35,12.43,133.73,12.43Zm-1.47,4.07c0-1.26.77-1.79,1.45-1.79s1.4.53,1.4,1.79Z"
62
+ />
63
+ <path
64
+ className="cls-1"
65
+ d="M113,13.36l-.17-.82h-2.32v9.71h2.68V15.67a1.87,1.87,0,0,1,2.05-.58V12.54A1.8,1.8,0,0,0,113,13.36Z"
66
+ />
67
+ <path
68
+ className="cls-1"
69
+ d="M99.46,15.46c0-.44.36-.61.93-.61a5.9,5.9,0,0,1,2.7.72V12.94a7,7,0,0,0-2.7-.51c-2.21,0-3.68,1.18-3.68,3.16,0,3.1,4.14,2.6,4.14,3.93,0,.52-.44.69-1,.69a6.78,6.78,0,0,1-3-.9V22a7.38,7.38,0,0,0,3,.64c2.26,0,3.82-1.15,3.82-3.16C103.62,16.12,99.46,16.72,99.46,15.46Z"
70
+ />
71
+ <path
72
+ className="cls-1"
73
+ d="M107.28,10.24l-2.65.58v8.93a2.77,2.77,0,0,0,2.82,2.87,4.16,4.16,0,0,0,1.91-.37V20c-.35.15-2.06.66-2.06-1V15h2.06V12.66h-2.06Z"
74
+ />
75
+ <polygon
76
+ className="cls-1"
77
+ points="116.25 11.7 118.98 11.13 118.98 8.97 116.25 9.54 116.25 11.7"
78
+ />
79
+ <rect className="cls-1" x="116.25" y="12.61" width="2.73" height="9.64" />
80
+ </svg>,
81
+ "PoweredByStripe"
82
+ );
83
+
84
+ export const StripeLogoLink = ({ height }: { height?: string }) => {
85
+ return (
86
+ <a href="https://stripe.com" target="_blank">
87
+ <PoweredByStripe
88
+ sx={{ width: "unset", height: height || "1em" }}
89
+ viewBox="0 0 150 34"
90
+ />
91
+ </a>
92
+ );
93
+ };
@@ -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
@@ -7,8 +7,17 @@ import {
7
7
  } from "./utils/storage";
8
8
  import type { Cart, CartProduct } from "./types";
9
9
  import AddToCartForm from "./AddToCartForm";
10
+ import { AddCard, AddCardProps } from "./checkout/Billing";
11
+ import Order from "./checkout/Order";
12
+ import Checkout from "./checkout";
13
+ import "react-credit-cards-2/dist/es/styles-compiled.css";
14
+ import "./index.css";
10
15
 
11
16
  export {
17
+ Checkout,
18
+ Order,
19
+ AddCard,
20
+ type AddCardProps,
12
21
  cartStore,
13
22
  CartButton,
14
23
  Cart,
@@ -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
+ }
package/vite.config.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import react from "@vitejs/plugin-react";
2
2
  import { resolve } from "path";
3
3
  import { defineConfig } from "vite";
4
+ import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
4
5
 
5
6
  export default defineConfig({
6
- plugins: [react()],
7
+ plugins: [react(), cssInjectedByJsPlugin()],
7
8
  build: {
8
9
  lib: {
9
10
  entry: resolve(__dirname, "src/index.ts"),
10
- name: "@springmicro/forms",
11
+ name: "@springmicro/cart",
11
12
  fileName: "index",
12
13
  },
13
14
  rollupOptions: {