@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.
Files changed (59) hide show
  1. package/es/checkout/donate.js +11 -1
  2. package/es/components/country-select.js +243 -21
  3. package/es/components/over-due-invoice-payment.d.ts +3 -1
  4. package/es/components/over-due-invoice-payment.js +6 -4
  5. package/es/contexts/payment.d.ts +2 -1
  6. package/es/contexts/payment.js +8 -1
  7. package/es/index.d.ts +1 -0
  8. package/es/index.js +1 -0
  9. package/es/libs/api.js +4 -0
  10. package/es/libs/currency.d.ts +3 -0
  11. package/es/libs/currency.js +22 -0
  12. package/es/libs/phone-validator.js +2 -0
  13. package/es/libs/validator.d.ts +1 -0
  14. package/es/libs/validator.js +70 -0
  15. package/es/payment/form/address.js +17 -3
  16. package/es/payment/form/index.js +10 -1
  17. package/es/payment/form/phone.js +12 -1
  18. package/es/payment/form/stripe/form.js +14 -5
  19. package/es/payment/index.js +33 -11
  20. package/es/payment/product-donation.js +110 -12
  21. package/es/types/shims.d.ts +2 -0
  22. package/lib/checkout/donate.js +11 -1
  23. package/lib/components/country-select.js +243 -39
  24. package/lib/components/over-due-invoice-payment.d.ts +3 -1
  25. package/lib/components/over-due-invoice-payment.js +7 -4
  26. package/lib/contexts/payment.d.ts +2 -1
  27. package/lib/contexts/payment.js +9 -1
  28. package/lib/index.d.ts +1 -0
  29. package/lib/index.js +12 -0
  30. package/lib/libs/api.js +4 -0
  31. package/lib/libs/currency.d.ts +3 -0
  32. package/lib/libs/currency.js +31 -0
  33. package/lib/libs/phone-validator.js +1 -0
  34. package/lib/libs/validator.d.ts +1 -0
  35. package/lib/libs/validator.js +20 -0
  36. package/lib/payment/form/address.js +15 -2
  37. package/lib/payment/form/index.js +12 -1
  38. package/lib/payment/form/phone.js +13 -1
  39. package/lib/payment/form/stripe/form.js +21 -5
  40. package/lib/payment/index.js +34 -10
  41. package/lib/payment/product-donation.js +106 -15
  42. package/lib/types/shims.d.ts +2 -0
  43. package/package.json +8 -8
  44. package/src/checkout/donate.tsx +11 -1
  45. package/src/components/country-select.tsx +265 -20
  46. package/src/components/over-due-invoice-payment.tsx +6 -2
  47. package/src/contexts/payment.tsx +11 -1
  48. package/src/index.ts +1 -0
  49. package/src/libs/api.ts +4 -1
  50. package/src/libs/currency.ts +25 -0
  51. package/src/libs/phone-validator.ts +1 -0
  52. package/src/libs/validator.ts +70 -0
  53. package/src/payment/form/address.tsx +17 -4
  54. package/src/payment/form/index.tsx +11 -1
  55. package/src/payment/form/phone.tsx +15 -1
  56. package/src/payment/form/stripe/form.tsx +20 -9
  57. package/src/payment/index.tsx +45 -14
  58. package/src/payment/product-donation.tsx +129 -10
  59. 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
- <LinkAuthenticationElement
172
- options={{
173
- defaultEmail: customer.email,
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={{
@@ -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
- const defaultCurrencyId =
74
- query.currencyId || state.checkoutSession.currency_id || state.checkoutSession.line_items[0]?.price.currency_id;
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
- setState({ custom: true, selected: '', error: '' });
95
- const savedCustomAmount = getSavedCustomAmount();
96
- if (savedCustomAmount) {
97
- setState({ input: savedCustomAmount });
98
- onChange({ priceId: item.price_id, amount: savedCustomAmount });
99
- setPayable(true);
100
- } else if (!state.input) {
101
- setPayable(false);
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
  )}
@@ -16,3 +16,5 @@ declare module 'pretty-ms-i18n';
16
16
  declare var blocklet: import('@blocklet/sdk').WindowBlocklet;
17
17
 
18
18
  declare var __PAYMENT_KIT_BASE_URL: string;
19
+
20
+ declare var __PAYMENT_KIT_AUTH_TOKEN: string;