@springmicro/cart 0.3.2 → 0.3.5

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.
Files changed (35) hide show
  1. package/.eslintrc.cjs +21 -21
  2. package/README.md +64 -64
  3. package/dist/index.js +9778 -9696
  4. package/dist/index.umd.cjs +66 -92
  5. package/package.json +5 -3
  6. package/src/AddToCartForm.tsx +16 -16
  7. package/src/CartButton.tsx +249 -249
  8. package/src/ProductCard.css +106 -106
  9. package/src/ProductCard.tsx +165 -165
  10. package/src/checkout/{CheckoutList.css → ReviewCartAndCalculateTaxes.css} +93 -93
  11. package/src/checkout/ReviewCartAndCalculateTaxes.tsx +366 -0
  12. package/src/checkout/components/AddCard.tsx +267 -0
  13. package/src/checkout/components/Address.tsx +261 -265
  14. package/src/checkout/components/CartList.tsx +151 -0
  15. package/src/checkout/components/CartProductCard.css +67 -67
  16. package/src/checkout/components/CartProductCard.tsx +90 -80
  17. package/src/checkout/components/Invoice.tsx +145 -0
  18. package/src/checkout/components/ProviderLogos.tsx +93 -93
  19. package/src/checkout/components/StatusBar.tsx +32 -0
  20. package/src/checkout/index.tsx +161 -0
  21. package/src/index.css +5 -5
  22. package/src/index.ts +36 -35
  23. package/src/types.d.ts +56 -56
  24. package/src/utils/api.ts +67 -67
  25. package/src/utils/cartAuthHandler.ts +50 -50
  26. package/src/utils/index.ts +28 -28
  27. package/src/utils/storage.ts +133 -133
  28. package/tsconfig.json +24 -24
  29. package/tsconfig.node.json +11 -11
  30. package/vite.config.ts +25 -25
  31. package/springmicro-cart-0.2.3.tgz +0 -0
  32. package/src/checkout/CheckoutList.tsx +0 -264
  33. package/src/checkout/components/Billing.tsx +0 -353
  34. package/src/checkout/components/Order.tsx +0 -93
  35. package/src/checkout/components/index.tsx +0 -104
@@ -1,93 +1,93 @@
1
- .two-col {
2
- display: flex;
3
- /* grid-template-columns: 1fr 1fr; */
4
- margin-top: 2rem;
5
- align-items: flex-start;
6
- }
7
-
8
- .two-col > * {
9
- width: 50%;
10
- }
11
-
12
- .checkout-list {
13
- display: flex;
14
- flex-direction: column;
15
- align-items: center;
16
- gap: 1rem;
17
- padding: 8px 4px;
18
- margin-top: 1rem;
19
- max-height: 50vh;
20
- overflow: auto;
21
- }
22
-
23
- @keyframes cardanimate {
24
- 0% {
25
- box-shadow: 0px 2px 5px 2px #dfdfdfff;
26
- }
27
- 50% {
28
- box-shadow: 0px 0px 0px 0px #ffffff00;
29
- }
30
- 100% {
31
- box-shadow: 0px 2px 5px 2px #dfdfdfff;
32
- }
33
- }
34
-
35
- .skeleton-card {
36
- min-height: 318px;
37
- border-radius: 8px;
38
- box-shadow: 0px 2px 5px 2px #dfdfdfff;
39
- animation: cardanimate 1.25s linear infinite;
40
- }
41
-
42
- #status-bar {
43
- display: flex;
44
- flex-direction: row;
45
- gap: 1rem;
46
- align-items: center;
47
- margin-left: 56px;
48
- margin-bottom: 1rem;
49
- margin-top: 2rem;
50
- font-size: 18px;
51
- font-weight: 600;
52
- }
53
-
54
- .status-bar {
55
- width: 200px;
56
- height: 6px;
57
- background: linear-gradient(
58
- 90deg,
59
- rgb(40, 130, 213) 0%,
60
- rgb(40, 130, 213) 50%,
61
- lightgrey 50%,
62
- lightgrey 100%
63
- );
64
- background-position: 100% 0;
65
- transition: color 0.3s 0s; /* active -> inactive */
66
- background-size: 200% 100%;
67
- border-radius: 16px;
68
- }
69
-
70
- .status-bar.active {
71
- background-position: 0 0;
72
- transition: background-position 0.75s; /* inactive -> active */
73
- }
74
-
75
- .status-text {
76
- color: lightgrey;
77
- transition: color 0.3s 0s; /* active -> inactive */
78
- }
79
-
80
- .status-text.active {
81
- color: rgb(40, 130, 213);
82
- transition: color 0.5s 0.65s; /* inactive -> active */
83
- }
84
-
85
- .shopping-button {
86
- color: white;
87
- background-color: rgb(39, 138, 230);
88
- padding: 4px 16px 6px;
89
- border-radius: 8px;
90
- outline: none;
91
- transition: all 0.3s;
92
- text-decoration: none;
93
- }
1
+ .two-col {
2
+ display: flex;
3
+ /* grid-template-columns: 1fr 1fr; */
4
+ margin-top: 2rem;
5
+ align-items: flex-start;
6
+ }
7
+
8
+ .two-col > * {
9
+ width: 50%;
10
+ }
11
+
12
+ .checkout-list {
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ gap: 1rem;
17
+ padding: 8px 4px;
18
+ margin-top: 1rem;
19
+ max-height: 50vh;
20
+ overflow: auto;
21
+ }
22
+
23
+ @keyframes cardanimate {
24
+ 0% {
25
+ box-shadow: 0px 2px 5px 2px #dfdfdfff;
26
+ }
27
+ 50% {
28
+ box-shadow: 0px 0px 0px 0px #ffffff00;
29
+ }
30
+ 100% {
31
+ box-shadow: 0px 2px 5px 2px #dfdfdfff;
32
+ }
33
+ }
34
+
35
+ .skeleton-card {
36
+ min-height: 318px;
37
+ border-radius: 8px;
38
+ box-shadow: 0px 2px 5px 2px #dfdfdfff;
39
+ animation: cardanimate 1.25s linear infinite;
40
+ }
41
+
42
+ #status-bar {
43
+ display: flex;
44
+ flex-direction: row;
45
+ gap: 1rem;
46
+ align-items: center;
47
+ margin-left: -29px;
48
+ margin-bottom: 1rem;
49
+ margin-top: 2rem;
50
+ font-size: 18px;
51
+ font-weight: 600;
52
+ }
53
+
54
+ .status-bar {
55
+ width: 200px;
56
+ height: 6px;
57
+ background: linear-gradient(
58
+ 90deg,
59
+ rgb(40, 130, 213) 0%,
60
+ rgb(40, 130, 213) 50%,
61
+ lightgrey 50%,
62
+ lightgrey 100%
63
+ );
64
+ background-position: 100% 0;
65
+ transition: color 0.3s 0s; /* active -> inactive */
66
+ background-size: 200% 100%;
67
+ border-radius: 16px;
68
+ }
69
+
70
+ .status-bar.active {
71
+ background-position: 0 0;
72
+ transition: background-position 0.75s; /* inactive -> active */
73
+ }
74
+
75
+ .status-text {
76
+ color: lightgrey;
77
+ transition: color 0.3s 0s; /* active -> inactive */
78
+ }
79
+
80
+ .status-text.active {
81
+ color: rgb(40, 130, 213);
82
+ transition: color 0.5s 0.65s; /* inactive -> active */
83
+ }
84
+
85
+ .shopping-button {
86
+ color: white;
87
+ background-color: rgb(39, 138, 230);
88
+ padding: 4px 16px 6px;
89
+ border-radius: 8px;
90
+ outline: none;
91
+ transition: all 0.3s;
92
+ text-decoration: none;
93
+ }
@@ -0,0 +1,366 @@
1
+ import "./ReviewCartAndCalculateTaxes.css";
2
+ import { Box, useTheme, useMediaQuery } from "@mui/material";
3
+ import { setOrder as cartSetOrder } from "../utils/storage";
4
+ import { AddCard, CCFocus, CreditCardValues } from "./components/AddCard";
5
+ import { CartList } from "./components/CartList";
6
+ import {
7
+ ChangeEventHandler,
8
+ FocusEventHandler,
9
+ useEffect,
10
+ useState,
11
+ } from "react";
12
+ import {
13
+ expiryDateHasNotPassed,
14
+ formatCreditCardNumber,
15
+ formatCVC,
16
+ formatExpirationDate,
17
+ splitMonthYear,
18
+ } from "@springmicro/utils/payment";
19
+ import * as Yup from "yup";
20
+ import {
21
+ allCountries,
22
+ allCountriesReverse,
23
+ getPostalCodeDefault,
24
+ } from "@springmicro/utils/address";
25
+ import { AddressValues } from "./components/Address";
26
+ import { useFormik } from "formik";
27
+ import { postCheckout } from "../utils/api";
28
+
29
+ async function createOrder(cart, apiBaseUrl) {
30
+ // If an order has already been created and hasn't been changed, get the previous order.
31
+ if (cart.order && cart.order.id && cart.order.reference) {
32
+ const res = await fetch(
33
+ `${apiBaseUrl}/api/ecommerce/orders/${cart.order.id}/reference/${cart.order.reference}`
34
+ );
35
+ const order = await res.json();
36
+
37
+ const currentUrl = new URL(window.location.href);
38
+ currentUrl.searchParams.set("orderId", order.id);
39
+ currentUrl.searchParams.set("orderRef", order.reference);
40
+ window.history.pushState({}, "", currentUrl);
41
+
42
+ return order;
43
+ }
44
+
45
+ // Otherwise, create a new order.
46
+ const res = await fetch(apiBaseUrl + "/api/ecommerce/orders", {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ },
51
+ body: JSON.stringify({
52
+ customer_id: 1,
53
+ cart_data: {
54
+ items: cart.items.map((li) => ({
55
+ ...li,
56
+ name: undefined,
57
+ image: undefined,
58
+ })),
59
+ },
60
+ }),
61
+ });
62
+ const order = await res.json();
63
+
64
+ if (!order) throw "Missing order";
65
+ cartSetOrder({ id: order.id, reference: order.reference });
66
+ const currentUrl = new URL(window.location.href);
67
+ currentUrl.searchParams.set("orderId", order.id);
68
+ currentUrl.searchParams.set("orderRef", order.reference);
69
+ window.history.pushState({}, "", currentUrl);
70
+
71
+ return order;
72
+ }
73
+
74
+ async function getTax(apiBaseUrl, taxProvider, items, v) {
75
+ const res = await fetch(
76
+ `${apiBaseUrl}/api/ecommerce/tax?tax_provider=${taxProvider ?? "dummy"}`,
77
+ {
78
+ method: "POST",
79
+ body: JSON.stringify({
80
+ cart_data: items,
81
+ address: {
82
+ country: v.country,
83
+ city: v.locality,
84
+ line1: v.street_address,
85
+ postal_code: v.postal_code,
86
+ state: v.region,
87
+ },
88
+ }),
89
+ headers: {
90
+ "Content-Type": "application/json",
91
+ },
92
+ }
93
+ );
94
+ return await res.json();
95
+ }
96
+
97
+ export default function ReviewAndCalculateTaxes({
98
+ subtotal,
99
+ discount,
100
+ taxState,
101
+ shipping,
102
+ statusState,
103
+ cart,
104
+ prices,
105
+ apiBaseUrl,
106
+ orderState,
107
+ disableProductLink,
108
+ disableMissingImage,
109
+ taxProvider,
110
+ setSuccessData,
111
+ invoiceId = undefined,
112
+ onPlacement = undefined,
113
+ }) {
114
+ const [formDisabled, setFormDisabled] = useState(true);
115
+ const [formError, setFormError] = useState(undefined);
116
+ const theme = useTheme();
117
+ const stacked = useMediaQuery(theme.breakpoints.down("lg"));
118
+
119
+ const [status, setStatus] = statusState;
120
+ const [tax, setTax] = taxState;
121
+ const [order, setOrder] = orderState;
122
+
123
+ async function createOrderAndGetTaxes(values) {
124
+ setFormError(undefined);
125
+ // status === 0 when this is clicked.
126
+ // Creates an order and calculates tax.
127
+
128
+ Promise.all([
129
+ createOrder(cart, apiBaseUrl),
130
+ getTax(
131
+ apiBaseUrl,
132
+ taxProvider,
133
+ cart.items.map((i) => ({
134
+ amount: prices[i.price_id].unit_amount,
135
+ reference: i.name,
136
+ })),
137
+ values
138
+ ),
139
+ ])
140
+ .then(([order, taxData]) => {
141
+ setTax(taxData);
142
+ setOrder(order);
143
+ setStatus(1);
144
+ })
145
+ .catch(() => {
146
+ setFormError(true);
147
+ });
148
+ }
149
+
150
+ async function confirmOrder(values) {
151
+ setFormError(undefined);
152
+ setFormDisabled(true);
153
+ postCheckout(
154
+ apiBaseUrl,
155
+ order.id,
156
+ order.reference,
157
+ values,
158
+ "stripe",
159
+ invoiceId
160
+ )
161
+ .then((data) => {
162
+ if (data instanceof Response) {
163
+ setFormError(true);
164
+ alert(JSON.stringify(data));
165
+ } else {
166
+ // success
167
+ setSuccessData(data);
168
+ onPlacement && onPlacement();
169
+ // Get the current URL
170
+ const currentUrl = new URL(window.location.href);
171
+ // Set the query parameter 'showReceipt' to '1'
172
+ currentUrl.searchParams.set("showReceipt", "1");
173
+ currentUrl.searchParams.set("orderId", order.id);
174
+ currentUrl.searchParams.set("orderRef", order.reference);
175
+
176
+ // Update the browser's URL and history without refreshing the page
177
+ window.history.pushState({}, "", currentUrl);
178
+ }
179
+ setFormDisabled(false);
180
+ })
181
+ .catch(() => {
182
+ console.log("ERROR");
183
+ setFormError(true);
184
+ });
185
+ }
186
+
187
+ /**
188
+ *
189
+ * AddCard formik data
190
+ *
191
+ */
192
+
193
+ const contact = undefined;
194
+ const address = true;
195
+
196
+ const initialValuesBase = {
197
+ cvc: "",
198
+ expiry: "",
199
+ focus: "number",
200
+ name: contact ? `${contact.first_name} ${contact.last_name}` : "",
201
+ number: "",
202
+ } as CreditCardValues;
203
+ const initialCountry = "US";
204
+ const initialValuesAddress = {
205
+ country: initialCountry ?? "",
206
+ email: contact?.email ?? "",
207
+ region: "",
208
+ line1: "",
209
+ line2: "",
210
+ organization: contact?.organization ?? "",
211
+ city: "",
212
+ postal_code: getPostalCodeDefault(initialCountry),
213
+ } as AddressValues;
214
+
215
+ const initialValues: CreditCardValues & Partial<AddressValues> = address
216
+ ? { ...initialValuesBase, ...initialValuesAddress }
217
+ : initialValuesBase;
218
+
219
+ const validationSchemaBase = {
220
+ expiry: Yup.string()
221
+ // .length(7, "Expiry date must be of the format MM/YY.")
222
+ .test("exp-date", "Expiry date has already passed.", (string) => {
223
+ if (string?.length === 5) {
224
+ if (string.indexOf("/") !== 2) return false;
225
+ const [month, year] = splitMonthYear(string);
226
+ return expiryDateHasNotPassed(month, year);
227
+ } else {
228
+ return true;
229
+ }
230
+ })
231
+ .required("Expiry date is required."),
232
+ number: Yup.string().required("Card Number is required."),
233
+ name: Yup.string().required("Cardholder Name is required."),
234
+ cvc: Yup.string().required("CVC is required."),
235
+ };
236
+
237
+ const validationSchemaAddress = {
238
+ country: Yup.string().required("Country is required."),
239
+ email: Yup.string().email().required("Email is required."),
240
+ line1: Yup.string().required("Street Address 1 is required."),
241
+ city: Yup.string().required("City is required."),
242
+ postal_code: Yup.string().required("Postal Code is required."),
243
+ };
244
+ const formik = useFormik({
245
+ initialValues,
246
+ validationSchema: Yup.object(
247
+ address
248
+ ? {
249
+ ...validationSchemaBase,
250
+ ...validationSchemaAddress,
251
+ }
252
+ : validationSchemaBase
253
+ ),
254
+ onSubmit: async (v, helpers) => {
255
+ setFormDisabled(true);
256
+ const values = { ...v };
257
+ if (formDisabled) {
258
+ setFormDisabled(false);
259
+ throw new Error("Attempted to submit an invalid form.");
260
+ }
261
+
262
+ const submitFunc = status === 0 ? createOrderAndGetTaxes : confirmOrder;
263
+
264
+ if (submitFunc) {
265
+ if (address) {
266
+ // match backend
267
+ values["street_address"] = values.line2
268
+ ? `${values.line1}\n${values.line2}`
269
+ : values.line1;
270
+ values["locality"] = values.city;
271
+ delete values.line1;
272
+ delete values.line2;
273
+ delete values.city;
274
+ if (Object.keys(allCountriesReverse).includes(values.country)) {
275
+ values.country = allCountriesReverse[values.country];
276
+ }
277
+ }
278
+ values["card_number"] = values.number;
279
+ values["expiry_date"] = values.expiry;
280
+ delete values.focus;
281
+ delete values.expiry;
282
+ delete values.number;
283
+
284
+ // const res = await submitFunc(values);
285
+ submitFunc(values)
286
+ .then((v) => {})
287
+ .finally(() => {
288
+ setFormDisabled(false);
289
+ });
290
+ } else {
291
+ setFormDisabled(false);
292
+ }
293
+ },
294
+ });
295
+
296
+ const handleInputFocus: FocusEventHandler<HTMLInputElement> = (e) => {
297
+ const focusedEl = (e.target as HTMLElement).getAttribute("name") as CCFocus;
298
+ if (focusedEl) {
299
+ formik.setValues({
300
+ ...formik.values,
301
+ focus: focusedEl,
302
+ });
303
+ }
304
+ };
305
+
306
+ const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
307
+ if (e.target.name === "card_number") {
308
+ e.target.value = formatCreditCardNumber(e.target.value);
309
+ } else if (e.target.name === "expiry") {
310
+ e.target.value = formatExpirationDate(e.target.value);
311
+ } else if (e.target.name === "cvc") {
312
+ e.target.value = formatCVC(e.target.value);
313
+ }
314
+ formik.handleChange(e);
315
+ };
316
+
317
+ useEffect(() => {
318
+ setFormDisabled(Object.keys(formik.errors).length > 0);
319
+ }, [formik.errors]);
320
+
321
+ return (
322
+ <Box
323
+ sx={{
324
+ mt: 2,
325
+ display: "flex",
326
+ flexDirection: { xs: "column", xl: "row" },
327
+ justifyContent: "center",
328
+ alignItems: { xs: undefined, xl: "flex-start" },
329
+ px: { xs: 32, xl: 16 },
330
+ "&>div": { width: { xs: undefined, xl: "50%" } },
331
+ }}
332
+ >
333
+ <CartList
334
+ {...{
335
+ status,
336
+ cart,
337
+ subtotal,
338
+ tax,
339
+ shipping,
340
+ discount,
341
+ prices,
342
+ disableMissingImage,
343
+ disableProductLink,
344
+ formik,
345
+ formDisabled,
346
+ formError,
347
+ }}
348
+ />
349
+ {status === 0 && (
350
+ <Box sx={{ flexGrow: 1 }}>
351
+ <AddCard
352
+ onSubmit={createOrderAndGetTaxes}
353
+ stacked={stacked}
354
+ formData={{
355
+ formik,
356
+ handleInputChange,
357
+ handleInputFocus,
358
+ formDisabled,
359
+ formError,
360
+ }}
361
+ />
362
+ </Box>
363
+ )}
364
+ </Box>
365
+ );
366
+ }