@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
|
@@ -49,6 +49,7 @@ function StripeCheckoutForm({
|
|
|
49
49
|
loaded: false,
|
|
50
50
|
showBillingForm: false,
|
|
51
51
|
isTransitioning: false,
|
|
52
|
+
paymentMethod: 'card',
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
const handlePaymentMethodChange = (event: any) => {
|
|
@@ -61,6 +62,7 @@ function StripeCheckoutForm({
|
|
|
61
62
|
setTimeout(() => {
|
|
62
63
|
setState({
|
|
63
64
|
isTransitioning: false,
|
|
65
|
+
paymentMethod: method,
|
|
64
66
|
showBillingForm: true,
|
|
65
67
|
});
|
|
66
68
|
}, 300);
|
|
@@ -68,6 +70,7 @@ function StripeCheckoutForm({
|
|
|
68
70
|
// if shouldShowForm is false, set showBillingForm to false immediately
|
|
69
71
|
setState({
|
|
70
72
|
showBillingForm: false,
|
|
73
|
+
paymentMethod: method,
|
|
71
74
|
isTransitioning: false,
|
|
72
75
|
});
|
|
73
76
|
}
|
|
@@ -113,15 +116,16 @@ function StripeCheckoutForm({
|
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
try {
|
|
116
|
-
setState({ confirming: true });
|
|
119
|
+
setState({ confirming: true, message: '' });
|
|
117
120
|
const method = intentType === 'payment_intent' ? 'confirmPayment' : 'confirmSetup';
|
|
118
121
|
|
|
119
122
|
const { error: submitError } = await elements.submit();
|
|
120
123
|
if (submitError) {
|
|
124
|
+
setState({ confirming: false });
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
const { error } = await stripe[method]({
|
|
128
|
+
const { error, paymentIntent, setupIntent } = await stripe[method]({
|
|
125
129
|
elements,
|
|
126
130
|
redirect: 'if_required',
|
|
127
131
|
confirmParams: {
|
|
@@ -148,6 +152,12 @@ function StripeCheckoutForm({
|
|
|
148
152
|
: {}),
|
|
149
153
|
},
|
|
150
154
|
});
|
|
155
|
+
const intent = paymentIntent || setupIntent;
|
|
156
|
+
if (intent?.status === 'canceled' || intent?.status === 'requires_payment_method') {
|
|
157
|
+
setState({ confirming: false });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
setState({ confirming: false });
|
|
152
162
|
if (error) {
|
|
153
163
|
if (error.type === 'validation_error') {
|
|
@@ -163,17 +173,18 @@ function StripeCheckoutForm({
|
|
|
163
173
|
setState({ confirming: false, message: err.message as string });
|
|
164
174
|
}
|
|
165
175
|
},
|
|
166
|
-
[customer, intentType, stripe] // eslint-disable-line
|
|
176
|
+
[customer, intentType, stripe, state.showBillingForm, returnUrl] // eslint-disable-line
|
|
167
177
|
);
|
|
168
178
|
|
|
169
179
|
return (
|
|
170
180
|
<Content onSubmit={handleSubmit}>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
{(!state.paymentMethod || state.paymentMethod === 'card') && (
|
|
182
|
+
<LinkAuthenticationElement
|
|
183
|
+
options={{
|
|
184
|
+
defaultEmail: customer.email,
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
)}
|
|
177
188
|
<PaymentElementContainer className={!state.isTransitioning ? 'visible' : ''}>
|
|
178
189
|
<PaymentElement
|
|
179
190
|
options={{
|
package/src/payment/index.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import { Box, Fade, Stack, type BoxProps } from '@mui/material';
|
|
|
16
16
|
import { styled } from '@mui/system';
|
|
17
17
|
import { fromTokenToUnit } from '@ocap/util';
|
|
18
18
|
import { useSetState } from 'ahooks';
|
|
19
|
-
import { useEffect, useState } from 'react';
|
|
19
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
20
20
|
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
|
21
21
|
import trim from 'lodash/trim';
|
|
22
22
|
import type { LiteralUnion } from 'type-fest';
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import type { CheckoutCallbacks, CheckoutContext, CheckoutFormData } from '../types';
|
|
35
35
|
import PaymentError from './error';
|
|
36
36
|
import CheckoutFooter from './footer';
|
|
37
|
-
import PaymentForm from './form';
|
|
37
|
+
import PaymentForm, { hasDidWallet } from './form';
|
|
38
38
|
// import PaymentHeader from './header';
|
|
39
39
|
import OverviewSkeleton from './skeleton/overview';
|
|
40
40
|
import PaymentSkeleton from './skeleton/payment';
|
|
@@ -42,6 +42,7 @@ import PaymentSuccess from './success';
|
|
|
42
42
|
import PaymentSummary from './summary';
|
|
43
43
|
import { useMobile } from '../hooks/mobile';
|
|
44
44
|
import { formatPhone } from '../libs/phone-validator';
|
|
45
|
+
import { getCurrencyPreference } from '../libs/currency';
|
|
45
46
|
|
|
46
47
|
// eslint-disable-next-line react/no-unused-prop-types
|
|
47
48
|
type Props = CheckoutContext & CheckoutCallbacks & { completed?: boolean; error?: any; showCheckoutSummary?: boolean };
|
|
@@ -70,8 +71,48 @@ function PaymentInner({
|
|
|
70
71
|
const { isMobile } = useMobile();
|
|
71
72
|
const [state, setState] = useSetState<{ checkoutSession: TCheckoutSessionExpanded }>({ checkoutSession });
|
|
72
73
|
const query = getQueryParams(window.location.href);
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
|
|
75
|
+
const availableCurrencyIds = useMemo(() => {
|
|
76
|
+
const currencyIds = new Set<string>();
|
|
77
|
+
paymentMethods.forEach((method) => {
|
|
78
|
+
method.payment_currencies.forEach((currency) => {
|
|
79
|
+
if (currency.active) {
|
|
80
|
+
currencyIds.add(currency.id);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return Array.from(currencyIds);
|
|
85
|
+
}, [paymentMethods]);
|
|
86
|
+
|
|
87
|
+
const defaultCurrencyId = useMemo(() => {
|
|
88
|
+
// 1. first check url currencyId
|
|
89
|
+
if (query.currencyId && availableCurrencyIds.includes(query.currencyId)) {
|
|
90
|
+
return query.currencyId;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. if user has no wallet, use the first available currency of stripe payment method
|
|
94
|
+
if (session?.user && !hasDidWallet(session.user)) {
|
|
95
|
+
const stripeCurrencyId = paymentMethods
|
|
96
|
+
.find((m) => m.type === 'stripe')
|
|
97
|
+
?.payment_currencies.find((c) => c.active)?.id;
|
|
98
|
+
if (stripeCurrencyId) {
|
|
99
|
+
return stripeCurrencyId;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. then check user's saved currency preference
|
|
104
|
+
const savedPreference = getCurrencyPreference(session?.user?.did, availableCurrencyIds);
|
|
105
|
+
if (savedPreference) {
|
|
106
|
+
return savedPreference;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. finally use the currency in checkoutSession or the first available currency
|
|
110
|
+
if (state.checkoutSession.currency_id && availableCurrencyIds.includes(state.checkoutSession.currency_id)) {
|
|
111
|
+
return state.checkoutSession.currency_id;
|
|
112
|
+
}
|
|
113
|
+
return availableCurrencyIds?.[0];
|
|
114
|
+
}, [query.currencyId, availableCurrencyIds, session?.user, state.checkoutSession.currency_id, paymentMethods]);
|
|
115
|
+
|
|
75
116
|
const defaultMethodId = paymentMethods.find((m) => m.payment_currencies.some((c) => c.id === defaultCurrencyId))?.id;
|
|
76
117
|
const hideSummaryCard = mode.endsWith('-minimal') || !showCheckoutSummary;
|
|
77
118
|
|
|
@@ -120,16 +161,6 @@ function PaymentInner({
|
|
|
120
161
|
};
|
|
121
162
|
}, []);
|
|
122
163
|
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (!methods || query.currencyId) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (state.checkoutSession.currency_id !== defaultCurrencyId) {
|
|
128
|
-
methods.setValue('payment_currency', state.checkoutSession.currency_id);
|
|
129
|
-
}
|
|
130
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
-
}, [state.checkoutSession, defaultCurrencyId, query.currencyId]);
|
|
132
|
-
|
|
133
164
|
const currencyId = useWatch({ control: methods.control, name: 'payment_currency', defaultValue: defaultCurrencyId });
|
|
134
165
|
const currency =
|
|
135
166
|
(findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) ||
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import type { DonationSettings, TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
|
|
3
|
-
import { Avatar, Box, Card, CardActionArea, Grid, Stack, TextField, Typography } from '@mui/material';
|
|
3
|
+
import { Avatar, Box, Card, CardActionArea, Grid, Stack, TextField, Typography, IconButton } from '@mui/material';
|
|
4
|
+
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
|
4
5
|
import { useSetState } from 'ahooks';
|
|
5
6
|
import { useEffect, useRef } from 'react';
|
|
6
7
|
|
|
@@ -78,6 +79,7 @@ export default function ProductDonation({
|
|
|
78
79
|
input: defaultCustomAmount,
|
|
79
80
|
custom: !supportPreset || defaultPreset === 'custom',
|
|
80
81
|
error: '',
|
|
82
|
+
animating: false,
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
const customInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -91,15 +93,98 @@ export default function ProductDonation({
|
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
const handleCustomSelect = () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
// Set custom mode and prepare for random amount
|
|
97
|
+
setState({
|
|
98
|
+
custom: true,
|
|
99
|
+
selected: '',
|
|
100
|
+
animating: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Prepare random amount range with correctly sorted presets
|
|
104
|
+
const hasPresets = presets.length > 0;
|
|
105
|
+
let sortedPresets: number[] = [];
|
|
106
|
+
if (hasPresets) {
|
|
107
|
+
sortedPresets = [...presets].map((p) => parseFloat(p)).sort((a, b) => a - b);
|
|
102
108
|
}
|
|
109
|
+
|
|
110
|
+
// Get min and max values for random amount
|
|
111
|
+
const minPreset = hasPresets ? sortedPresets[0] : 1;
|
|
112
|
+
const middleIndex = Math.floor(sortedPresets.length / 2);
|
|
113
|
+
const maxPreset = hasPresets ? sortedPresets[middleIndex] : 10;
|
|
114
|
+
|
|
115
|
+
// Detect precision from existing presets
|
|
116
|
+
const detectPrecision = () => {
|
|
117
|
+
let maxPrecision = 2; // Default to 2 decimal places for currency
|
|
118
|
+
|
|
119
|
+
// If no presets, default to 0 precision (integers) for simplicity
|
|
120
|
+
if (!hasPresets) return 0;
|
|
121
|
+
|
|
122
|
+
const allIntegers = presets.every((preset) => {
|
|
123
|
+
const num = parseFloat(preset);
|
|
124
|
+
return num === Math.floor(num);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (allIntegers) return 0;
|
|
128
|
+
|
|
129
|
+
presets.forEach((preset) => {
|
|
130
|
+
const decimalPart = preset.toString().split('.')[1];
|
|
131
|
+
if (decimalPart) {
|
|
132
|
+
maxPrecision = Math.max(maxPrecision, decimalPart.length);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return maxPrecision;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const precision = detectPrecision();
|
|
140
|
+
|
|
141
|
+
// Generate random amount with matching precision
|
|
142
|
+
let randomAmount;
|
|
143
|
+
if (precision === 0) {
|
|
144
|
+
randomAmount = (Math.round(Math.random() * (maxPreset - minPreset) + minPreset) || 1).toString();
|
|
145
|
+
} else {
|
|
146
|
+
randomAmount = (Math.random() * (maxPreset - minPreset) + minPreset).toFixed(precision);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get starting value for animation - use either current input, cached value, or 0
|
|
150
|
+
const startValue = state.input ? parseFloat(state.input) : 0;
|
|
151
|
+
const targetValue = parseFloat(randomAmount);
|
|
152
|
+
const difference = targetValue - startValue;
|
|
153
|
+
|
|
154
|
+
// Animate value change
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
const duration = 800;
|
|
157
|
+
|
|
158
|
+
const updateCounter = () => {
|
|
159
|
+
const currentTime = Date.now();
|
|
160
|
+
const elapsed = currentTime - startTime;
|
|
161
|
+
|
|
162
|
+
if (elapsed < duration) {
|
|
163
|
+
const progress = elapsed / duration;
|
|
164
|
+
const intermediateValue = startValue + difference * progress;
|
|
165
|
+
const currentValue =
|
|
166
|
+
precision === 0 ? Math.floor(intermediateValue).toString() : intermediateValue.toFixed(precision);
|
|
167
|
+
|
|
168
|
+
setState({ input: currentValue });
|
|
169
|
+
requestAnimationFrame(updateCounter);
|
|
170
|
+
} else {
|
|
171
|
+
// Animation complete
|
|
172
|
+
setState({
|
|
173
|
+
input: randomAmount,
|
|
174
|
+
animating: false,
|
|
175
|
+
error: '',
|
|
176
|
+
});
|
|
177
|
+
onChange({ priceId: item.price_id, amount: formatAmount(randomAmount) });
|
|
178
|
+
setPayable(true);
|
|
179
|
+
localStorage.setItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE), formatAmount(randomAmount));
|
|
180
|
+
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
customInputRef.current?.focus();
|
|
183
|
+
}, 200);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
requestAnimationFrame(updateCounter);
|
|
103
188
|
localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), 'custom');
|
|
104
189
|
};
|
|
105
190
|
|
|
@@ -111,7 +196,6 @@ export default function ProductDonation({
|
|
|
111
196
|
}
|
|
112
197
|
};
|
|
113
198
|
|
|
114
|
-
// 使用useTabNavigation进行键盘导航
|
|
115
199
|
const { handleKeyDown } = useTabNavigation(presets, handleTabSelect, {
|
|
116
200
|
includeCustom: supportCustom,
|
|
117
201
|
currentValue: state.custom ? undefined : state.selected,
|
|
@@ -286,14 +370,49 @@ export default function ProductDonation({
|
|
|
286
370
|
InputProps={{
|
|
287
371
|
endAdornment: (
|
|
288
372
|
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ ml: 1 }}>
|
|
373
|
+
<IconButton
|
|
374
|
+
size="small"
|
|
375
|
+
onClick={handleCustomSelect}
|
|
376
|
+
disabled={state.animating}
|
|
377
|
+
sx={{
|
|
378
|
+
mr: 0.5,
|
|
379
|
+
opacity: state.animating ? 0.5 : 1,
|
|
380
|
+
transition: 'all 0.2s ease',
|
|
381
|
+
'&:hover': {
|
|
382
|
+
transform: 'scale(1.2)',
|
|
383
|
+
transition: 'transform 0.3s ease',
|
|
384
|
+
},
|
|
385
|
+
}}
|
|
386
|
+
aria-label={t('common.random')}>
|
|
387
|
+
<AutoAwesomeIcon fontSize="small" />
|
|
388
|
+
</IconButton>
|
|
289
389
|
<Avatar src={currency?.logo} sx={{ width: 16, height: 16 }} alt={currency?.symbol} />
|
|
290
390
|
<Typography>{currency?.symbol}</Typography>
|
|
291
391
|
</Stack>
|
|
292
392
|
),
|
|
293
393
|
autoComplete: 'off',
|
|
394
|
+
sx: {
|
|
395
|
+
'& input': {
|
|
396
|
+
transition: 'all 0.25s ease',
|
|
397
|
+
},
|
|
398
|
+
},
|
|
294
399
|
}}
|
|
295
400
|
sx={{
|
|
296
401
|
mt: defaultPreset !== '0' ? 0 : 1,
|
|
402
|
+
'& .MuiInputBase-root': {
|
|
403
|
+
transition: 'all 0.3s ease',
|
|
404
|
+
},
|
|
405
|
+
'& input[type=number]': {
|
|
406
|
+
MozAppearance: 'textfield',
|
|
407
|
+
},
|
|
408
|
+
'& input[type=number]::-webkit-outer-spin-button': {
|
|
409
|
+
WebkitAppearance: 'none',
|
|
410
|
+
margin: 0,
|
|
411
|
+
},
|
|
412
|
+
'& input[type=number]::-webkit-inner-spin-button': {
|
|
413
|
+
WebkitAppearance: 'none',
|
|
414
|
+
margin: 0,
|
|
415
|
+
},
|
|
297
416
|
}}
|
|
298
417
|
/>
|
|
299
418
|
)}
|