@springmicro/cart 0.3.0 → 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.
- package/.eslintrc.cjs +21 -21
- package/README.md +64 -64
- package/dist/index.js +2327 -2322
- package/dist/index.umd.cjs +61 -61
- package/package.json +5 -3
- package/src/AddToCartForm.tsx +16 -16
- package/src/CartButton.tsx +249 -249
- package/src/ProductCard.css +106 -106
- package/src/ProductCard.tsx +165 -165
- package/src/checkout/CheckoutList.css +93 -93
- package/src/checkout/CheckoutList.tsx +272 -264
- package/src/checkout/components/Address.tsx +265 -265
- package/src/checkout/components/Billing.tsx +353 -353
- package/src/checkout/components/CartProductCard.css +67 -67
- package/src/checkout/components/CartProductCard.tsx +80 -80
- package/src/checkout/components/Order.tsx +95 -93
- package/src/checkout/components/ProviderLogos.tsx +93 -93
- package/src/checkout/components/index.tsx +104 -104
- package/src/index.css +5 -5
- package/src/index.ts +35 -35
- package/src/types.d.ts +56 -56
- package/src/utils/api.ts +67 -67
- package/src/utils/cartAuthHandler.ts +50 -50
- package/src/utils/index.ts +28 -28
- package/src/utils/storage.ts +133 -133
- package/tsconfig.json +24 -24
- package/tsconfig.node.json +11 -11
- package/vite.config.ts +25 -25
- package/springmicro-cart-0.2.3.tgz +0 -0
|
@@ -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
|
+
};
|