@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.
- package/dist/index.js +28697 -5795
- package/dist/index.umd.cjs +97 -57
- package/package.json +11 -5
- package/src/checkout/Address.tsx +265 -0
- package/src/checkout/Billing.tsx +346 -0
- package/src/checkout/Order.tsx +93 -0
- package/src/checkout/ProviderLogos.tsx +93 -0
- package/src/checkout/index.tsx +97 -0
- package/src/index.css +5 -0
- package/src/index.ts +9 -0
- package/src/utils/api.ts +68 -0
- package/vite.config.ts +3 -2
|
@@ -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
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,
|
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
|
+
}
|
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/
|
|
11
|
+
name: "@springmicro/cart",
|
|
11
12
|
fileName: "index",
|
|
12
13
|
},
|
|
13
14
|
rollupOptions: {
|