@blocklet/payment-react 1.18.25 → 1.18.27
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/es/checkout/donate.js +11 -1
- package/es/components/country-select.js +243 -21
- package/es/components/over-due-invoice-payment.d.ts +3 -1
- package/es/components/over-due-invoice-payment.js +6 -4
- package/es/contexts/payment.d.ts +2 -1
- package/es/contexts/payment.js +8 -1
- package/es/index.d.ts +1 -0
- package/es/index.js +1 -0
- package/es/libs/api.js +4 -0
- package/es/libs/currency.d.ts +3 -0
- package/es/libs/currency.js +22 -0
- package/es/libs/phone-validator.js +2 -0
- package/es/libs/validator.d.ts +1 -0
- package/es/libs/validator.js +70 -0
- package/es/payment/form/address.js +17 -3
- package/es/payment/form/index.js +10 -1
- package/es/payment/form/phone.js +12 -1
- package/es/payment/form/stripe/form.js +14 -5
- package/es/payment/index.js +33 -11
- package/es/payment/product-donation.js +110 -12
- package/es/types/shims.d.ts +2 -0
- package/lib/checkout/donate.js +11 -1
- package/lib/components/country-select.js +243 -39
- package/lib/components/over-due-invoice-payment.d.ts +3 -1
- package/lib/components/over-due-invoice-payment.js +7 -4
- package/lib/contexts/payment.d.ts +2 -1
- package/lib/contexts/payment.js +9 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +12 -0
- package/lib/libs/api.js +4 -0
- package/lib/libs/currency.d.ts +3 -0
- package/lib/libs/currency.js +31 -0
- package/lib/libs/phone-validator.js +1 -0
- package/lib/libs/validator.d.ts +1 -0
- package/lib/libs/validator.js +20 -0
- package/lib/payment/form/address.js +15 -2
- package/lib/payment/form/index.js +12 -1
- package/lib/payment/form/phone.js +13 -1
- package/lib/payment/form/stripe/form.js +21 -5
- package/lib/payment/index.js +34 -10
- package/lib/payment/product-donation.js +106 -15
- package/lib/types/shims.d.ts +2 -0
- package/package.json +8 -8
- package/src/checkout/donate.tsx +11 -1
- package/src/components/country-select.tsx +265 -20
- package/src/components/over-due-invoice-payment.tsx +6 -2
- package/src/contexts/payment.tsx +11 -1
- package/src/index.ts +1 -0
- package/src/libs/api.ts +4 -1
- package/src/libs/currency.ts +25 -0
- package/src/libs/phone-validator.ts +1 -0
- package/src/libs/validator.ts +70 -0
- package/src/payment/form/address.tsx +17 -4
- package/src/payment/form/index.tsx +11 -1
- package/src/payment/form/phone.tsx +15 -1
- package/src/payment/form/stripe/form.tsx +20 -9
- package/src/payment/index.tsx +45 -14
- package/src/payment/product-donation.tsx +129 -10
- package/src/types/shims.d.ts +2 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
2
|
+
import { useMemo, forwardRef, useState, useEffect, ChangeEvent, useRef, KeyboardEvent } from 'react';
|
|
3
|
+
import { Box, MenuItem, Select, Typography, TextField } from '@mui/material';
|
|
3
4
|
import { useFormContext } from 'react-hook-form';
|
|
4
5
|
import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
|
|
5
6
|
import type { SxProps } from '@mui/material';
|
|
6
7
|
import type { CountryIso2 } from 'react-international-phone';
|
|
8
|
+
import { useMobile } from '../hooks/mobile';
|
|
7
9
|
|
|
8
10
|
export type CountrySelectProps = {
|
|
9
11
|
value: CountryIso2;
|
|
@@ -14,6 +16,123 @@ export type CountrySelectProps = {
|
|
|
14
16
|
|
|
15
17
|
const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(({ value, onChange, name, sx }, ref) => {
|
|
16
18
|
const { setValue } = useFormContext();
|
|
19
|
+
const [open, setOpen] = useState(false);
|
|
20
|
+
const [searchText, setSearchText] = useState('');
|
|
21
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
22
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
25
|
+
const itemHeightRef = useRef<number>(40);
|
|
26
|
+
const { isMobile } = useMobile();
|
|
27
|
+
const measuredRef = useRef(false);
|
|
28
|
+
|
|
29
|
+
// Handle window resize
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!open) return () => {};
|
|
32
|
+
const handleResize = () => {
|
|
33
|
+
measuredRef.current = false;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
window.addEventListener('resize', handleResize);
|
|
37
|
+
return () => {
|
|
38
|
+
window.removeEventListener('resize', handleResize);
|
|
39
|
+
};
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
}, [open]);
|
|
42
|
+
|
|
43
|
+
const scrollToTop = () => {
|
|
44
|
+
if (listRef.current) {
|
|
45
|
+
listRef.current.scrollTop = 0;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const measureItemHeight = () => {
|
|
50
|
+
if (!listRef.current || !open) return;
|
|
51
|
+
|
|
52
|
+
const items = listRef.current.querySelectorAll('.MuiMenuItem-root');
|
|
53
|
+
if (items.length > 0) {
|
|
54
|
+
const firstItem = items[0] as HTMLElement;
|
|
55
|
+
if (firstItem.offsetHeight > 0) {
|
|
56
|
+
itemHeightRef.current = firstItem.offsetHeight;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const controlScrollPosition = (index: number) => {
|
|
62
|
+
if (!listRef.current) return;
|
|
63
|
+
|
|
64
|
+
// Always measure height when dropdown is open for accuracy
|
|
65
|
+
if (open && !measuredRef.current) {
|
|
66
|
+
measureItemHeight();
|
|
67
|
+
measuredRef.current = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const listHeight = listRef.current.clientHeight;
|
|
71
|
+
const targetPosition = index * itemHeightRef.current;
|
|
72
|
+
|
|
73
|
+
if (index < 2) {
|
|
74
|
+
listRef.current.scrollTop = 0;
|
|
75
|
+
} else if (index > filteredCountries.length - 3) {
|
|
76
|
+
listRef.current.scrollTop = listRef.current.scrollHeight - listHeight;
|
|
77
|
+
} else {
|
|
78
|
+
const scrollPosition = targetPosition - listHeight / 2 + itemHeightRef.current / 2;
|
|
79
|
+
listRef.current.scrollTop = Math.max(0, scrollPosition);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
let timeout: NodeJS.Timeout | null = null;
|
|
85
|
+
if (open) {
|
|
86
|
+
timeout = setTimeout(() => {
|
|
87
|
+
scrollToTop();
|
|
88
|
+
if (!isMobile && inputRef.current) {
|
|
89
|
+
inputRef.current.focus();
|
|
90
|
+
}
|
|
91
|
+
}, 100);
|
|
92
|
+
} else {
|
|
93
|
+
setSearchText('');
|
|
94
|
+
setFocusedIndex(-1);
|
|
95
|
+
}
|
|
96
|
+
return () => {
|
|
97
|
+
if (timeout) {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}, [open, isMobile]);
|
|
102
|
+
|
|
103
|
+
const filteredCountries = useMemo(() => {
|
|
104
|
+
if (!searchText) return defaultCountries;
|
|
105
|
+
|
|
106
|
+
return defaultCountries.filter((c) => {
|
|
107
|
+
const parsed = parseCountry(c);
|
|
108
|
+
return (
|
|
109
|
+
parsed.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
110
|
+
parsed.iso2.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
111
|
+
`+${parsed.dialCode}`.includes(searchText)
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}, [searchText]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
scrollToTop();
|
|
118
|
+
setFocusedIndex(-1);
|
|
119
|
+
}, [searchText]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
let timeout: NodeJS.Timeout | null = null;
|
|
123
|
+
if (focusedIndex >= 0) {
|
|
124
|
+
timeout = setTimeout(() => {
|
|
125
|
+
controlScrollPosition(focusedIndex);
|
|
126
|
+
}, 10);
|
|
127
|
+
}
|
|
128
|
+
return () => {
|
|
129
|
+
if (timeout) {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
+
}, [focusedIndex, filteredCountries.length]);
|
|
135
|
+
|
|
17
136
|
const countryDetail = useMemo(() => {
|
|
18
137
|
const item = defaultCountries.find((v) => v[1] === value);
|
|
19
138
|
return value && item ? parseCountry(item) : { name: '' };
|
|
@@ -24,12 +143,68 @@ const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(({ value, o
|
|
|
24
143
|
setValue(name, e.target.value);
|
|
25
144
|
};
|
|
26
145
|
|
|
146
|
+
const handleCountryClick = (code: CountryIso2) => {
|
|
147
|
+
onChange(code);
|
|
148
|
+
setValue(name, code);
|
|
149
|
+
setOpen(false);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
setSearchText(e.target.value);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
|
|
160
|
+
if (e.key === 'Escape') {
|
|
161
|
+
setOpen(false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const handleNavigation = (direction: 'next' | 'prev') => {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
setFocusedIndex((prev) => {
|
|
168
|
+
if (direction === 'next') {
|
|
169
|
+
if (prev === -1) return 0;
|
|
170
|
+
return prev >= filteredCountries.length - 1 ? 0 : prev + 1;
|
|
171
|
+
}
|
|
172
|
+
if (prev === -1) return filteredCountries.length - 1;
|
|
173
|
+
return prev <= 0 ? filteredCountries.length - 1 : prev - 1;
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (e.key === 'Tab') {
|
|
178
|
+
handleNavigation(e.shiftKey ? 'prev' : 'next');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (e.key === 'ArrowDown') {
|
|
183
|
+
handleNavigation('next');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (e.key === 'ArrowUp') {
|
|
188
|
+
handleNavigation('prev');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (e.key === 'Enter' && focusedIndex >= 0 && focusedIndex < filteredCountries.length) {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
const country = parseCountry(filteredCountries[focusedIndex]);
|
|
195
|
+
handleCountryClick(country.iso2);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
27
199
|
return (
|
|
28
200
|
<Select
|
|
29
201
|
ref={ref}
|
|
202
|
+
open={open}
|
|
203
|
+
onOpen={() => setOpen(true)}
|
|
204
|
+
onClose={() => setOpen(false)}
|
|
30
205
|
MenuProps={{
|
|
31
206
|
style: {
|
|
32
|
-
|
|
207
|
+
maxHeight: '300px',
|
|
33
208
|
top: '10px',
|
|
34
209
|
},
|
|
35
210
|
anchorOrigin: {
|
|
@@ -40,6 +215,18 @@ const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(({ value, o
|
|
|
40
215
|
vertical: 'top',
|
|
41
216
|
horizontal: 'left',
|
|
42
217
|
},
|
|
218
|
+
PaperProps: {
|
|
219
|
+
ref: menuRef,
|
|
220
|
+
sx: {
|
|
221
|
+
display: 'flex',
|
|
222
|
+
'& .MuiList-root': {
|
|
223
|
+
pt: 0,
|
|
224
|
+
display: 'flex',
|
|
225
|
+
flexDirection: 'column',
|
|
226
|
+
overflowY: 'hidden',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
43
230
|
}}
|
|
44
231
|
sx={{
|
|
45
232
|
width: '100%',
|
|
@@ -65,24 +252,82 @@ const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(({ value, o
|
|
|
65
252
|
}}
|
|
66
253
|
value={value}
|
|
67
254
|
onChange={onCountryChange}
|
|
68
|
-
renderValue={(code) =>
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
255
|
+
renderValue={(code) => (
|
|
256
|
+
<Box display="flex" alignItems="center" flexWrap="nowrap" gap={0.5} sx={{ cursor: 'pointer' }}>
|
|
257
|
+
<FlagEmoji iso2={code} style={{ display: 'flex' }} />
|
|
258
|
+
<Typography>{countryDetail?.name}</Typography>
|
|
259
|
+
</Box>
|
|
260
|
+
)}>
|
|
261
|
+
<Box
|
|
262
|
+
sx={{
|
|
263
|
+
position: 'sticky',
|
|
264
|
+
top: 0,
|
|
265
|
+
zIndex: 1,
|
|
266
|
+
bgcolor: 'background.paper',
|
|
267
|
+
p: 1,
|
|
268
|
+
}}
|
|
269
|
+
onClick={(e) => {
|
|
270
|
+
e.stopPropagation();
|
|
271
|
+
}}>
|
|
272
|
+
<TextField
|
|
273
|
+
inputRef={inputRef}
|
|
274
|
+
autoFocus={!isMobile}
|
|
275
|
+
fullWidth
|
|
276
|
+
placeholder="Search country..."
|
|
277
|
+
value={searchText}
|
|
278
|
+
onChange={handleSearchChange}
|
|
279
|
+
onKeyDown={handleKeyDown}
|
|
280
|
+
onClick={(e) => e.stopPropagation()}
|
|
281
|
+
size="small"
|
|
282
|
+
variant="outlined"
|
|
283
|
+
/>
|
|
284
|
+
</Box>
|
|
285
|
+
|
|
286
|
+
<Box
|
|
287
|
+
ref={listRef}
|
|
288
|
+
sx={{
|
|
289
|
+
flex: 1,
|
|
290
|
+
overflowY: 'auto',
|
|
291
|
+
overflowX: 'hidden',
|
|
292
|
+
maxHeight: 'calc(300px - 65px)',
|
|
293
|
+
scrollBehavior: 'smooth',
|
|
294
|
+
}}>
|
|
295
|
+
{filteredCountries.length > 0 ? (
|
|
296
|
+
filteredCountries.map((c: any, index) => {
|
|
297
|
+
const parsed = parseCountry(c);
|
|
298
|
+
const isFocused = index === focusedIndex;
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<MenuItem
|
|
302
|
+
key={parsed.iso2}
|
|
303
|
+
value={parsed.iso2}
|
|
304
|
+
selected={parsed.iso2 === value}
|
|
305
|
+
onClick={() => handleCountryClick(parsed.iso2)}
|
|
306
|
+
sx={{
|
|
307
|
+
'&.Mui-selected': {
|
|
308
|
+
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
|
309
|
+
},
|
|
310
|
+
'&:hover': {
|
|
311
|
+
backgroundColor: 'var(--backgrounds-bg-highlight, #eff6ff)',
|
|
312
|
+
},
|
|
313
|
+
...(isFocused
|
|
314
|
+
? {
|
|
315
|
+
backgroundColor: 'var(--backgrounds-bg-highlight, #eff6ff)',
|
|
316
|
+
outline: 'none',
|
|
317
|
+
}
|
|
318
|
+
: {}),
|
|
319
|
+
}}>
|
|
320
|
+
<FlagEmoji iso2={parsed.iso2} style={{ marginRight: '8px' }} />
|
|
321
|
+
<Typography>{parsed.name}</Typography>
|
|
322
|
+
</MenuItem>
|
|
323
|
+
);
|
|
324
|
+
})
|
|
325
|
+
) : (
|
|
326
|
+
<MenuItem disabled>
|
|
327
|
+
<Typography color="text.secondary">No countries found</Typography>
|
|
83
328
|
</MenuItem>
|
|
84
|
-
)
|
|
85
|
-
|
|
329
|
+
)}
|
|
330
|
+
</Box>
|
|
86
331
|
</Select>
|
|
87
332
|
);
|
|
88
333
|
});
|
|
@@ -46,6 +46,7 @@ type Props = {
|
|
|
46
46
|
detailUrl: string;
|
|
47
47
|
}
|
|
48
48
|
) => React.ReactNode;
|
|
49
|
+
authToken?: string;
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
type SummaryItem = {
|
|
@@ -64,6 +65,7 @@ type OverdueInvoicesResult = {
|
|
|
64
65
|
const fetchOverdueInvoices = async (params: {
|
|
65
66
|
subscriptionId?: string;
|
|
66
67
|
customerId?: string;
|
|
68
|
+
authToken?: string;
|
|
67
69
|
}): Promise<OverdueInvoicesResult> => {
|
|
68
70
|
if (!params.subscriptionId && !params.customerId) {
|
|
69
71
|
throw new Error('Either subscriptionId or customerId must be provided');
|
|
@@ -76,7 +78,7 @@ const fetchOverdueInvoices = async (params: {
|
|
|
76
78
|
url = `/api/customers/${params.customerId}/overdue/invoices`;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
const res = await api.get(url);
|
|
81
|
+
const res = await api.get(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
|
|
80
82
|
return res.data;
|
|
81
83
|
};
|
|
82
84
|
|
|
@@ -90,6 +92,7 @@ function OverdueInvoicePayment({
|
|
|
90
92
|
detailLinkOptions = { enabled: true },
|
|
91
93
|
successToast = true,
|
|
92
94
|
alertMessage = '',
|
|
95
|
+
authToken,
|
|
93
96
|
}: Props) {
|
|
94
97
|
const { t } = useLocaleContext();
|
|
95
98
|
const { connect } = usePaymentContext();
|
|
@@ -109,7 +112,7 @@ function OverdueInvoicePayment({
|
|
|
109
112
|
error,
|
|
110
113
|
loading,
|
|
111
114
|
runAsync: refresh,
|
|
112
|
-
} = useRequest(() => fetchOverdueInvoices({ subscriptionId, customerId }), {
|
|
115
|
+
} = useRequest(() => fetchOverdueInvoices({ subscriptionId, customerId, authToken }), {
|
|
113
116
|
ready: !!subscriptionId || !!customerId,
|
|
114
117
|
});
|
|
115
118
|
|
|
@@ -520,6 +523,7 @@ OverdueInvoicePayment.defaultProps = {
|
|
|
520
523
|
customerId: undefined,
|
|
521
524
|
successToast: true,
|
|
522
525
|
alertMessage: '',
|
|
526
|
+
authToken: undefined,
|
|
523
527
|
};
|
|
524
528
|
|
|
525
529
|
export default OverdueInvoicePayment;
|
package/src/contexts/payment.tsx
CHANGED
|
@@ -34,6 +34,8 @@ export type PaymentContextProps = {
|
|
|
34
34
|
children: any;
|
|
35
35
|
// eslint-disable-next-line react/require-default-props
|
|
36
36
|
baseUrl?: string;
|
|
37
|
+
// eslint-disable-next-line react/require-default-props
|
|
38
|
+
authToken?: string;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const formatData = (data: any) => {
|
|
@@ -72,10 +74,18 @@ const getMethod = (methodId: string, methods: TPaymentMethodExpanded[]) => {
|
|
|
72
74
|
return methods.find((x) => x.id === methodId);
|
|
73
75
|
};
|
|
74
76
|
|
|
75
|
-
function PaymentProvider({ session, connect, children, baseUrl }: PaymentContextProps) {
|
|
77
|
+
function PaymentProvider({ session, connect, children, baseUrl, authToken }: PaymentContextProps) {
|
|
76
78
|
if (baseUrl) {
|
|
77
79
|
// This is hack but efficient to share
|
|
78
80
|
window.__PAYMENT_KIT_BASE_URL = baseUrl;
|
|
81
|
+
} else {
|
|
82
|
+
window.__PAYMENT_KIT_BASE_URL = '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (authToken) {
|
|
86
|
+
window.__PAYMENT_KIT_AUTH_TOKEN = authToken;
|
|
87
|
+
} else {
|
|
88
|
+
window.__PAYMENT_KIT_AUTH_TOKEN = '';
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
const [livemode, setLivemode] = useLocalStorageState('livemode', { defaultValue: true });
|
package/src/index.ts
CHANGED
package/src/libs/api.ts
CHANGED
|
@@ -11,10 +11,13 @@ api.interceptors.request.use(
|
|
|
11
11
|
(config: any) => {
|
|
12
12
|
const prefix = getPrefix();
|
|
13
13
|
config.baseURL = prefix || '';
|
|
14
|
-
|
|
15
14
|
const locale = getLocale(window.blocklet?.languages);
|
|
16
15
|
const query = new URLSearchParams(config.url?.split('?').pop());
|
|
17
16
|
config.params = { ...(config.params || {}), locale };
|
|
17
|
+
const authToken = window.__PAYMENT_KIT_AUTH_TOKEN;
|
|
18
|
+
if (authToken && typeof config.params.authToken === 'undefined' && !query.has('authToken')) {
|
|
19
|
+
config.params.authToken = authToken;
|
|
20
|
+
}
|
|
18
21
|
// If we do not have livemode neither in config nor in query params, we will use the value from localStorage
|
|
19
22
|
if (typeof config.params.livemode === 'undefined' && query.has('livemode') === false) {
|
|
20
23
|
const livemode = localStorage.getItem('livemode');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CURRENCY_PREFERENCE_KEY_BASE = 'payment-currency-preference';
|
|
2
|
+
|
|
3
|
+
export const getUserStorageKey = (base: string, did?: string) => {
|
|
4
|
+
return did ? `${base}:${did}` : base;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const saveCurrencyPreference = (currencyId: string, did?: string) => {
|
|
8
|
+
try {
|
|
9
|
+
localStorage.setItem(getUserStorageKey(CURRENCY_PREFERENCE_KEY_BASE, did), currencyId);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.warn('Failed to save currency preference', e);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getCurrencyPreference = (did?: string, availableCurrencyIds?: string[]) => {
|
|
16
|
+
try {
|
|
17
|
+
const saved = localStorage.getItem(getUserStorageKey(CURRENCY_PREFERENCE_KEY_BASE, did));
|
|
18
|
+
if (saved && (!availableCurrencyIds || availableCurrencyIds.includes(saved))) {
|
|
19
|
+
return saved;
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.warn('Failed to access localStorage', e);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import isPostalCode, { PostalCodeLocale } from 'validator/lib/isPostalCode';
|
|
2
|
+
|
|
3
|
+
const POSTAL_CODE_SUPPORTED_COUNTRIES: PostalCodeLocale[] = [
|
|
4
|
+
'AD',
|
|
5
|
+
'AT',
|
|
6
|
+
'AU',
|
|
7
|
+
'BE',
|
|
8
|
+
'BG',
|
|
9
|
+
'BR',
|
|
10
|
+
'CA',
|
|
11
|
+
'CH',
|
|
12
|
+
'CN',
|
|
13
|
+
'CZ',
|
|
14
|
+
'DE',
|
|
15
|
+
'DK',
|
|
16
|
+
'DZ',
|
|
17
|
+
'EE',
|
|
18
|
+
'ES',
|
|
19
|
+
'FI',
|
|
20
|
+
'FR',
|
|
21
|
+
'GB',
|
|
22
|
+
'GR',
|
|
23
|
+
'HR',
|
|
24
|
+
'HU',
|
|
25
|
+
'ID',
|
|
26
|
+
'IE',
|
|
27
|
+
'IL',
|
|
28
|
+
'IN',
|
|
29
|
+
'IR',
|
|
30
|
+
'IS',
|
|
31
|
+
'IT',
|
|
32
|
+
'JP',
|
|
33
|
+
'KE',
|
|
34
|
+
'KR',
|
|
35
|
+
'LI',
|
|
36
|
+
'LT',
|
|
37
|
+
'LU',
|
|
38
|
+
'LV',
|
|
39
|
+
'MX',
|
|
40
|
+
'MT',
|
|
41
|
+
'NL',
|
|
42
|
+
'NO',
|
|
43
|
+
'NZ',
|
|
44
|
+
'PL',
|
|
45
|
+
'PR',
|
|
46
|
+
'PT',
|
|
47
|
+
'RO',
|
|
48
|
+
'RU',
|
|
49
|
+
'SA',
|
|
50
|
+
'SE',
|
|
51
|
+
'SI',
|
|
52
|
+
'SK',
|
|
53
|
+
'TN',
|
|
54
|
+
'TW',
|
|
55
|
+
'UA',
|
|
56
|
+
'US',
|
|
57
|
+
'ZA',
|
|
58
|
+
'ZM',
|
|
59
|
+
];
|
|
60
|
+
export function validatePostalCode(postalCode: string, country?: string): boolean {
|
|
61
|
+
if (!postalCode) return true;
|
|
62
|
+
const countryUpper = country?.toUpperCase();
|
|
63
|
+
const isSupported = country && POSTAL_CODE_SUPPORTED_COUNTRIES.includes(countryUpper as PostalCodeLocale);
|
|
64
|
+
try {
|
|
65
|
+
return isPostalCode(postalCode, isSupported ? (countryUpper as PostalCodeLocale) : 'any');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(error);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { Fade, FormLabel, InputAdornment, Stack } from '@mui/material';
|
|
3
3
|
import type { SxProps } from '@mui/material';
|
|
4
|
-
import { Controller, useFormContext } from 'react-hook-form';
|
|
4
|
+
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
5
5
|
import FormInput from '../../components/input';
|
|
6
6
|
import CountrySelect from '../../components/country-select';
|
|
7
|
+
import { validatePostalCode } from '../../libs/validator';
|
|
7
8
|
|
|
8
9
|
type Props = {
|
|
9
10
|
mode: string;
|
|
@@ -18,7 +19,7 @@ AddressForm.defaultProps = {
|
|
|
18
19
|
export default function AddressForm({ mode, stripe, sx = {} }: Props) {
|
|
19
20
|
const { t } = useLocaleContext();
|
|
20
21
|
const { control } = useFormContext();
|
|
21
|
-
|
|
22
|
+
const country = useWatch({ control, name: 'billing_address.country' });
|
|
22
23
|
if (mode === 'required') {
|
|
23
24
|
return (
|
|
24
25
|
<Fade in>
|
|
@@ -51,7 +52,13 @@ export default function AddressForm({ mode, stripe, sx = {} }: Props) {
|
|
|
51
52
|
<FormLabel className="base-label">{t('payment.checkout.billing.postal_code')}</FormLabel>
|
|
52
53
|
<FormInput
|
|
53
54
|
name="billing_address.postal_code"
|
|
54
|
-
rules={{
|
|
55
|
+
rules={{
|
|
56
|
+
required: t('payment.checkout.required'),
|
|
57
|
+
validate: (x: string) => {
|
|
58
|
+
const isValid = validatePostalCode(x, country);
|
|
59
|
+
return isValid ? true : t('payment.checkout.invalid');
|
|
60
|
+
},
|
|
61
|
+
}}
|
|
55
62
|
errorPosition="right"
|
|
56
63
|
variant="outlined"
|
|
57
64
|
placeholder={t('payment.checkout.billing.postal_code')}
|
|
@@ -91,7 +98,13 @@ export default function AddressForm({ mode, stripe, sx = {} }: Props) {
|
|
|
91
98
|
<Stack direction="row" spacing={0}>
|
|
92
99
|
<FormInput
|
|
93
100
|
name="billing_address.postal_code"
|
|
94
|
-
rules={{
|
|
101
|
+
rules={{
|
|
102
|
+
required: t('payment.checkout.required'),
|
|
103
|
+
validate: (x: string) => {
|
|
104
|
+
const isValid = validatePostalCode(x, country);
|
|
105
|
+
return isValid ? true : t('payment.checkout.invalid');
|
|
106
|
+
},
|
|
107
|
+
}}
|
|
95
108
|
errorPosition="right"
|
|
96
109
|
variant="outlined"
|
|
97
110
|
placeholder={t('payment.checkout.billing.postal_code')}
|
|
@@ -42,6 +42,7 @@ import { useMobile } from '../../hooks/mobile';
|
|
|
42
42
|
import { formatPhone, validatePhoneNumber } from '../../libs/phone-validator';
|
|
43
43
|
import LoadingButton from '../../components/loading-button';
|
|
44
44
|
import OverdueInvoicePayment from '../../components/over-due-invoice-payment';
|
|
45
|
+
import { saveCurrencyPreference } from '../../libs/currency';
|
|
45
46
|
|
|
46
47
|
export const waitForCheckoutComplete = async (sessionId: string) => {
|
|
47
48
|
let result: CheckoutContext;
|
|
@@ -220,6 +221,14 @@ export default function PaymentForm({
|
|
|
220
221
|
return index >= 0 ? index : 0;
|
|
221
222
|
});
|
|
222
223
|
|
|
224
|
+
const handleCurrencyChange = (index: number) => {
|
|
225
|
+
setPaymentCurrencyIndex(index);
|
|
226
|
+
const selectedCurrencyId = currencies[index]?.id;
|
|
227
|
+
if (selectedCurrencyId) {
|
|
228
|
+
saveCurrencyPreference(selectedCurrencyId, session?.user?.did);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
223
232
|
const onCheckoutComplete = useMemoizedFn(async ({ response }: { response: TCheckoutSession }) => {
|
|
224
233
|
if (response.id === checkoutSession.id && state.paid === false) {
|
|
225
234
|
await handleConnected();
|
|
@@ -357,6 +366,7 @@ export default function PaymentForm({
|
|
|
357
366
|
const skipBindWallet = method.type === 'stripe';
|
|
358
367
|
|
|
359
368
|
const handleConnected = async () => {
|
|
369
|
+
setState({ paying: true });
|
|
360
370
|
try {
|
|
361
371
|
const result = await waitForCheckoutComplete(checkoutSession.id);
|
|
362
372
|
if (state.paid === false) {
|
|
@@ -614,7 +624,7 @@ export default function PaymentForm({
|
|
|
614
624
|
<CurrencySelector
|
|
615
625
|
value={paymentCurrencyIndex}
|
|
616
626
|
currencies={currencies}
|
|
617
|
-
onChange={
|
|
627
|
+
onChange={handleCurrencyChange}
|
|
618
628
|
/>
|
|
619
629
|
)}
|
|
620
630
|
/>
|
|
@@ -6,13 +6,15 @@ import { useFormContext, useWatch } from 'react-hook-form';
|
|
|
6
6
|
import { defaultCountries, usePhoneInput } from 'react-international-phone';
|
|
7
7
|
import type { CountryIso2 } from 'react-international-phone';
|
|
8
8
|
|
|
9
|
+
import { useMount } from 'ahooks';
|
|
9
10
|
import FormInput from '../../components/input';
|
|
10
11
|
import { isValidCountry } from '../../libs/util';
|
|
11
12
|
import CountrySelect from '../../components/country-select';
|
|
13
|
+
import { getPhoneUtil } from '../../libs/phone-validator';
|
|
12
14
|
|
|
13
15
|
export default function PhoneInput({ ...props }) {
|
|
14
16
|
const countryFieldName = props.countryFieldName || 'billing_address.country';
|
|
15
|
-
const { control, getValues, setValue } = useFormContext();
|
|
17
|
+
const { control, getValues, setValue, trigger } = useFormContext();
|
|
16
18
|
const values = getValues();
|
|
17
19
|
|
|
18
20
|
const isUpdatingRef = useRef(false);
|
|
@@ -51,6 +53,12 @@ export default function PhoneInput({ ...props }) {
|
|
|
51
53
|
});
|
|
52
54
|
}, [userCountry, country, setCountry, safeUpdate]);
|
|
53
55
|
|
|
56
|
+
useMount(() => {
|
|
57
|
+
getPhoneUtil().catch((err) => {
|
|
58
|
+
console.error('Failed to preload phone validator:', err);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
54
62
|
const onCountryChange = useCallback(
|
|
55
63
|
(v: CountryIso2) => {
|
|
56
64
|
safeUpdate(() => {
|
|
@@ -60,6 +68,11 @@ export default function PhoneInput({ ...props }) {
|
|
|
60
68
|
[setCountry, safeUpdate]
|
|
61
69
|
);
|
|
62
70
|
|
|
71
|
+
const handleBlur = useCallback(() => {
|
|
72
|
+
trigger(props.name);
|
|
73
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
+
}, [props.name]);
|
|
75
|
+
|
|
63
76
|
return (
|
|
64
77
|
// @ts-ignore
|
|
65
78
|
<FormInput
|
|
@@ -67,6 +80,7 @@ export default function PhoneInput({ ...props }) {
|
|
|
67
80
|
onChange={handlePhoneValueChange}
|
|
68
81
|
type="tel"
|
|
69
82
|
inputRef={inputRef}
|
|
83
|
+
onBlur={handleBlur}
|
|
70
84
|
InputProps={{
|
|
71
85
|
startAdornment: (
|
|
72
86
|
<InputAdornment position="start" style={{ marginRight: '2px', marginLeft: '-8px' }}>
|