@springmicro/cart 0.2.0-alpha.2 → 0.2.0-alpha.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/dist/index.js +27053 -5428
- package/dist/index.umd.cjs +74 -48
- package/package.json +9 -4
- package/src/checkout/Address.tsx +246 -0
- package/src/checkout/Billing.tsx +327 -0
- package/src/checkout/ProviderLogos.tsx +93 -0
- package/src/index.ts +4 -0
- package/vite.config.ts +3 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springmicro/cart",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.0-alpha.
|
|
4
|
+
"version": "0.2.0-alpha.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"registry": "https://registry.npmjs.org/"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"dev": "vite",
|
|
13
|
+
"dev": "vite build --watch",
|
|
14
14
|
"build": "rm -rf dist && vite build",
|
|
15
15
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
16
16
|
"preview": "vite preview"
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
"@nanostores/persistent": "^0.10.1",
|
|
24
24
|
"@nanostores/query": "^0.3.3",
|
|
25
25
|
"@nanostores/react": "^0.7.2",
|
|
26
|
+
"@springmicro/utils": "0.2.0-alpha.4",
|
|
26
27
|
"dotenv": "^16.4.5",
|
|
27
28
|
"nanostores": "^0.10.3",
|
|
28
29
|
"react": "^18.2.0",
|
|
30
|
+
"react-credit-cards-2": "^1.0.2",
|
|
29
31
|
"react-dom": "^18.2.0",
|
|
30
32
|
"unstorage": "^1.10.2"
|
|
31
33
|
},
|
|
@@ -38,8 +40,11 @@
|
|
|
38
40
|
"eslint": "^8.57.0",
|
|
39
41
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
40
42
|
"eslint-plugin-react-refresh": "^0.4.6",
|
|
43
|
+
"formik": "^2.4.6",
|
|
41
44
|
"typescript": "^5.2.2",
|
|
42
|
-
"vite": "^5.2.0"
|
|
45
|
+
"vite": "^5.2.0",
|
|
46
|
+
"vite-plugin-css-injected-by-js": "^3.5.1",
|
|
47
|
+
"yup": "^1.4.0"
|
|
43
48
|
},
|
|
44
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "235b44295dc41168e78c6bc31f286ae7c4587a6c"
|
|
45
50
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { FormikProps } from "formik";
|
|
2
|
+
import {
|
|
3
|
+
allCountries,
|
|
4
|
+
allProvinces,
|
|
5
|
+
allCountriesReverse,
|
|
6
|
+
getRegionLabel,
|
|
7
|
+
getPostalCodeLabel,
|
|
8
|
+
getPostalCodeDefault,
|
|
9
|
+
provinces,
|
|
10
|
+
} from "@springmicro/utils/address";
|
|
11
|
+
import type { Alpha2Code, Province } from "@springmicro/utils/address";
|
|
12
|
+
import React from "react";
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
import { SxProps } from "@mui/material";
|
|
15
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
16
|
+
import Box from "@mui/material/Box";
|
|
17
|
+
import TextField, { StandardTextFieldProps } from "@mui/material/TextField";
|
|
18
|
+
import Autocomplete from "@mui/material/Autocomplete";
|
|
19
|
+
|
|
20
|
+
export type AddressValues = {
|
|
21
|
+
country: string;
|
|
22
|
+
region: string;
|
|
23
|
+
line1: string;
|
|
24
|
+
line2?: string;
|
|
25
|
+
organization?: string;
|
|
26
|
+
city: string;
|
|
27
|
+
postal_code: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type AddressFieldProps = {
|
|
31
|
+
formik: FormikProps<AddressValues & any>; // formik object might contain other values in addition to address
|
|
32
|
+
name: keyof AddressValues;
|
|
33
|
+
sx?: SxProps;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function AddressCountryField({ formik, name, sx }: AddressFieldProps) {
|
|
37
|
+
/**
|
|
38
|
+
* Selects the full country name, need to do a reverse lookup for the Alpha2Code.
|
|
39
|
+
*/
|
|
40
|
+
const [inputValue, setInputValue] = useState(formik.values[name] as string);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Autocomplete
|
|
44
|
+
sx={{ width: 300, ...sx }}
|
|
45
|
+
options={Object.values(allCountries)}
|
|
46
|
+
autoHighlight
|
|
47
|
+
getOptionLabel={(option) => option}
|
|
48
|
+
value={formik.values[name] as string}
|
|
49
|
+
onChange={(e, newValue) => {
|
|
50
|
+
// console.log(newValue)
|
|
51
|
+
formik.setValues({
|
|
52
|
+
...formik.values,
|
|
53
|
+
region: "",
|
|
54
|
+
country: newValue as string,
|
|
55
|
+
line1: "",
|
|
56
|
+
line2: "",
|
|
57
|
+
organization: "",
|
|
58
|
+
city: "",
|
|
59
|
+
postal_code: "",
|
|
60
|
+
});
|
|
61
|
+
}}
|
|
62
|
+
onBlur={formik.handleBlur}
|
|
63
|
+
inputValue={inputValue}
|
|
64
|
+
onInputChange={(event, newInputValue) => {
|
|
65
|
+
setInputValue(newInputValue);
|
|
66
|
+
}}
|
|
67
|
+
renderOption={(props, option) => (
|
|
68
|
+
<Box
|
|
69
|
+
component="li"
|
|
70
|
+
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
<img
|
|
74
|
+
loading="lazy"
|
|
75
|
+
width="20"
|
|
76
|
+
src={`https://flagcdn.com/w20/${allCountriesReverse[
|
|
77
|
+
option
|
|
78
|
+
].toLowerCase()}.png`}
|
|
79
|
+
srcSet={`https://flagcdn.com/w40/${allCountriesReverse[
|
|
80
|
+
option
|
|
81
|
+
].toLowerCase()}.png 2x`}
|
|
82
|
+
alt=""
|
|
83
|
+
/>
|
|
84
|
+
{option}
|
|
85
|
+
</Box>
|
|
86
|
+
)}
|
|
87
|
+
renderInput={(params) => (
|
|
88
|
+
<TextField
|
|
89
|
+
{...params}
|
|
90
|
+
required={true}
|
|
91
|
+
name={name as string}
|
|
92
|
+
label="Country"
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function AddressRegionField({ formik, name, sx }: AddressFieldProps) {
|
|
100
|
+
const [addressRegionFieldOptional, setAddressRegionFieldOptional] =
|
|
101
|
+
useState<boolean>(false);
|
|
102
|
+
const country2code = allCountriesReverse[
|
|
103
|
+
formik.values["country"] as string
|
|
104
|
+
] as Alpha2Code;
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
setAddressRegionFieldOptional(
|
|
108
|
+
provinces.filter(
|
|
109
|
+
(province: Province) => province.country === country2code
|
|
110
|
+
).length === 0
|
|
111
|
+
);
|
|
112
|
+
}, [country2code]);
|
|
113
|
+
|
|
114
|
+
const baseProps = {
|
|
115
|
+
sx: { width: 300, ...sx },
|
|
116
|
+
label: getRegionLabel(country2code),
|
|
117
|
+
name: name as string,
|
|
118
|
+
id: name as string,
|
|
119
|
+
value: formik.values[name],
|
|
120
|
+
onChange: formik.handleChange,
|
|
121
|
+
onBlur: formik.handleBlur,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (!country2code || !getRegionLabel(country2code)) {
|
|
125
|
+
return <></>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (addressRegionFieldOptional) {
|
|
129
|
+
return (
|
|
130
|
+
<TextField
|
|
131
|
+
{...baseProps}
|
|
132
|
+
required={false}
|
|
133
|
+
sx={{ display: "block", ...baseProps.sx }}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
return (
|
|
138
|
+
<TextField {...baseProps} select required={true}>
|
|
139
|
+
{allProvinces
|
|
140
|
+
.filter((val) => val.country === country2code)
|
|
141
|
+
.map((province, i) => (
|
|
142
|
+
<MenuItem key={i} value={province.value}>
|
|
143
|
+
{province.name}
|
|
144
|
+
</MenuItem>
|
|
145
|
+
))}
|
|
146
|
+
</TextField>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function AddressStreetField({
|
|
152
|
+
formik,
|
|
153
|
+
name,
|
|
154
|
+
sx,
|
|
155
|
+
required = true,
|
|
156
|
+
lineNo,
|
|
157
|
+
}: AddressFieldProps & {
|
|
158
|
+
required?: boolean;
|
|
159
|
+
lineNo?: "1" | "2" | "3" | undefined;
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<TextField
|
|
164
|
+
label={lineNo ? `Street Address ${lineNo}` : "Street Address"}
|
|
165
|
+
sx={{ width: 400, display: "block", ...sx }}
|
|
166
|
+
name={name as string}
|
|
167
|
+
id={name as string}
|
|
168
|
+
value={formik.values[name]}
|
|
169
|
+
onChange={formik.handleChange}
|
|
170
|
+
onBlur={formik.handleBlur}
|
|
171
|
+
required={required}
|
|
172
|
+
/>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function AddressOrganizationNameField({
|
|
178
|
+
formik,
|
|
179
|
+
name,
|
|
180
|
+
sx,
|
|
181
|
+
}: AddressFieldProps) {
|
|
182
|
+
return (
|
|
183
|
+
<TextField
|
|
184
|
+
label="Organization Name"
|
|
185
|
+
sx={{ width: 400, display: "block", ...sx }}
|
|
186
|
+
name={name as string}
|
|
187
|
+
id={name as string}
|
|
188
|
+
value={formik.values[name]}
|
|
189
|
+
onChange={formik.handleChange}
|
|
190
|
+
onBlur={formik.handleBlur}
|
|
191
|
+
required={false}
|
|
192
|
+
helperText="Only include organization name if you want it included on the address."
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function AddressCityField({ formik, name, sx }: AddressFieldProps) {
|
|
198
|
+
return (
|
|
199
|
+
<TextField
|
|
200
|
+
label="City"
|
|
201
|
+
sx={{ width: 400, display: "block", ...sx }}
|
|
202
|
+
name={name as string}
|
|
203
|
+
id={name as string}
|
|
204
|
+
value={formik.values[name]}
|
|
205
|
+
onChange={formik.handleChange}
|
|
206
|
+
onBlur={formik.handleBlur}
|
|
207
|
+
required={true}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function AddressPostalCodeField({
|
|
213
|
+
formik,
|
|
214
|
+
name,
|
|
215
|
+
sx,
|
|
216
|
+
}: AddressFieldProps) {
|
|
217
|
+
const country2code = allCountriesReverse[
|
|
218
|
+
formik.values["country"] as string
|
|
219
|
+
] as Alpha2Code;
|
|
220
|
+
|
|
221
|
+
React.useEffect(() => {
|
|
222
|
+
if (!!getPostalCodeDefault(country2code)) {
|
|
223
|
+
formik
|
|
224
|
+
.setFieldValue(name, getPostalCodeDefault(country2code))
|
|
225
|
+
.then(() => formik.setFieldTouched(name, true));
|
|
226
|
+
}
|
|
227
|
+
}, [country2code]);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<TextField
|
|
231
|
+
label={getPostalCodeLabel(country2code)}
|
|
232
|
+
sx={{ width: 400, display: "block", ...sx }}
|
|
233
|
+
name={name as string}
|
|
234
|
+
id={name as string}
|
|
235
|
+
value={formik.values[name]}
|
|
236
|
+
onChange={formik.handleChange}
|
|
237
|
+
onBlur={formik.handleBlur}
|
|
238
|
+
required={true}
|
|
239
|
+
helperText={
|
|
240
|
+
!!getPostalCodeDefault(country2code)
|
|
241
|
+
? 'Our records indicate that your country does not have a postal code system, so "00000" will be used by default.'
|
|
242
|
+
: ""
|
|
243
|
+
}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
AddressValues,
|
|
13
|
+
} from "./Address";
|
|
14
|
+
import {
|
|
15
|
+
allCountries,
|
|
16
|
+
allCountriesReverse,
|
|
17
|
+
getPostalCodeDefault,
|
|
18
|
+
} from "@springmicro/utils/address";
|
|
19
|
+
import { Box, Button, TextField, Typography } from "@mui/material";
|
|
20
|
+
import Cards from "react-credit-cards-2";
|
|
21
|
+
import { FocusEventHandler } from "react";
|
|
22
|
+
import {
|
|
23
|
+
formatCreditCardNumber,
|
|
24
|
+
formatCVC,
|
|
25
|
+
formatExpirationDate,
|
|
26
|
+
splitMonthYear,
|
|
27
|
+
expiryDateHasNotPassed,
|
|
28
|
+
} from "@springmicro/utils/payment";
|
|
29
|
+
|
|
30
|
+
// moved to index.ts
|
|
31
|
+
// import "react-credit-cards-2/dist/es/styles-compiled.css";
|
|
32
|
+
import { StripeLogoLink } from "./ProviderLogos";
|
|
33
|
+
|
|
34
|
+
type CCFocus = "number" | "name" | "cvc" | "expiry";
|
|
35
|
+
|
|
36
|
+
export type CreditCardValues = {
|
|
37
|
+
cvc: string;
|
|
38
|
+
expiry: string;
|
|
39
|
+
focus: CCFocus;
|
|
40
|
+
name: string;
|
|
41
|
+
number: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type AddCardProps = {
|
|
45
|
+
stacked?: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
address?: boolean;
|
|
48
|
+
onSubmit?: (
|
|
49
|
+
values: CreditCardValues & Partial<AddressValues>
|
|
50
|
+
) => Promise<any>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const AddCard = ({
|
|
54
|
+
error,
|
|
55
|
+
stacked = false,
|
|
56
|
+
address = true,
|
|
57
|
+
onSubmit,
|
|
58
|
+
}: AddCardProps) => {
|
|
59
|
+
const [disabled, setDisabled] = useState(true);
|
|
60
|
+
|
|
61
|
+
const initialValuesBase = {
|
|
62
|
+
cvc: "",
|
|
63
|
+
expiry: "",
|
|
64
|
+
focus: "number",
|
|
65
|
+
name: "",
|
|
66
|
+
number: "",
|
|
67
|
+
} as CreditCardValues;
|
|
68
|
+
const initialCountry = "US";
|
|
69
|
+
const initialValuesAddress = {
|
|
70
|
+
country: initialCountry ? allCountries[initialCountry] : "",
|
|
71
|
+
region: "",
|
|
72
|
+
line1: "",
|
|
73
|
+
line2: "",
|
|
74
|
+
organization: "",
|
|
75
|
+
city: "",
|
|
76
|
+
postal_code: getPostalCodeDefault(initialCountry),
|
|
77
|
+
} as AddressValues;
|
|
78
|
+
|
|
79
|
+
// const initialValues = { ...initialValuesBase, ...initialValuesAddress }
|
|
80
|
+
const initialValues = address
|
|
81
|
+
? { ...initialValuesBase, ...initialValuesAddress }
|
|
82
|
+
: initialValuesBase;
|
|
83
|
+
|
|
84
|
+
const validationSchemaBase = {
|
|
85
|
+
expiry: Yup.string()
|
|
86
|
+
// .length(7, "Expiry date must be of the format MM/YY.")
|
|
87
|
+
.test("exp-date", "Expiry date has already passed.", (string) => {
|
|
88
|
+
if (string?.length === 5) {
|
|
89
|
+
if (string.indexOf("/") !== 2) return false;
|
|
90
|
+
const [month, year] = splitMonthYear(string);
|
|
91
|
+
return expiryDateHasNotPassed(month, year);
|
|
92
|
+
} else {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.required("Expiry date is required."),
|
|
97
|
+
number: Yup.string().required("Card Number is required."),
|
|
98
|
+
name: Yup.string().required("Cardholder Name is required."),
|
|
99
|
+
cvc: Yup.string().required("CVC is required."),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const validationSchemaAddress = {
|
|
103
|
+
country: Yup.string().required("Country is required."),
|
|
104
|
+
line1: Yup.string().required("Street Address 1 is required."),
|
|
105
|
+
city: Yup.string().required("City is required."),
|
|
106
|
+
postal_code: Yup.string().required("Postal Code is required."),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const formik = useFormik({
|
|
110
|
+
initialValues,
|
|
111
|
+
validationSchema: Yup.object(
|
|
112
|
+
address
|
|
113
|
+
? {
|
|
114
|
+
...validationSchemaBase,
|
|
115
|
+
...validationSchemaAddress,
|
|
116
|
+
}
|
|
117
|
+
: validationSchemaBase
|
|
118
|
+
),
|
|
119
|
+
onSubmit: async (values, helpers) => {
|
|
120
|
+
if (disabled) {
|
|
121
|
+
throw new Error("Attempted to submit an invalid form.");
|
|
122
|
+
}
|
|
123
|
+
if (onSubmit) {
|
|
124
|
+
if (address) {
|
|
125
|
+
// match backend
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
values["street_address"] = values.line2
|
|
128
|
+
? // @ts-ignore
|
|
129
|
+
`${values.line1}\n${values.line2}`
|
|
130
|
+
: // @ts-ignore
|
|
131
|
+
values.line1;
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
values["locality"] = values.city;
|
|
134
|
+
// @ts-ignore
|
|
135
|
+
delete values.line1;
|
|
136
|
+
// @ts-ignore
|
|
137
|
+
delete values.line2;
|
|
138
|
+
// @ts-ignore
|
|
139
|
+
delete values.city;
|
|
140
|
+
}
|
|
141
|
+
values["card_number"] = values.number;
|
|
142
|
+
values["expiry_date"] = values.expiry;
|
|
143
|
+
delete values.focus;
|
|
144
|
+
delete values.expiry;
|
|
145
|
+
delete values.number;
|
|
146
|
+
|
|
147
|
+
const res = await onSubmit(values);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const handleInputFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
|
153
|
+
const focusedEl = (e.target as HTMLElement).getAttribute("name") as CCFocus;
|
|
154
|
+
if (focusedEl) {
|
|
155
|
+
formik.setValues({
|
|
156
|
+
...formik.values,
|
|
157
|
+
focus: focusedEl,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
163
|
+
if (e.target.name === "card_number") {
|
|
164
|
+
e.target.value = formatCreditCardNumber(e.target.value);
|
|
165
|
+
} else if (e.target.name === "expiry") {
|
|
166
|
+
e.target.value = formatExpirationDate(e.target.value);
|
|
167
|
+
} else if (e.target.name === "cvc") {
|
|
168
|
+
e.target.value = formatCVC(e.target.value);
|
|
169
|
+
}
|
|
170
|
+
formik.handleChange(e);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
setDisabled(Object.keys(formik.errors).length > 0);
|
|
175
|
+
}, [formik.errors]);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
<form
|
|
180
|
+
onSubmit={(e) => {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
formik.handleSubmit();
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<Box
|
|
186
|
+
sx={
|
|
187
|
+
stacked
|
|
188
|
+
? {}
|
|
189
|
+
: {
|
|
190
|
+
display: "flex",
|
|
191
|
+
flexDirection: { xs: "column", sm: "row" },
|
|
192
|
+
gap: "1em",
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
>
|
|
196
|
+
<Cards
|
|
197
|
+
{...(formik.values as CreditCardValues)}
|
|
198
|
+
focused={formik.values.focus || "number"}
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
<div>
|
|
202
|
+
<TextField
|
|
203
|
+
sx={stacked ? { mt: 2 } : {}}
|
|
204
|
+
fullWidth
|
|
205
|
+
type="tel"
|
|
206
|
+
name="number"
|
|
207
|
+
label="Card Number"
|
|
208
|
+
onBlur={formik.handleBlur}
|
|
209
|
+
onChange={handleInputChange}
|
|
210
|
+
onFocus={handleInputFocus}
|
|
211
|
+
variant="outlined"
|
|
212
|
+
value={formik.values.number}
|
|
213
|
+
error={Boolean(formik.touched.number && formik.errors.number)}
|
|
214
|
+
helperText={formik.touched.number && formik.errors.number}
|
|
215
|
+
/>
|
|
216
|
+
<TextField
|
|
217
|
+
sx={{ mt: 2 }}
|
|
218
|
+
fullWidth
|
|
219
|
+
type="text"
|
|
220
|
+
name="name"
|
|
221
|
+
label="Name on Card"
|
|
222
|
+
onBlur={formik.handleBlur}
|
|
223
|
+
onChange={handleInputChange}
|
|
224
|
+
onFocus={handleInputFocus}
|
|
225
|
+
variant="outlined"
|
|
226
|
+
value={formik.values.name}
|
|
227
|
+
error={Boolean(formik.touched.name && formik.errors.name)}
|
|
228
|
+
helperText={formik.touched.name && formik.errors.name}
|
|
229
|
+
/>
|
|
230
|
+
<TextField
|
|
231
|
+
sx={{ mt: 2, width: "calc(50% - 8px)", mr: 1 }}
|
|
232
|
+
type="tel"
|
|
233
|
+
name="expiry"
|
|
234
|
+
label="Valid Thru"
|
|
235
|
+
placeholder="MM/YY"
|
|
236
|
+
inputProps={{ pattern: "[0-9]{2}/[0-9]{2}" }}
|
|
237
|
+
required
|
|
238
|
+
onBlur={formik.handleBlur}
|
|
239
|
+
onChange={handleInputChange}
|
|
240
|
+
onFocus={handleInputFocus}
|
|
241
|
+
variant="outlined"
|
|
242
|
+
value={formik.values.expiry}
|
|
243
|
+
error={Boolean(formik.touched.expiry && formik.errors.expiry)}
|
|
244
|
+
helperText={
|
|
245
|
+
(formik.touched.expiry && formik.errors.expiry) || "MM/YY"
|
|
246
|
+
}
|
|
247
|
+
/>
|
|
248
|
+
<TextField
|
|
249
|
+
sx={{ mt: 2, width: "calc(50% - 8px)", ml: 1 }}
|
|
250
|
+
type="tel"
|
|
251
|
+
name="cvc"
|
|
252
|
+
label="CVC"
|
|
253
|
+
onBlur={formik.handleBlur}
|
|
254
|
+
onChange={handleInputChange}
|
|
255
|
+
onFocus={handleInputFocus}
|
|
256
|
+
variant="outlined"
|
|
257
|
+
value={formik.values.cvc}
|
|
258
|
+
error={Boolean(formik.touched.cvc && formik.errors.cvc)}
|
|
259
|
+
helperText={formik.touched.cvc && formik.errors.cvc}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
{/* {formik.errors.submit && (
|
|
263
|
+
<Typography color="error" sx={{ mt: 2 }} variant="body2">
|
|
264
|
+
{formik.errors.submit}
|
|
265
|
+
</Typography>
|
|
266
|
+
)} */}
|
|
267
|
+
</Box>
|
|
268
|
+
<Box>
|
|
269
|
+
{!address ? null : (
|
|
270
|
+
<Box>
|
|
271
|
+
<Typography variant="h6" sx={{ mt: 2, mb: 3 }}>
|
|
272
|
+
Billing Address
|
|
273
|
+
</Typography>
|
|
274
|
+
<AddressCountryField formik={formik} name="country" />
|
|
275
|
+
<AddressOrganizationNameField
|
|
276
|
+
sx={{ mt: 2 }}
|
|
277
|
+
formik={formik}
|
|
278
|
+
name="organization"
|
|
279
|
+
/>
|
|
280
|
+
<AddressStreetField
|
|
281
|
+
sx={{ mt: 2 }}
|
|
282
|
+
formik={formik}
|
|
283
|
+
name="line1"
|
|
284
|
+
lineNo="1"
|
|
285
|
+
/>
|
|
286
|
+
<AddressStreetField
|
|
287
|
+
sx={{ mt: 2 }}
|
|
288
|
+
formik={formik}
|
|
289
|
+
name="line2"
|
|
290
|
+
lineNo="2"
|
|
291
|
+
required={false}
|
|
292
|
+
/>
|
|
293
|
+
<AddressCityField sx={{ mt: 2 }} formik={formik} name="city" />
|
|
294
|
+
<AddressRegionField
|
|
295
|
+
sx={{ mt: 2 }}
|
|
296
|
+
formik={formik}
|
|
297
|
+
name="region"
|
|
298
|
+
/>
|
|
299
|
+
<AddressPostalCodeField
|
|
300
|
+
sx={{ mt: 2 }}
|
|
301
|
+
formik={formik}
|
|
302
|
+
name="postal_code"
|
|
303
|
+
/>
|
|
304
|
+
</Box>
|
|
305
|
+
)}
|
|
306
|
+
</Box>
|
|
307
|
+
{error && (
|
|
308
|
+
<Typography color="error" sx={{ mt: 2 }} variant="body2">
|
|
309
|
+
{error}
|
|
310
|
+
</Typography>
|
|
311
|
+
)}
|
|
312
|
+
<Button
|
|
313
|
+
size="large"
|
|
314
|
+
sx={{ mt: 3 }}
|
|
315
|
+
variant="contained"
|
|
316
|
+
type="submit"
|
|
317
|
+
disabled={disabled}
|
|
318
|
+
>
|
|
319
|
+
Continue
|
|
320
|
+
</Button>
|
|
321
|
+
<Box sx={{ mt: 1 }}>
|
|
322
|
+
<StripeLogoLink />
|
|
323
|
+
</Box>
|
|
324
|
+
</form>
|
|
325
|
+
</>
|
|
326
|
+
);
|
|
327
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createSvgIcon } from "@mui/material";
|
|
2
|
+
// Source: https://brandfolder.com/s/99gctvbpwgvzbc7mz3j9g4x
|
|
3
|
+
|
|
4
|
+
export const PoweredByStripe = createSvgIcon(
|
|
5
|
+
<svg
|
|
6
|
+
id="Layer_1"
|
|
7
|
+
data-name="Layer 1"
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
viewBox="0 0 150 34"
|
|
10
|
+
>
|
|
11
|
+
<defs>
|
|
12
|
+
<style>{`.cls-1{fill: #635bff}`}</style>
|
|
13
|
+
</defs>
|
|
14
|
+
<title>Powered by Stripe - blurple</title>
|
|
15
|
+
<path
|
|
16
|
+
className="cls-1"
|
|
17
|
+
d="M146,0H3.73A3.73,3.73,0,0,0,0,3.73V30.27A3.73,3.73,0,0,0,3.73,34H146a4,4,0,0,0,4-4V4A4,4,0,0,0,146,0Zm3,30a3,3,0,0,1-3,3H3.73A2.74,2.74,0,0,1,1,30.27V3.73A2.74,2.74,0,0,1,3.73,1H146a3,3,0,0,1,3,3Z"
|
|
18
|
+
/>
|
|
19
|
+
<path
|
|
20
|
+
className="cls-1"
|
|
21
|
+
d="M17.07,11.24h-4.3V22h1.92V17.84h2.38c2.4,0,3.9-1.16,3.9-3.3S19.47,11.24,17.07,11.24Zm-.1,5H14.69v-3.3H17c1.38,0,2.11.59,2.11,1.65S18.35,16.19,17,16.19Z"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
className="cls-1"
|
|
25
|
+
d="M25.1,14a3.77,3.77,0,0,0-3.8,4.09,3.81,3.81,0,1,0,7.59,0A3.76,3.76,0,0,0,25.1,14Zm0,6.67c-1.22,0-2-1-2-2.58s.76-2.58,2-2.58,2,1,2,2.58S26.31,20.66,25.1,20.66Z"
|
|
26
|
+
/>
|
|
27
|
+
<polygon
|
|
28
|
+
className="cls-1"
|
|
29
|
+
points="36.78 19.35 35.37 14.13 33.89 14.13 32.49 19.35 31.07 14.13 29.22 14.13 31.59 22.01 33.15 22.01 34.59 16.85 36.03 22.01 37.59 22.01 39.96 14.13 38.18 14.13 36.78 19.35"
|
|
30
|
+
/>
|
|
31
|
+
<path
|
|
32
|
+
className="cls-1"
|
|
33
|
+
d="M44,14a3.83,3.83,0,0,0-3.75,4.09,3.79,3.79,0,0,0,3.83,4.09A3.47,3.47,0,0,0,47.49,20L46,19.38a1.78,1.78,0,0,1-1.83,1.26A2.12,2.12,0,0,1,42,18.47h5.52v-.6C47.54,15.71,46.32,14,44,14Zm-1.93,3.13A1.92,1.92,0,0,1,44,15.5a1.56,1.56,0,0,1,1.69,1.62Z"
|
|
34
|
+
/>
|
|
35
|
+
<path
|
|
36
|
+
className="cls-1"
|
|
37
|
+
d="M50.69,15.3V14.13h-1.8V22h1.8V17.87a1.89,1.89,0,0,1,2-2,4.68,4.68,0,0,1,.66,0v-1.8c-.14,0-.3,0-.51,0A2.29,2.29,0,0,0,50.69,15.3Z"
|
|
38
|
+
/>
|
|
39
|
+
<path
|
|
40
|
+
className="cls-1"
|
|
41
|
+
d="M57.48,14a3.83,3.83,0,0,0-3.75,4.09,3.79,3.79,0,0,0,3.83,4.09A3.47,3.47,0,0,0,60.93,20l-1.54-.59a1.78,1.78,0,0,1-1.83,1.26,2.12,2.12,0,0,1-2.1-2.17H61v-.6C61,15.71,59.76,14,57.48,14Zm-1.93,3.13a1.92,1.92,0,0,1,1.92-1.62,1.56,1.56,0,0,1,1.69,1.62Z"
|
|
42
|
+
/>
|
|
43
|
+
<path
|
|
44
|
+
className="cls-1"
|
|
45
|
+
d="M67.56,15a2.85,2.85,0,0,0-2.26-1c-2.21,0-3.47,1.85-3.47,4.09s1.26,4.09,3.47,4.09a2.82,2.82,0,0,0,2.26-1V22h1.8V11.24h-1.8Zm0,3.35a2,2,0,0,1-2,2.28c-1.31,0-2-1-2-2.52s.7-2.52,2-2.52c1.11,0,2,.81,2,2.29Z"
|
|
46
|
+
/>
|
|
47
|
+
<path
|
|
48
|
+
className="cls-1"
|
|
49
|
+
d="M79.31,14A2.88,2.88,0,0,0,77,15V11.24h-1.8V22H77v-.83a2.86,2.86,0,0,0,2.27,1c2.2,0,3.46-1.86,3.46-4.09S81.51,14,79.31,14ZM79,20.6a2,2,0,0,1-2-2.28v-.47c0-1.48.84-2.29,2-2.29,1.3,0,2,1,2,2.52S80.25,20.6,79,20.6Z"
|
|
50
|
+
/>
|
|
51
|
+
<path
|
|
52
|
+
className="cls-1"
|
|
53
|
+
d="M86.93,19.66,85,14.13H83.1L86,21.72l-.3.74a1,1,0,0,1-1.14.79,4.12,4.12,0,0,1-.6,0v1.51a4.62,4.62,0,0,0,.73.05,2.67,2.67,0,0,0,2.78-2l3.24-8.62H88.82Z"
|
|
54
|
+
/>
|
|
55
|
+
<path
|
|
56
|
+
className="cls-1"
|
|
57
|
+
d="M125,12.43a3,3,0,0,0-2.13.87l-.14-.69h-2.39V25.53l2.72-.59V21.81a3,3,0,0,0,1.93.7c1.94,0,3.72-1.59,3.72-5.11C128.71,14.18,126.91,12.43,125,12.43Zm-.65,7.63a1.61,1.61,0,0,1-1.28-.52l0-4.11a1.64,1.64,0,0,1,1.3-.55c1,0,1.68,1.13,1.68,2.58S125.36,20.06,124.35,20.06Z"
|
|
58
|
+
/>
|
|
59
|
+
<path
|
|
60
|
+
className="cls-1"
|
|
61
|
+
d="M133.73,12.43c-2.62,0-4.21,2.26-4.21,5.11,0,3.37,1.88,5.08,4.56,5.08a6.12,6.12,0,0,0,3-.73V19.64a5.79,5.79,0,0,1-2.7.62c-1.08,0-2-.39-2.14-1.7h5.38c0-.15,0-.74,0-1C137.71,14.69,136.35,12.43,133.73,12.43Zm-1.47,4.07c0-1.26.77-1.79,1.45-1.79s1.4.53,1.4,1.79Z"
|
|
62
|
+
/>
|
|
63
|
+
<path
|
|
64
|
+
className="cls-1"
|
|
65
|
+
d="M113,13.36l-.17-.82h-2.32v9.71h2.68V15.67a1.87,1.87,0,0,1,2.05-.58V12.54A1.8,1.8,0,0,0,113,13.36Z"
|
|
66
|
+
/>
|
|
67
|
+
<path
|
|
68
|
+
className="cls-1"
|
|
69
|
+
d="M99.46,15.46c0-.44.36-.61.93-.61a5.9,5.9,0,0,1,2.7.72V12.94a7,7,0,0,0-2.7-.51c-2.21,0-3.68,1.18-3.68,3.16,0,3.1,4.14,2.6,4.14,3.93,0,.52-.44.69-1,.69a6.78,6.78,0,0,1-3-.9V22a7.38,7.38,0,0,0,3,.64c2.26,0,3.82-1.15,3.82-3.16C103.62,16.12,99.46,16.72,99.46,15.46Z"
|
|
70
|
+
/>
|
|
71
|
+
<path
|
|
72
|
+
className="cls-1"
|
|
73
|
+
d="M107.28,10.24l-2.65.58v8.93a2.77,2.77,0,0,0,2.82,2.87,4.16,4.16,0,0,0,1.91-.37V20c-.35.15-2.06.66-2.06-1V15h2.06V12.66h-2.06Z"
|
|
74
|
+
/>
|
|
75
|
+
<polygon
|
|
76
|
+
className="cls-1"
|
|
77
|
+
points="116.25 11.7 118.98 11.13 118.98 8.97 116.25 9.54 116.25 11.7"
|
|
78
|
+
/>
|
|
79
|
+
<rect className="cls-1" x="116.25" y="12.61" width="2.73" height="9.64" />
|
|
80
|
+
</svg>,
|
|
81
|
+
"PoweredByStripe"
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const StripeLogoLink = ({ height }: { height?: string }) => {
|
|
85
|
+
return (
|
|
86
|
+
<a href="https://stripe.com" target="_blank">
|
|
87
|
+
<PoweredByStripe
|
|
88
|
+
sx={{ width: "unset", height: height || "1em" }}
|
|
89
|
+
viewBox="0 0 150 34"
|
|
90
|
+
/>
|
|
91
|
+
</a>
|
|
92
|
+
);
|
|
93
|
+
};
|