@springmicro/cart 0.5.1 → 0.5.2

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.
@@ -1,370 +1,370 @@
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
- CollectExtraInfo = undefined,
114
- }) {
115
- const [formDisabled, setFormDisabled] = useState(true);
116
- const [formError, setFormError] = useState(undefined);
117
- const theme = useTheme();
118
- const stacked = useMediaQuery(theme.breakpoints.down("lg"));
119
-
120
- const [status, setStatus] = statusState;
121
- const [tax, setTax] = taxState;
122
- const [order, setOrder] = orderState;
123
-
124
- async function createOrderAndGetTaxes(values) {
125
- setFormError(undefined);
126
- // status === 0 when this is clicked.
127
- // Creates an order and calculates tax.
128
-
129
- Promise.all([
130
- createOrder(cart, apiBaseUrl),
131
- getTax(
132
- apiBaseUrl,
133
- taxProvider,
134
- cart.items.map((i) => ({
135
- amount: prices[i.price_id].unit_amount,
136
- reference: i.name,
137
- })),
138
- values
139
- ),
140
- ])
141
- .then(([order, taxData]) => {
142
- setTax(taxData);
143
- setOrder(order);
144
- setStatus(1);
145
- })
146
- .catch(() => {
147
- setFormError(true);
148
- });
149
- }
150
-
151
- async function confirmOrder(values) {
152
- setFormError(undefined);
153
- setFormDisabled(true);
154
- postCheckout(
155
- apiBaseUrl,
156
- order.id,
157
- order.reference,
158
- values,
159
- "stripe",
160
- invoiceId
161
- )
162
- .then((data) => {
163
- if (data instanceof Response) {
164
- setFormError(true);
165
- alert(JSON.stringify(data));
166
- } else {
167
- // success
168
- setSuccessData(data);
169
- onPlacement && onPlacement();
170
- // Get the current URL
171
- const currentUrl = new URL(window.location.href);
172
- // Set the query parameter 'showReceipt' to '1'
173
- currentUrl.searchParams.set("showReceipt", "1");
174
- currentUrl.searchParams.set("orderId", order.id);
175
- currentUrl.searchParams.set("orderRef", order.reference);
176
-
177
- // Update the browser's URL and history without refreshing the page
178
- window.history.pushState({}, "", currentUrl);
179
- }
180
- setFormDisabled(false);
181
- })
182
- .catch(() => {
183
- console.log("ERROR");
184
- setFormError(true);
185
- });
186
- }
187
-
188
- /**
189
- *
190
- * AddCard formik data
191
- *
192
- */
193
-
194
- const contact = undefined;
195
- const address = true;
196
-
197
- const initialValuesBase = {
198
- cvc: "",
199
- expiry: "",
200
- focus: "number",
201
- name: contact ? `${contact.first_name} ${contact.last_name}` : "",
202
- number: "",
203
- } as CreditCardValues;
204
- const initialCountry = "US";
205
- const initialValuesAddress = {
206
- country: initialCountry ?? "",
207
- email: contact?.email ?? "",
208
- region: "",
209
- line1: "",
210
- line2: "",
211
- organization: contact?.organization ?? "",
212
- city: "",
213
- postal_code: getPostalCodeDefault(initialCountry),
214
- } as AddressValues;
215
-
216
- const initialValues: CreditCardValues & Partial<AddressValues> = address
217
- ? { ...initialValuesBase, ...initialValuesAddress }
218
- : initialValuesBase;
219
-
220
- const validationSchemaBase = {
221
- expiry: Yup.string()
222
- // .length(7, "Expiry date must be of the format MM/YY.")
223
- .test("exp-date", "Expiry date has already passed.", (string) => {
224
- if (string?.length === 5) {
225
- if (string.indexOf("/") !== 2) return false;
226
- const [month, year] = splitMonthYear(string);
227
- return expiryDateHasNotPassed(month, year);
228
- } else {
229
- return true;
230
- }
231
- })
232
- .required("Expiry date is required."),
233
- number: Yup.string().required("Card Number is required."),
234
- name: Yup.string().required("Cardholder Name is required."),
235
- cvc: Yup.string().required("CVC is required."),
236
- };
237
-
238
- const validationSchemaAddress = {
239
- country: Yup.string().required("Country is required."),
240
- email: Yup.string().email().required("Email is required."),
241
- line1: Yup.string().required("Street Address 1 is required."),
242
- city: Yup.string().required("City is required."),
243
- postal_code: Yup.string().required("Postal Code is required."),
244
- };
245
- const formik = useFormik({
246
- initialValues,
247
- validationSchema: Yup.object(
248
- address
249
- ? {
250
- ...validationSchemaBase,
251
- ...validationSchemaAddress,
252
- }
253
- : validationSchemaBase
254
- ),
255
- onSubmit: async (v, helpers) => {
256
- setFormDisabled(true);
257
- const values = { ...v };
258
- if (formDisabled) {
259
- setFormDisabled(false);
260
- throw new Error("Attempted to submit an invalid form.");
261
- }
262
-
263
- const submitFunc = status === 0 ? createOrderAndGetTaxes : confirmOrder;
264
-
265
- if (submitFunc) {
266
- if (address) {
267
- // match backend
268
- values["street_address"] = values.line2
269
- ? `${values.line1}\n${values.line2}`
270
- : values.line1;
271
- values["locality"] = values.city;
272
- delete values.line1;
273
- delete values.line2;
274
- delete values.city;
275
- if (Object.keys(allCountriesReverse).includes(values.country)) {
276
- values.country = allCountriesReverse[values.country];
277
- }
278
- }
279
- values["card_number"] = values.number;
280
- values["expiry_date"] = values.expiry;
281
- delete values.focus;
282
- delete values.expiry;
283
- delete values.number;
284
-
285
- // const res = await submitFunc(values);
286
- submitFunc(values)
287
- .then((v) => {})
288
- .finally(() => {
289
- setFormDisabled(false);
290
- });
291
- } else {
292
- setFormDisabled(false);
293
- }
294
- },
295
- });
296
-
297
- const handleInputFocus: FocusEventHandler<HTMLInputElement> = (e) => {
298
- const focusedEl = (e.target as HTMLElement).getAttribute("name") as CCFocus;
299
- if (focusedEl) {
300
- formik.setValues({
301
- ...formik.values,
302
- focus: focusedEl,
303
- });
304
- }
305
- };
306
-
307
- const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
308
- if (e.target.name === "card_number") {
309
- e.target.value = formatCreditCardNumber(e.target.value);
310
- } else if (e.target.name === "expiry") {
311
- e.target.value = formatExpirationDate(e.target.value);
312
- } else if (e.target.name === "cvc") {
313
- e.target.value = formatCVC(e.target.value);
314
- }
315
- formik.handleChange(e);
316
- };
317
-
318
- useEffect(() => {
319
- setFormDisabled(Object.keys(formik.errors).length > 0);
320
- }, [formik.errors]);
321
-
322
- return (
323
- <Box
324
- sx={{
325
- mt: 2,
326
- display: "flex",
327
- flexDirection: { xs: "column", xl: "row" },
328
- justifyContent: "center",
329
- alignItems: { xs: undefined, xl: "flex-start" },
330
- px: { xs: 32, xl: 16 },
331
- "&>div": { width: { xs: undefined, xl: "50%" } },
332
- }}
333
- >
334
- <CartList
335
- {...{
336
- status,
337
- cart,
338
- subtotal,
339
- tax,
340
- shipping,
341
- discount,
342
- prices,
343
- disableMissingImage,
344
- disableProductLink,
345
- formik,
346
- formDisabled,
347
- formError,
348
- }}
349
- />
350
- {status === 0 && (
351
- <Box
352
- sx={{ flexGrow: 1, display: "flex", flexDirection: "column", gap: 2 }}
353
- >
354
- {CollectExtraInfo && <CollectExtraInfo formik={formik} />}
355
- <AddCard
356
- onSubmit={createOrderAndGetTaxes}
357
- stacked={stacked}
358
- formData={{
359
- formik,
360
- handleInputChange,
361
- handleInputFocus,
362
- formDisabled,
363
- formError,
364
- }}
365
- />
366
- </Box>
367
- )}
368
- </Box>
369
- );
370
- }
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
+ CollectExtraInfo = undefined,
114
+ }) {
115
+ const [formDisabled, setFormDisabled] = useState(true);
116
+ const [formError, setFormError] = useState(undefined);
117
+ const theme = useTheme();
118
+ const stacked = useMediaQuery(theme.breakpoints.down("lg"));
119
+
120
+ const [status, setStatus] = statusState;
121
+ const [tax, setTax] = taxState;
122
+ const [order, setOrder] = orderState;
123
+
124
+ async function createOrderAndGetTaxes(values) {
125
+ setFormError(undefined);
126
+ // status === 0 when this is clicked.
127
+ // Creates an order and calculates tax.
128
+
129
+ Promise.all([
130
+ createOrder(cart, apiBaseUrl),
131
+ getTax(
132
+ apiBaseUrl,
133
+ taxProvider,
134
+ cart.items.map((i) => ({
135
+ amount: prices[i.price_id].unit_amount,
136
+ reference: i.name,
137
+ })),
138
+ values
139
+ ),
140
+ ])
141
+ .then(([order, taxData]) => {
142
+ setTax(taxData);
143
+ setOrder(order);
144
+ setStatus(1);
145
+ })
146
+ .catch(() => {
147
+ setFormError(true);
148
+ });
149
+ }
150
+
151
+ async function confirmOrder(values) {
152
+ setFormError(undefined);
153
+ setFormDisabled(true);
154
+ postCheckout(
155
+ apiBaseUrl,
156
+ order.id,
157
+ order.reference,
158
+ values,
159
+ "stripe",
160
+ invoiceId
161
+ )
162
+ .then((data) => {
163
+ if (data instanceof Response) {
164
+ setFormError(true);
165
+ alert(JSON.stringify(data));
166
+ } else {
167
+ // success
168
+ setSuccessData(data);
169
+ onPlacement && onPlacement();
170
+ // Get the current URL
171
+ const currentUrl = new URL(window.location.href);
172
+ // Set the query parameter 'showReceipt' to '1'
173
+ currentUrl.searchParams.set("showReceipt", "1");
174
+ currentUrl.searchParams.set("orderId", order.id);
175
+ currentUrl.searchParams.set("orderRef", order.reference);
176
+
177
+ // Update the browser's URL and history without refreshing the page
178
+ window.history.pushState({}, "", currentUrl);
179
+ }
180
+ setFormDisabled(false);
181
+ })
182
+ .catch(() => {
183
+ console.log("ERROR");
184
+ setFormError(true);
185
+ });
186
+ }
187
+
188
+ /**
189
+ *
190
+ * AddCard formik data
191
+ *
192
+ */
193
+
194
+ const contact = undefined;
195
+ const address = true;
196
+
197
+ const initialValuesBase = {
198
+ cvc: "",
199
+ expiry: "",
200
+ focus: "number",
201
+ name: contact ? `${contact.first_name} ${contact.last_name}` : "",
202
+ number: "",
203
+ } as CreditCardValues;
204
+ const initialCountry = "US";
205
+ const initialValuesAddress = {
206
+ country: initialCountry ?? "",
207
+ email: contact?.email ?? "",
208
+ region: "",
209
+ line1: "",
210
+ line2: "",
211
+ organization: contact?.organization ?? "",
212
+ city: "",
213
+ postal_code: getPostalCodeDefault(initialCountry),
214
+ } as AddressValues;
215
+
216
+ const initialValues: CreditCardValues & Partial<AddressValues> = address
217
+ ? { ...initialValuesBase, ...initialValuesAddress }
218
+ : initialValuesBase;
219
+
220
+ const validationSchemaBase = {
221
+ expiry: Yup.string()
222
+ // .length(7, "Expiry date must be of the format MM/YY.")
223
+ .test("exp-date", "Expiry date has already passed.", (string) => {
224
+ if (string?.length === 5) {
225
+ if (string.indexOf("/") !== 2) return false;
226
+ const [month, year] = splitMonthYear(string);
227
+ return expiryDateHasNotPassed(month, year);
228
+ } else {
229
+ return true;
230
+ }
231
+ })
232
+ .required("Expiry date is required."),
233
+ number: Yup.string().required("Card Number is required."),
234
+ name: Yup.string().required("Cardholder Name is required."),
235
+ cvc: Yup.string().required("CVC is required."),
236
+ };
237
+
238
+ const validationSchemaAddress = {
239
+ country: Yup.string().required("Country is required."),
240
+ email: Yup.string().email().required("Email is required."),
241
+ line1: Yup.string().required("Street Address 1 is required."),
242
+ city: Yup.string().required("City is required."),
243
+ postal_code: Yup.string().required("Postal Code is required."),
244
+ };
245
+ const formik = useFormik({
246
+ initialValues,
247
+ validationSchema: Yup.object(
248
+ address
249
+ ? {
250
+ ...validationSchemaBase,
251
+ ...validationSchemaAddress,
252
+ }
253
+ : validationSchemaBase
254
+ ),
255
+ onSubmit: async (v, helpers) => {
256
+ setFormDisabled(true);
257
+ const values = { ...v };
258
+ if (formDisabled) {
259
+ setFormDisabled(false);
260
+ throw new Error("Attempted to submit an invalid form.");
261
+ }
262
+
263
+ const submitFunc = status === 0 ? createOrderAndGetTaxes : confirmOrder;
264
+
265
+ if (submitFunc) {
266
+ if (address) {
267
+ // match backend
268
+ values["street_address"] = values.line2
269
+ ? `${values.line1}\n${values.line2}`
270
+ : values.line1;
271
+ values["locality"] = values.city;
272
+ delete values.line1;
273
+ delete values.line2;
274
+ delete values.city;
275
+ if (Object.keys(allCountriesReverse).includes(values.country)) {
276
+ values.country = allCountriesReverse[values.country];
277
+ }
278
+ }
279
+ values["card_number"] = values.number;
280
+ values["expiry_date"] = values.expiry;
281
+ delete values.focus;
282
+ delete values.expiry;
283
+ delete values.number;
284
+
285
+ // const res = await submitFunc(values);
286
+ submitFunc(values)
287
+ .then((v) => {})
288
+ .finally(() => {
289
+ setFormDisabled(false);
290
+ });
291
+ } else {
292
+ setFormDisabled(false);
293
+ }
294
+ },
295
+ });
296
+
297
+ const handleInputFocus: FocusEventHandler<HTMLInputElement> = (e) => {
298
+ const focusedEl = (e.target as HTMLElement).getAttribute("name") as CCFocus;
299
+ if (focusedEl) {
300
+ formik.setValues({
301
+ ...formik.values,
302
+ focus: focusedEl,
303
+ });
304
+ }
305
+ };
306
+
307
+ const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
308
+ if (e.target.name === "card_number") {
309
+ e.target.value = formatCreditCardNumber(e.target.value);
310
+ } else if (e.target.name === "expiry") {
311
+ e.target.value = formatExpirationDate(e.target.value);
312
+ } else if (e.target.name === "cvc") {
313
+ e.target.value = formatCVC(e.target.value);
314
+ }
315
+ formik.handleChange(e);
316
+ };
317
+
318
+ useEffect(() => {
319
+ setFormDisabled(Object.keys(formik.errors).length > 0);
320
+ }, [formik.errors]);
321
+
322
+ return (
323
+ <Box
324
+ sx={{
325
+ mt: 2,
326
+ display: "flex",
327
+ flexDirection: { xs: "column", xl: "row" },
328
+ justifyContent: "center",
329
+ alignItems: { xs: undefined, xl: "flex-start" },
330
+ px: { xs: 32, xl: 16 },
331
+ "&>div": { width: { xs: undefined, xl: "50%" } },
332
+ }}
333
+ >
334
+ <CartList
335
+ {...{
336
+ status,
337
+ cart,
338
+ subtotal,
339
+ tax,
340
+ shipping,
341
+ discount,
342
+ prices,
343
+ disableMissingImage,
344
+ disableProductLink,
345
+ formik,
346
+ formDisabled,
347
+ formError,
348
+ }}
349
+ />
350
+ {status === 0 && (
351
+ <Box
352
+ sx={{ flexGrow: 1, display: "flex", flexDirection: "column", gap: 2 }}
353
+ >
354
+ {CollectExtraInfo && <CollectExtraInfo formik={formik} />}
355
+ <AddCard
356
+ onSubmit={createOrderAndGetTaxes}
357
+ stacked={stacked}
358
+ formData={{
359
+ formik,
360
+ handleInputChange,
361
+ handleInputFocus,
362
+ formDisabled,
363
+ formError,
364
+ }}
365
+ />
366
+ </Box>
367
+ )}
368
+ </Box>
369
+ );
370
+ }