@springmicro/cart 0.3.2 → 0.3.4

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,353 +1,353 @@
1
- import { useFormik, FormikProps } from "formik";
2
- import * as Yup from "yup";
3
- import { ChangeEventHandler, useEffect } from "react";
4
- import { PropsWithChildren, useState } from "react";
5
- import {
6
- AddressCityField,
7
- AddressOrganizationNameField,
8
- AddressCountryField,
9
- AddressPostalCodeField,
10
- AddressRegionField,
11
- AddressStreetField,
12
- AddressEmailField,
13
- AddressValues,
14
- } from "./Address";
15
- import {
16
- allCountries,
17
- allCountriesReverse,
18
- getPostalCodeDefault,
19
- } from "@springmicro/utils/address";
20
- import { Box, Button, Container, TextField, Typography } from "@mui/material";
21
- import Cards from "react-credit-cards-2";
22
- import { FocusEventHandler } from "react";
23
- import {
24
- formatCreditCardNumber,
25
- formatCVC,
26
- formatExpirationDate,
27
- splitMonthYear,
28
- expiryDateHasNotPassed,
29
- } from "@springmicro/utils/payment";
30
-
31
- // moved to index.ts
32
- // import "react-credit-cards-2/dist/es/styles-compiled.css";
33
- import { StripeLogoLink } from "./ProviderLogos";
34
-
35
- type CCFocus = "number" | "name" | "cvc" | "expiry";
36
-
37
- export type CreditCardValues = {
38
- cvc: string;
39
- expiry: string;
40
- focus: CCFocus;
41
- name: string;
42
- number: string;
43
- };
44
-
45
- export type AddCardProps = {
46
- contact?: {
47
- organization?: string;
48
- email: string;
49
- first_name: string;
50
- last_name: string;
51
- };
52
- stacked?: boolean;
53
- error?: string;
54
- address?: boolean;
55
- onSubmit?: (
56
- values: CreditCardValues & Partial<AddressValues>
57
- ) => Promise<any>;
58
- };
59
-
60
- export const AddCard = ({
61
- contact,
62
- error,
63
- stacked = false,
64
- address = true,
65
- onSubmit,
66
- }: AddCardProps) => {
67
- const [disabled, setDisabled] = useState(true);
68
-
69
- const initialValuesBase = {
70
- cvc: "",
71
- expiry: "",
72
- focus: "number",
73
- name: contact ? `${contact.first_name} ${contact.last_name}` : "",
74
- number: "",
75
- } as CreditCardValues;
76
- const initialCountry = "US";
77
- const initialValuesAddress = {
78
- country: initialCountry ? allCountries[initialCountry] : "",
79
- email: contact?.email ?? "",
80
- region: "",
81
- line1: "",
82
- line2: "",
83
- organization: contact?.organization ?? "",
84
- city: "",
85
- postal_code: getPostalCodeDefault(initialCountry),
86
- } as AddressValues;
87
-
88
- // const initialValues = { ...initialValuesBase, ...initialValuesAddress }
89
- const initialValues = address
90
- ? { ...initialValuesBase, ...initialValuesAddress }
91
- : initialValuesBase;
92
-
93
- const validationSchemaBase = {
94
- expiry: Yup.string()
95
- // .length(7, "Expiry date must be of the format MM/YY.")
96
- .test("exp-date", "Expiry date has already passed.", (string) => {
97
- if (string?.length === 5) {
98
- if (string.indexOf("/") !== 2) return false;
99
- const [month, year] = splitMonthYear(string);
100
- return expiryDateHasNotPassed(month, year);
101
- } else {
102
- return true;
103
- }
104
- })
105
- .required("Expiry date is required."),
106
- number: Yup.string().required("Card Number is required."),
107
- name: Yup.string().required("Cardholder Name is required."),
108
- cvc: Yup.string().required("CVC is required."),
109
- };
110
-
111
- const validationSchemaAddress = {
112
- country: Yup.string().required("Country is required."),
113
- email: Yup.string().email().required("Email is required."),
114
- line1: Yup.string().required("Street Address 1 is required."),
115
- city: Yup.string().required("City is required."),
116
- postal_code: Yup.string().required("Postal Code is required."),
117
- };
118
-
119
- const formik = useFormik({
120
- initialValues,
121
- validationSchema: Yup.object(
122
- address
123
- ? {
124
- ...validationSchemaBase,
125
- ...validationSchemaAddress,
126
- }
127
- : validationSchemaBase
128
- ),
129
- onSubmit: async (v, helpers) => {
130
- setDisabled(true);
131
- const values = { ...v };
132
- if (disabled) {
133
- setDisabled(false);
134
- throw new Error("Attempted to submit an invalid form.");
135
- }
136
- if (onSubmit) {
137
- if (address) {
138
- // match backend
139
- // @ts-expect-error value exists
140
- values["street_address"] = values.line2
141
- ? // @ts-expect-error value exists
142
- `${values.line1}\n${values.line2}`
143
- : // @ts-expect-error value exists
144
- values.line1;
145
- // @ts-expect-error value exists
146
- values["locality"] = values.city;
147
- // @ts-expect-error value exists
148
- delete values.line1;
149
- // @ts-expect-error value exists
150
- delete values.line2;
151
- // @ts-expect-error value exists
152
- delete values.city;
153
- // @ts-expect-error value exists
154
- if (Object.keys(allCountriesReverse).includes(values.country)) {
155
- // @ts-expect-error value exists
156
- values.country = allCountriesReverse[values.country];
157
- }
158
- }
159
- values["card_number"] = values.number;
160
- values["expiry_date"] = values.expiry;
161
- delete values.focus;
162
- delete values.expiry;
163
- delete values.number;
164
-
165
- const res = await onSubmit(values);
166
- setDisabled(false);
167
- } else {
168
- setDisabled(false);
169
- }
170
- },
171
- });
172
-
173
- const handleInputFocus: FocusEventHandler<HTMLInputElement> = (e) => {
174
- const focusedEl = (e.target as HTMLElement).getAttribute("name") as CCFocus;
175
- if (focusedEl) {
176
- formik.setValues({
177
- ...formik.values,
178
- focus: focusedEl,
179
- });
180
- }
181
- };
182
-
183
- const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
184
- if (e.target.name === "card_number") {
185
- e.target.value = formatCreditCardNumber(e.target.value);
186
- } else if (e.target.name === "expiry") {
187
- e.target.value = formatExpirationDate(e.target.value);
188
- } else if (e.target.name === "cvc") {
189
- e.target.value = formatCVC(e.target.value);
190
- }
191
- formik.handleChange(e);
192
- };
193
-
194
- useEffect(() => {
195
- setDisabled(Object.keys(formik.errors).length > 0);
196
- }, [formik.errors]);
197
-
198
- return (
199
- <Container id="paymentMethodForm">
200
- <form
201
- onSubmit={(e) => {
202
- e.preventDefault();
203
- formik.handleSubmit(e);
204
- }}
205
- >
206
- <Box
207
- sx={
208
- stacked
209
- ? {}
210
- : {
211
- display: "flex",
212
- flexDirection: { xs: "column", sm: "row" },
213
- gap: "1em",
214
- }
215
- }
216
- >
217
- <Cards
218
- {...(formik.values as CreditCardValues)}
219
- focused={formik.values.focus || "number"}
220
- />
221
-
222
- <div>
223
- <TextField
224
- sx={stacked ? { mt: 2 } : {}}
225
- fullWidth
226
- type="tel"
227
- name="number"
228
- label="Card Number"
229
- required
230
- onBlur={formik.handleBlur}
231
- onChange={handleInputChange}
232
- onFocus={handleInputFocus}
233
- variant="outlined"
234
- value={formik.values.number}
235
- error={Boolean(formik.touched.number && formik.errors.number)}
236
- helperText={formik.touched.number && formik.errors.number}
237
- />
238
- <TextField
239
- sx={{ mt: 2 }}
240
- fullWidth
241
- type="text"
242
- name="name"
243
- label="Name on Card"
244
- required
245
- onBlur={formik.handleBlur}
246
- onChange={handleInputChange}
247
- onFocus={handleInputFocus}
248
- variant="outlined"
249
- value={formik.values.name}
250
- error={Boolean(formik.touched.name && formik.errors.name)}
251
- helperText={formik.touched.name && formik.errors.name}
252
- />
253
- <TextField
254
- sx={{ mt: 2, width: "calc(50% - 8px)", mr: 1 }}
255
- type="tel"
256
- name="expiry"
257
- label="Valid Thru"
258
- placeholder="MM/YY"
259
- inputProps={{ pattern: "[0-9]{2}/[0-9]{2}" }}
260
- required
261
- onBlur={formik.handleBlur}
262
- onChange={handleInputChange}
263
- onFocus={handleInputFocus}
264
- variant="outlined"
265
- value={formik.values.expiry}
266
- error={Boolean(formik.touched.expiry && formik.errors.expiry)}
267
- helperText={
268
- (formik.touched.expiry && formik.errors.expiry) || "MM/YY"
269
- }
270
- />
271
- <TextField
272
- sx={{ mt: 2, width: "calc(50% - 8px)", ml: 1 }}
273
- type="tel"
274
- name="cvc"
275
- label="CVC"
276
- required
277
- onBlur={formik.handleBlur}
278
- onChange={handleInputChange}
279
- onFocus={handleInputFocus}
280
- variant="outlined"
281
- value={formik.values.cvc}
282
- error={Boolean(formik.touched.cvc && formik.errors.cvc)}
283
- helperText={formik.touched.cvc && formik.errors.cvc}
284
- />
285
- </div>
286
- {/* {formik.errors.submit && (
287
- <Typography color="error" sx={{ mt: 2 }} variant="body2">
288
- {formik.errors.submit}
289
- </Typography>
290
- )} */}
291
- </Box>
292
- <Box>
293
- {!address ? null : (
294
- <Box>
295
- <Typography variant="h6" sx={{ mt: 2, mb: 3 }}>
296
- Billing Address
297
- </Typography>
298
- <AddressCountryField formik={formik} name="country" />
299
- <AddressEmailField formik={formik} name="email" sx={{ mt: 2 }} />
300
- <AddressOrganizationNameField
301
- sx={{ mt: 2 }}
302
- formik={formik}
303
- name="organization"
304
- />
305
- <AddressStreetField
306
- sx={{ mt: 2 }}
307
- formik={formik}
308
- name="line1"
309
- lineNo="1"
310
- />
311
- <AddressStreetField
312
- sx={{ mt: 2 }}
313
- formik={formik}
314
- name="line2"
315
- lineNo="2"
316
- required={false}
317
- />
318
- <AddressCityField sx={{ mt: 2 }} formik={formik} name="city" />
319
- <AddressRegionField
320
- sx={{ mt: 2 }}
321
- formik={formik}
322
- name="region"
323
- />
324
- <AddressPostalCodeField
325
- sx={{ mt: 2 }}
326
- formik={formik}
327
- name="postal_code"
328
- />
329
- </Box>
330
- )}
331
- </Box>
332
- {error && (
333
- <Typography color="error" sx={{ mt: 2 }} variant="body2">
334
- {error}
335
- </Typography>
336
- )}
337
- <Button
338
- size="large"
339
- sx={{ mt: 3 }}
340
- variant="contained"
341
- // onClick={formik.handleSubmit}
342
- type="submit"
343
- disabled={disabled}
344
- >
345
- Complete and pay
346
- </Button>
347
- <Box sx={{ mt: 1 }}>
348
- <StripeLogoLink />
349
- </Box>
350
- </form>
351
- </Container>
352
- );
353
- };
1
+ import { useFormik, FormikProps } from "formik";
2
+ import * as Yup from "yup";
3
+ import { ChangeEventHandler, useEffect } from "react";
4
+ import { PropsWithChildren, useState } from "react";
5
+ import {
6
+ AddressCityField,
7
+ AddressOrganizationNameField,
8
+ AddressCountryField,
9
+ AddressPostalCodeField,
10
+ AddressRegionField,
11
+ AddressStreetField,
12
+ AddressEmailField,
13
+ AddressValues,
14
+ } from "./Address";
15
+ import {
16
+ allCountries,
17
+ allCountriesReverse,
18
+ getPostalCodeDefault,
19
+ } from "@springmicro/utils/address";
20
+ import { Box, Button, Container, TextField, Typography } from "@mui/material";
21
+ import Cards from "react-credit-cards-2";
22
+ import { FocusEventHandler } from "react";
23
+ import {
24
+ formatCreditCardNumber,
25
+ formatCVC,
26
+ formatExpirationDate,
27
+ splitMonthYear,
28
+ expiryDateHasNotPassed,
29
+ } from "@springmicro/utils/payment";
30
+
31
+ // moved to index.ts
32
+ // import "react-credit-cards-2/dist/es/styles-compiled.css";
33
+ import { StripeLogoLink } from "./ProviderLogos";
34
+
35
+ type CCFocus = "number" | "name" | "cvc" | "expiry";
36
+
37
+ export type CreditCardValues = {
38
+ cvc: string;
39
+ expiry: string;
40
+ focus: CCFocus;
41
+ name: string;
42
+ number: string;
43
+ };
44
+
45
+ export type AddCardProps = {
46
+ contact?: {
47
+ organization?: string;
48
+ email: string;
49
+ first_name: string;
50
+ last_name: string;
51
+ };
52
+ stacked?: boolean;
53
+ error?: string;
54
+ address?: boolean;
55
+ onSubmit?: (
56
+ values: CreditCardValues & Partial<AddressValues>
57
+ ) => Promise<any>;
58
+ };
59
+
60
+ export const AddCard = ({
61
+ contact,
62
+ error,
63
+ stacked = false,
64
+ address = true,
65
+ onSubmit,
66
+ }: AddCardProps) => {
67
+ const [disabled, setDisabled] = useState(true);
68
+
69
+ const initialValuesBase = {
70
+ cvc: "",
71
+ expiry: "",
72
+ focus: "number",
73
+ name: contact ? `${contact.first_name} ${contact.last_name}` : "",
74
+ number: "",
75
+ } as CreditCardValues;
76
+ const initialCountry = "US";
77
+ const initialValuesAddress = {
78
+ country: initialCountry ? allCountries[initialCountry] : "",
79
+ email: contact?.email ?? "",
80
+ region: "",
81
+ line1: "",
82
+ line2: "",
83
+ organization: contact?.organization ?? "",
84
+ city: "",
85
+ postal_code: getPostalCodeDefault(initialCountry),
86
+ } as AddressValues;
87
+
88
+ // const initialValues = { ...initialValuesBase, ...initialValuesAddress }
89
+ const initialValues = address
90
+ ? { ...initialValuesBase, ...initialValuesAddress }
91
+ : initialValuesBase;
92
+
93
+ const validationSchemaBase = {
94
+ expiry: Yup.string()
95
+ // .length(7, "Expiry date must be of the format MM/YY.")
96
+ .test("exp-date", "Expiry date has already passed.", (string) => {
97
+ if (string?.length === 5) {
98
+ if (string.indexOf("/") !== 2) return false;
99
+ const [month, year] = splitMonthYear(string);
100
+ return expiryDateHasNotPassed(month, year);
101
+ } else {
102
+ return true;
103
+ }
104
+ })
105
+ .required("Expiry date is required."),
106
+ number: Yup.string().required("Card Number is required."),
107
+ name: Yup.string().required("Cardholder Name is required."),
108
+ cvc: Yup.string().required("CVC is required."),
109
+ };
110
+
111
+ const validationSchemaAddress = {
112
+ country: Yup.string().required("Country is required."),
113
+ email: Yup.string().email().required("Email is required."),
114
+ line1: Yup.string().required("Street Address 1 is required."),
115
+ city: Yup.string().required("City is required."),
116
+ postal_code: Yup.string().required("Postal Code is required."),
117
+ };
118
+
119
+ const formik = useFormik({
120
+ initialValues,
121
+ validationSchema: Yup.object(
122
+ address
123
+ ? {
124
+ ...validationSchemaBase,
125
+ ...validationSchemaAddress,
126
+ }
127
+ : validationSchemaBase
128
+ ),
129
+ onSubmit: async (v, helpers) => {
130
+ setDisabled(true);
131
+ const values = { ...v };
132
+ if (disabled) {
133
+ setDisabled(false);
134
+ throw new Error("Attempted to submit an invalid form.");
135
+ }
136
+ if (onSubmit) {
137
+ if (address) {
138
+ // match backend
139
+ // @ts-expect-error value exists
140
+ values["street_address"] = values.line2
141
+ ? // @ts-expect-error value exists
142
+ `${values.line1}\n${values.line2}`
143
+ : // @ts-expect-error value exists
144
+ values.line1;
145
+ // @ts-expect-error value exists
146
+ values["locality"] = values.city;
147
+ // @ts-expect-error value exists
148
+ delete values.line1;
149
+ // @ts-expect-error value exists
150
+ delete values.line2;
151
+ // @ts-expect-error value exists
152
+ delete values.city;
153
+ // @ts-expect-error value exists
154
+ if (Object.keys(allCountriesReverse).includes(values.country)) {
155
+ // @ts-expect-error value exists
156
+ values.country = allCountriesReverse[values.country];
157
+ }
158
+ }
159
+ values["card_number"] = values.number;
160
+ values["expiry_date"] = values.expiry;
161
+ delete values.focus;
162
+ delete values.expiry;
163
+ delete values.number;
164
+
165
+ const res = await onSubmit(values);
166
+ setDisabled(false);
167
+ } else {
168
+ setDisabled(false);
169
+ }
170
+ },
171
+ });
172
+
173
+ const handleInputFocus: FocusEventHandler<HTMLInputElement> = (e) => {
174
+ const focusedEl = (e.target as HTMLElement).getAttribute("name") as CCFocus;
175
+ if (focusedEl) {
176
+ formik.setValues({
177
+ ...formik.values,
178
+ focus: focusedEl,
179
+ });
180
+ }
181
+ };
182
+
183
+ const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
184
+ if (e.target.name === "card_number") {
185
+ e.target.value = formatCreditCardNumber(e.target.value);
186
+ } else if (e.target.name === "expiry") {
187
+ e.target.value = formatExpirationDate(e.target.value);
188
+ } else if (e.target.name === "cvc") {
189
+ e.target.value = formatCVC(e.target.value);
190
+ }
191
+ formik.handleChange(e);
192
+ };
193
+
194
+ useEffect(() => {
195
+ setDisabled(Object.keys(formik.errors).length > 0);
196
+ }, [formik.errors]);
197
+
198
+ return (
199
+ <Container id="paymentMethodForm">
200
+ <form
201
+ onSubmit={(e) => {
202
+ e.preventDefault();
203
+ formik.handleSubmit(e);
204
+ }}
205
+ >
206
+ <Box
207
+ sx={
208
+ stacked
209
+ ? {}
210
+ : {
211
+ display: "flex",
212
+ flexDirection: { xs: "column", sm: "row" },
213
+ gap: "1em",
214
+ }
215
+ }
216
+ >
217
+ <Cards
218
+ {...(formik.values as CreditCardValues)}
219
+ focused={formik.values.focus || "number"}
220
+ />
221
+
222
+ <div>
223
+ <TextField
224
+ sx={stacked ? { mt: 2 } : {}}
225
+ fullWidth
226
+ type="tel"
227
+ name="number"
228
+ label="Card Number"
229
+ required
230
+ onBlur={formik.handleBlur}
231
+ onChange={handleInputChange}
232
+ onFocus={handleInputFocus}
233
+ variant="outlined"
234
+ value={formik.values.number}
235
+ error={Boolean(formik.touched.number && formik.errors.number)}
236
+ helperText={formik.touched.number && formik.errors.number}
237
+ />
238
+ <TextField
239
+ sx={{ mt: 2 }}
240
+ fullWidth
241
+ type="text"
242
+ name="name"
243
+ label="Name on Card"
244
+ required
245
+ onBlur={formik.handleBlur}
246
+ onChange={handleInputChange}
247
+ onFocus={handleInputFocus}
248
+ variant="outlined"
249
+ value={formik.values.name}
250
+ error={Boolean(formik.touched.name && formik.errors.name)}
251
+ helperText={formik.touched.name && formik.errors.name}
252
+ />
253
+ <TextField
254
+ sx={{ mt: 2, width: "calc(50% - 8px)", mr: 1 }}
255
+ type="tel"
256
+ name="expiry"
257
+ label="Valid Thru"
258
+ placeholder="MM/YY"
259
+ inputProps={{ pattern: "[0-9]{2}/[0-9]{2}" }}
260
+ required
261
+ onBlur={formik.handleBlur}
262
+ onChange={handleInputChange}
263
+ onFocus={handleInputFocus}
264
+ variant="outlined"
265
+ value={formik.values.expiry}
266
+ error={Boolean(formik.touched.expiry && formik.errors.expiry)}
267
+ helperText={
268
+ (formik.touched.expiry && formik.errors.expiry) || "MM/YY"
269
+ }
270
+ />
271
+ <TextField
272
+ sx={{ mt: 2, width: "calc(50% - 8px)", ml: 1 }}
273
+ type="tel"
274
+ name="cvc"
275
+ label="CVC"
276
+ required
277
+ onBlur={formik.handleBlur}
278
+ onChange={handleInputChange}
279
+ onFocus={handleInputFocus}
280
+ variant="outlined"
281
+ value={formik.values.cvc}
282
+ error={Boolean(formik.touched.cvc && formik.errors.cvc)}
283
+ helperText={formik.touched.cvc && formik.errors.cvc}
284
+ />
285
+ </div>
286
+ {/* {formik.errors.submit && (
287
+ <Typography color="error" sx={{ mt: 2 }} variant="body2">
288
+ {formik.errors.submit}
289
+ </Typography>
290
+ )} */}
291
+ </Box>
292
+ <Box>
293
+ {!address ? null : (
294
+ <Box>
295
+ <Typography variant="h6" sx={{ mt: 2, mb: 3 }}>
296
+ Billing Address
297
+ </Typography>
298
+ <AddressCountryField formik={formik} name="country" />
299
+ <AddressEmailField formik={formik} name="email" sx={{ mt: 2 }} />
300
+ <AddressOrganizationNameField
301
+ sx={{ mt: 2 }}
302
+ formik={formik}
303
+ name="organization"
304
+ />
305
+ <AddressStreetField
306
+ sx={{ mt: 2 }}
307
+ formik={formik}
308
+ name="line1"
309
+ lineNo="1"
310
+ />
311
+ <AddressStreetField
312
+ sx={{ mt: 2 }}
313
+ formik={formik}
314
+ name="line2"
315
+ lineNo="2"
316
+ required={false}
317
+ />
318
+ <AddressCityField sx={{ mt: 2 }} formik={formik} name="city" />
319
+ <AddressRegionField
320
+ sx={{ mt: 2 }}
321
+ formik={formik}
322
+ name="region"
323
+ />
324
+ <AddressPostalCodeField
325
+ sx={{ mt: 2 }}
326
+ formik={formik}
327
+ name="postal_code"
328
+ />
329
+ </Box>
330
+ )}
331
+ </Box>
332
+ {error && (
333
+ <Typography color="error" sx={{ mt: 2 }} variant="body2">
334
+ {error}
335
+ </Typography>
336
+ )}
337
+ <Button
338
+ size="large"
339
+ sx={{ mt: 3 }}
340
+ variant="contained"
341
+ // onClick={formik.handleSubmit}
342
+ type="submit"
343
+ disabled={disabled}
344
+ >
345
+ Complete and pay
346
+ </Button>
347
+ <Box sx={{ mt: 1 }}>
348
+ <StripeLogoLink />
349
+ </Box>
350
+ </form>
351
+ </Container>
352
+ );
353
+ };