@springmicro/cart 0.7.1 → 0.7.3

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.7.1",
4
+ "version": "0.7.3",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -24,7 +24,7 @@
24
24
  "@nanostores/persistent": "^0.10.1",
25
25
  "@nanostores/query": "^0.3.3",
26
26
  "@nanostores/react": "^0.7.2",
27
- "@springmicro/utils": "0.7.1",
27
+ "@springmicro/utils": "0.7.3",
28
28
  "dotenv": "^16.4.5",
29
29
  "nanostores": "^0.10.3",
30
30
  "react": "^18.2.0",
@@ -49,5 +49,5 @@
49
49
  "vite-plugin-css-injected-by-js": "^3.5.1",
50
50
  "yup": "^1.4.0"
51
51
  },
52
- "gitHead": "3fba722cf2e25b298d1b774c2b39ae8ac5743076"
52
+ "gitHead": "bdacd2971dd48131464cdbd4bb570870cee476a7"
53
53
  }
@@ -160,6 +160,7 @@ function CartModal({
160
160
  })
161
161
  )
162
162
  .catch(() => {
163
+ console.error("Couldn't get prices");
163
164
  setPrices(null);
164
165
  });
165
166
  }, [cart]);
@@ -214,17 +215,7 @@ function CartModal({
214
215
  {prices === null
215
216
  ? "Price not found."
216
217
  : price
217
- ? `$${(price.unit_amount / 100).toFixed(2)}${
218
- price.recurring
219
- ? `/${
220
- price.recurring.interval_count > 1
221
- ? `${price.recurring.interval_count} `
222
- : ""
223
- }${price.recurring.interval}${
224
- price.recurring.interval_count > 1 ? "s" : ""
225
- }`
226
- : ""
227
- }`
218
+ ? formatPrice(price)
228
219
  : "Loading price..."}
229
220
  </h3>
230
221
  </Box>
@@ -255,3 +246,28 @@ function CartModal({
255
246
  </Box>
256
247
  );
257
248
  }
249
+
250
+ function formatPrice(price) {
251
+ let baseUnitAmount = (price.unit_amount / 100).toFixed(2);
252
+ if (baseUnitAmount.endsWith("00"))
253
+ baseUnitAmount = baseUnitAmount.replace(".00", "");
254
+
255
+ const unitAmountStr = `$${baseUnitAmount}`;
256
+
257
+ if (!price.recurring) return unitAmountStr;
258
+
259
+ let { interval, interval_count } = price.recurring;
260
+ if (interval === "month" && interval_count % 12 === 0) {
261
+ interval = "year";
262
+ interval_count /= 12;
263
+ }
264
+
265
+ const SLASH = "/";
266
+ let count = "";
267
+ if (interval_count != 1) {
268
+ interval += "s";
269
+ count = `${interval_count} `;
270
+ }
271
+
272
+ return unitAmountStr + SLASH + count + interval;
273
+ }
@@ -166,7 +166,12 @@ function DefaultProductCard({
166
166
 
167
167
  function formatPricing({ unit_amount, recurring }: any) {
168
168
  if (recurring) {
169
- const { interval, interval_count } = recurring;
169
+ let { interval, interval_count } = recurring;
170
+ if (interval === "month" && interval_count % 12 === 0) {
171
+ interval = "year";
172
+ interval_count /= 12;
173
+ }
174
+
170
175
  return `$${(unit_amount / 100).toFixed(2)}/${
171
176
  interval_count === 1 ? interval : `${interval_count} ${interval}s`
172
177
  }`;
@@ -1,5 +1,16 @@
1
1
  import "./ReviewCartAndCalculateTaxes.css";
2
- import { Box, useTheme, useMediaQuery, Button, TextField } from "@mui/material";
2
+ import {
3
+ Box,
4
+ useTheme,
5
+ useMediaQuery,
6
+ Button,
7
+ TextField,
8
+ Modal,
9
+ Backdrop,
10
+ Fade,
11
+ Typography,
12
+ IconButton,
13
+ } from "@mui/material";
3
14
  import { setOrder as cartSetOrder } from "../utils/storage";
4
15
  import { AddCard, CCFocus, CreditCardValues } from "./components/AddCard";
5
16
  import { CartList } from "./components/CartList";
@@ -26,8 +37,9 @@ import { AddressValues } from "./components/Address";
26
37
  import { useFormik } from "formik";
27
38
  import { postCheckout } from "../utils/api";
28
39
  import { CheckoutFieldsType } from ".";
40
+ import CloseIcon from "@mui/icons-material/Close";
29
41
 
30
- async function createOrder(cart, apiBaseUrl, dev) {
42
+ async function createOrder(cart, customer_create, apiBaseUrl, dev) {
31
43
  dev && console.log("Creating order");
32
44
  // If an order has already been created and hasn't been changed, get the previous order.
33
45
  if (cart.order && cart.order.id && cart.order.reference) {
@@ -51,7 +63,7 @@ async function createOrder(cart, apiBaseUrl, dev) {
51
63
  "Content-Type": "application/json",
52
64
  },
53
65
  body: JSON.stringify({
54
- customer_id: 1,
66
+ customer_create,
55
67
  cart_data: {
56
68
  items: cart.items.map((li) => ({
57
69
  ...li,
@@ -60,6 +72,7 @@ async function createOrder(cart, apiBaseUrl, dev) {
60
72
  })),
61
73
  },
62
74
  }),
75
+ credentials: "include",
63
76
  });
64
77
  const order = await res.json();
65
78
 
@@ -134,7 +147,7 @@ export default function ReviewAndCalculateTaxes({
134
147
  dev,
135
148
  hideFields = [],
136
149
  }) {
137
- dev && console.log({ products, prices });
150
+ // dev && console.log({ products, prices });
138
151
  const [formDisabled, setFormDisabled] = useState(true);
139
152
  const [formError, setFormError] = useState(undefined);
140
153
  const theme = useTheme();
@@ -144,6 +157,8 @@ export default function ReviewAndCalculateTaxes({
144
157
  const [tax, setTax] = taxState;
145
158
  const [order, setOrder] = orderState;
146
159
 
160
+ const [precheckErrors, setPrecheckErrors] = useState([]);
161
+
147
162
  async function createOrderAndGetTaxes(values) {
148
163
  setFormError(undefined);
149
164
  setFormDisabled(true);
@@ -151,7 +166,15 @@ export default function ReviewAndCalculateTaxes({
151
166
  // Creates an order and calculates tax.
152
167
  dev && console.log("Cart items", cart.items);
153
168
  Promise.all([
154
- createOrder(cart, apiBaseUrl, dev),
169
+ createOrder(
170
+ cart,
171
+ {
172
+ email: values.email,
173
+ name: values.name,
174
+ },
175
+ apiBaseUrl,
176
+ dev
177
+ ),
155
178
  getTax(
156
179
  apiBaseUrl,
157
180
  taxProvider,
@@ -190,11 +213,15 @@ export default function ReviewAndCalculateTaxes({
190
213
  "stripe",
191
214
  invoiceId
192
215
  )
193
- .then((data) => {
216
+ .then(async (data) => {
194
217
  dev && console.log("Checkout submitted");
195
218
  if (data instanceof Response) {
196
219
  setFormError(true);
197
- dev && alert(data.statusText);
220
+ const { detail: errorData } = await data.json();
221
+ // dev && alert(data.statusText);
222
+ // console.log(errorData);
223
+ // TODO display issues
224
+ if (!errorData.valid) setPrecheckErrors(errorData.errors);
198
225
  } else {
199
226
  // success
200
227
  setSuccessData(data);
@@ -362,61 +389,69 @@ export default function ReviewAndCalculateTaxes({
362
389
  }, [formik.errors]);
363
390
 
364
391
  return (
365
- <Box
366
- sx={{
367
- mt: 2,
368
- display: "flex",
369
- flexDirection: { xs: "column", xl: "row" },
370
- justifyContent: "center",
371
- alignItems: { xs: undefined, xl: "flex-start" },
372
- px: { xs: 32, xl: 16 },
373
- "&>div": { width: { xs: undefined, xl: "50%" } },
374
- }}
375
- >
376
- <CartList
377
- {...{
378
- statusState: [status, setStatus],
379
- cart,
380
- subtotal,
381
- tax,
382
- shipping,
383
- discount,
384
- prices,
385
- products,
386
- disableMissingImage,
387
- disableProductLink,
388
- formik,
389
- formDisabled,
390
- formErrorState: [formError, setFormError],
392
+ <>
393
+ <Box
394
+ sx={{
395
+ mt: 2,
396
+ display: "flex",
397
+ flexDirection: { xs: "column", xl: "row" },
398
+ justifyContent: "center",
399
+ alignItems: { xs: undefined, xl: "flex-start" },
400
+ px: { xs: 32, xl: 16 },
401
+ "&>div": { width: { xs: undefined, xl: "50%" } },
391
402
  }}
392
- />
393
- {status === 0 && (
394
- <Box
395
- sx={{ flexGrow: 1, display: "flex", flexDirection: "column", gap: 2 }}
396
- >
397
- {CollectExtraInfo && <CollectExtraInfo formik={formik} />}
398
- <CheckoutActionFields
399
- formik={formik}
400
- prices={prices}
401
- products={products}
402
- hideFields={hideFields}
403
- />
404
- <AddCard
405
- cardRequired={cardRequired}
406
- onSubmit={createOrderAndGetTaxes}
407
- stacked={stacked}
408
- formData={{
409
- formik,
410
- handleInputChange,
411
- handleInputFocus,
412
- formDisabled,
413
- formError,
403
+ >
404
+ <CartList
405
+ {...{
406
+ statusState: [status, setStatus],
407
+ cart,
408
+ subtotal,
409
+ tax,
410
+ shipping,
411
+ discount,
412
+ prices,
413
+ products,
414
+ disableMissingImage,
415
+ disableProductLink,
416
+ formik,
417
+ formDisabled,
418
+ formErrorState: [formError, setFormError],
419
+ }}
420
+ />
421
+ {status === 0 && (
422
+ <Box
423
+ sx={{
424
+ flexGrow: 1,
425
+ display: "flex",
426
+ flexDirection: "column",
427
+ gap: 2,
414
428
  }}
415
- hideFields={hideFields}
416
- />
417
- </Box>
418
- )}
419
- </Box>
429
+ >
430
+ {CollectExtraInfo && <CollectExtraInfo formik={formik} />}
431
+ <CheckoutActionFields
432
+ formik={formik}
433
+ prices={prices}
434
+ products={products}
435
+ hideFields={hideFields}
436
+ />
437
+ <AddCard
438
+ cardRequired={cardRequired}
439
+ onSubmit={createOrderAndGetTaxes}
440
+ stacked={stacked}
441
+ formData={{
442
+ formik,
443
+ handleInputChange,
444
+ handleInputFocus,
445
+ formDisabled,
446
+ formError,
447
+ }}
448
+ hideFields={hideFields}
449
+ />
450
+ </Box>
451
+ )}
452
+ </Box>
453
+ <PrecheckModal errors={precheckErrors} setErrors={setPrecheckErrors} />
454
+ </>
420
455
  );
421
456
  }
422
457
 
@@ -455,7 +490,14 @@ function CheckoutActionFields({ formik, products, prices, hideFields = [] }) {
455
490
  return (
456
491
  <Box sx={{ display: "flex", flexDirection: "column", mx: 3, gap: 2 }}>
457
492
  {Object.keys(checkoutFields)
458
- .filter((key) => !["email", ...hideFields].includes(key))
493
+ .filter(
494
+ (key) =>
495
+ !["email", ...hideFields].includes(key) &&
496
+ !(
497
+ checkoutFields[key].hidden === true ||
498
+ checkoutFields[key].hidden === "true"
499
+ )
500
+ )
459
501
  .map((key) => {
460
502
  const {
461
503
  name,
@@ -483,3 +525,70 @@ function CheckoutActionFields({ formik, products, prices, hideFields = [] }) {
483
525
  </Box>
484
526
  );
485
527
  }
528
+
529
+ function PrecheckModal({ errors, setErrors }) {
530
+ function onClose() {
531
+ setErrors([]);
532
+ }
533
+ return (
534
+ <Modal
535
+ open={errors.length > 0}
536
+ onClose={onClose} // called when backdrop clicked or escape pressed
537
+ closeAfterTransition
538
+ slots={{ backdrop: Backdrop }}
539
+ slotProps={{
540
+ backdrop: {
541
+ timeout: 300,
542
+ },
543
+ }}
544
+ aria-labelledby="centered-modal-title"
545
+ aria-describedby="centered-modal-description"
546
+ >
547
+ <Fade in={errors.length > 0}>
548
+ <Box
549
+ sx={{
550
+ position: "absolute",
551
+ top: "50%",
552
+ left: "50%",
553
+ transform: "translate(-50%, -50%)",
554
+ minWidth: 300,
555
+ maxWidth: "90vw",
556
+ bgcolor: "background.paper",
557
+ boxShadow: 24,
558
+ borderRadius: 2,
559
+ p: 3,
560
+ outline: "none",
561
+ }}
562
+ >
563
+ <Box
564
+ sx={{
565
+ display: "flex",
566
+ alignItems: "center",
567
+ justifyContent: "space-between",
568
+ mb: 1,
569
+ gap: 4,
570
+ }}
571
+ >
572
+ <Typography id="centered-modal-title" variant="h5">
573
+ Your purchase was unsuccessful.
574
+ </Typography>
575
+ <IconButton aria-label="close" onClick={onClose} size="small">
576
+ <CloseIcon fontSize="small" />
577
+ </IconButton>
578
+ </Box>
579
+
580
+ <Box id="centered-modal-description">
581
+ <Typography variant="h6" component="h2">
582
+ Errors:
583
+ </Typography>
584
+ <ul>
585
+ {errors.map((error) => (
586
+ <Typography component="li">{error}</Typography>
587
+ ))}
588
+ </ul>
589
+ </Box>
590
+ </Box>
591
+ </Fade>
592
+ </Modal>
593
+ );
594
+ }
@@ -138,10 +138,10 @@ export function CartList({
138
138
  alignItems: "center",
139
139
  }}
140
140
  >
141
- <Typography color="red">
141
+ {/* <Typography color="red">
142
142
  Could not confirm payment. Your card info may have been entered
143
143
  incorrectly.
144
- </Typography>
144
+ </Typography> */}
145
145
  <Button
146
146
  onClick={() => {
147
147
  setFormError(undefined);
@@ -13,6 +13,7 @@ export type CheckoutFieldsType = {
13
13
  label?: string; // Defaults to a formated version of the name
14
14
  required?: boolean | string; // Defaults to true
15
15
  type?: "string"; // Defaults to "string" (text)
16
+ hidden?: boolean | string; // Defaults to true
16
17
  };
17
18
 
18
19
  export default function Checkout({
@@ -60,15 +61,20 @@ export default function Checkout({
60
61
  if (oid && or) {
61
62
  fetch(`${apiBaseUrl}/api/ecommerce/orders/${oid}/reference/${or}`).then(
62
63
  async (res) =>
63
- await res.json().then((order) => {
64
- setOrder(order);
65
- setStatus(
66
- order.charge_id ||
67
- !["pending", "awaiting_payment"].includes(order.status)
68
- ? 2
69
- : 0 // normally this should be set to 1 but we need to ensure it sends card data to the payment provider and that data is likely lost on refresh.
70
- );
71
- })
64
+ await res
65
+ .json()
66
+ .then((order) => {
67
+ setOrder(order);
68
+ setStatus(
69
+ order.charge_id ||
70
+ !["pending", "awaiting_payment"].includes(order.status)
71
+ ? 2
72
+ : 0 // normally this should be set to 1 but we need to ensure it sends card data to the payment provider and that data is likely lost on refresh.
73
+ );
74
+ })
75
+ .catch(() => {
76
+ console.error("Failed to get order");
77
+ })
72
78
  );
73
79
  }
74
80
  }, []);
package/src/types.d.ts CHANGED
@@ -15,6 +15,8 @@ export type CartProduct = {
15
15
  quantity?: number;
16
16
  // Used in local cart, delete when sent to api
17
17
  name: string;
18
+ action_data?: object;
19
+ hidden_id?: string;
18
20
  image?: string;
19
21
  };
20
22
 
@@ -37,20 +37,31 @@ function apiCartToLocalCart(cart) {
37
37
  });
38
38
  }
39
39
 
40
+ function jsonOrObj(items) {
41
+ try {
42
+ return JSON.parse(items);
43
+ } catch {
44
+ return items;
45
+ }
46
+ }
47
+
40
48
  apiPathDetails.listen((pathDetails, oldPathDetails) => {
41
49
  const localCartData = JSON.parse(cartStore.get());
42
50
  if (pathDetailsIsFullyDefined(pathDetails)) {
43
51
  // Runs on init if there is a user id and api key. Automatically logs in if userId is updated from undefined.
44
- fetchFromCartApi("GET", pathDetails).then(async ({ status, json }) => {
52
+ fetchFromCartApi("GET", pathDetails).then(async (res) => {
53
+ const { status } = res;
45
54
  if (status === 200) {
46
- const cart = await json();
47
- cartStore.set(
48
- JSON.stringify({
49
- authentication: { loggedIn: true, user_id: cart.user_id },
50
- items: JSON.parse((cart as any).items),
51
- order: cart.order ? JSON.parse((cart as any).order) : undefined,
52
- })
53
- );
55
+ const cart = await res.json();
56
+
57
+ const cartStr = JSON.stringify({
58
+ authentication: { loggedIn: true, user_id: cart.user_id },
59
+ items: jsonOrObj(cart.items),
60
+ order: cart.order ? jsonOrObj(cart.order) : undefined,
61
+ });
62
+
63
+ cartStore.set(cartStr);
64
+ console.log(cartStr);
54
65
  } else if (status === 404 && localCartData.items.length > 0) {
55
66
  fetchFromCartApi(
56
67
  "PUT",
@@ -59,11 +70,19 @@ apiPathDetails.listen((pathDetails, oldPathDetails) => {
59
70
  ).then(async ({ ok, json }) => {
60
71
  if (!ok) return;
61
72
  const cart = await json();
73
+ console.log(
74
+ "Update local cart",
75
+ JSON.stringify({
76
+ authentication: { loggedIn: true, user_id: cart.user_id },
77
+ items: jsonOrObj(cart.items),
78
+ order: cart.order ? jsonOrObj(cart.order) : undefined,
79
+ })
80
+ );
62
81
  cartStore.set(
63
82
  JSON.stringify({
64
83
  authentication: { loggedIn: true, user_id: cart.user_id },
65
- items: JSON.parse((cart as any).items),
66
- order: cart.order ? JSON.parse((cart as any).order) : undefined,
84
+ items: jsonOrObj(cart.items),
85
+ order: cart.order ? jsonOrObj(cart.order) : undefined,
67
86
  })
68
87
  );
69
88
  });
@@ -96,6 +115,7 @@ export function logout(apiBaseUrl: string | undefined) {
96
115
  }
97
116
 
98
117
  export function addToCart(p: CartProduct) {
118
+ console.log("ADD TO CART", p);
99
119
  const cart: Cart = JSON.parse(cartStore.get());
100
120
  const newCart: Cart = {
101
121
  ...cart,
Binary file
Binary file
Binary file