@blocklet/payment-react 1.18.19 → 1.18.21

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.
@@ -36,6 +36,7 @@ import ProductDonation from './product-donation';
36
36
  import ConfirmDialog from '../components/confirm';
37
37
  import PaymentBeneficiaries, { TBeneficiary } from '../components/payment-beneficiaries';
38
38
  import DonationSkeleton from './skeleton/donation';
39
+ import { formatPhone } from '../libs/phone-validator';
39
40
 
40
41
  const getBenefits = async (id: string, url?: string) => {
41
42
  const { data } = await api.get(`/api/payment-links/${id}/benefits?${url ? `url=${url}` : ''}`);
@@ -74,6 +75,7 @@ function PaymentInner({
74
75
  }: MainProps) {
75
76
  const { t } = useLocaleContext();
76
77
  const { settings, session } = usePaymentContext();
78
+ const { isMobile } = useMobile();
77
79
  const [state, setState] = useSetState({
78
80
  checkoutSession,
79
81
  submitting: false,
@@ -101,20 +103,24 @@ function PaymentInner({
101
103
  defaultValues: {
102
104
  customer_name: customer?.name || session?.user?.fullName || '',
103
105
  customer_email: customer?.email || session?.user?.email || '',
104
- customer_phone: customer?.phone || session?.user?.phone || '',
106
+ customer_phone: formatPhone(customer?.phone || session?.user?.phone || ''),
105
107
  payment_method: defaultMethodId,
106
108
  payment_currency: defaultCurrencyId,
107
109
  billing_address: Object.assign(
108
110
  {
109
- country: '',
110
- state: '',
111
- city: '',
112
- line1: '',
113
- line2: '',
114
- postal_code: '',
111
+ country: session?.user?.address?.country || '',
112
+ state: session?.user?.address?.province || '',
113
+ city: session?.user?.address?.city || '',
114
+ line1: session?.user?.address?.line1 || '',
115
+ line2: session?.user?.address?.line2 || '',
116
+ postal_code: session?.user?.address?.postalCode || '',
115
117
  },
116
118
  customer?.address || {},
117
- { country: isValidCountry(customer?.address?.country || '') ? customer?.address?.country : 'us' }
119
+ {
120
+ country: isValidCountry(customer?.address?.country || session?.user?.address?.country || '')
121
+ ? customer?.address?.country
122
+ : 'us',
123
+ }
118
124
  ),
119
125
  },
120
126
  });
@@ -238,17 +244,49 @@ function PaymentInner({
238
244
  )}
239
245
 
240
246
  <Stack sx={{ display: benefitsState.open ? 'none' : 'block' }}>
241
- <Typography
242
- title={t('payment.checkout.orderSummary')}
243
- sx={{
244
- color: 'text.primary',
245
- fontSize: '18px',
246
- fontWeight: '500',
247
- lineHeight: '24px',
248
- mb: 2,
249
- }}>
250
- {t('payment.checkout.donation.tipAmount')}
251
- </Typography>
247
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
248
+ <Typography
249
+ title={t('payment.checkout.orderSummary')}
250
+ sx={{
251
+ color: 'text.primary',
252
+ fontSize: '18px',
253
+ fontWeight: '500',
254
+ lineHeight: '24px',
255
+ }}>
256
+ {t('payment.checkout.donation.tipAmount')}
257
+ </Typography>
258
+
259
+ {!isMobile && donationSettings?.amount?.presets && donationSettings.amount.presets.length > 0 && (
260
+ <Typography
261
+ sx={{
262
+ color: 'text.secondary',
263
+ fontSize: '13px',
264
+ display: 'flex',
265
+ alignItems: 'center',
266
+ gap: 0.5,
267
+ opacity: 0.8,
268
+ }}>
269
+ <Box
270
+ component="span"
271
+ sx={{
272
+ border: '1px solid',
273
+ borderColor: 'divider',
274
+ borderRadius: 0.75,
275
+ px: 0.75,
276
+ py: 0.25,
277
+ fontSize: '12px',
278
+ lineHeight: 1,
279
+ color: 'text.secondary',
280
+ fontWeight: '400',
281
+ bgcolor: 'transparent',
282
+ }}>
283
+ Tab
284
+ </Box>
285
+ {t('payment.checkout.donation.tabHint')}
286
+ </Typography>
287
+ )}
288
+ </Stack>
289
+
252
290
  {items.map((x: TLineItemExpanded) => (
253
291
  <ProductDonation
254
292
  key={`${x.price_id}-${currency.id}`}
@@ -21,7 +21,6 @@ import { dispatch } from 'use-bus';
21
21
  import isEmail from 'validator/es/lib/isEmail';
22
22
 
23
23
  import isEmpty from 'lodash/isEmpty';
24
- import ConfirmDialog from '../../components/confirm';
25
24
  import FormInput from '../../components/input';
26
25
  import { usePaymentContext } from '../../contexts/payment';
27
26
  import { useSubscription } from '../../hooks/subscription';
@@ -40,8 +39,9 @@ import CurrencySelector from './currency';
40
39
  import PhoneInput from './phone';
41
40
  import StripeCheckout from './stripe';
42
41
  import { useMobile } from '../../hooks/mobile';
43
- import { validatePhoneNumber } from '../../libs/phone-validator';
42
+ import { formatPhone, validatePhoneNumber } from '../../libs/phone-validator';
44
43
  import LoadingButton from '../../components/loading-button';
44
+ import OverdueInvoicePayment from '../../components/over-due-invoice-payment';
45
45
 
46
46
  export const waitForCheckoutComplete = async (sessionId: string) => {
47
47
  let result: CheckoutContext;
@@ -83,6 +83,63 @@ type PageData = CheckoutContext &
83
83
  isDonation?: boolean;
84
84
  };
85
85
 
86
+ type UserInfo = {
87
+ name?: string;
88
+ fullName?: string;
89
+ email?: string;
90
+ phone?: string;
91
+ address?: {
92
+ country?: string;
93
+ state?: string;
94
+ province?: string;
95
+ line1?: string;
96
+ line2?: string;
97
+ city?: string;
98
+ postal_code?: string;
99
+ postalCode?: string;
100
+ };
101
+ metadata?: {
102
+ phone?: {
103
+ country?: string;
104
+ phoneNumber?: string;
105
+ };
106
+ };
107
+ };
108
+
109
+ const setUserFormValues = (
110
+ userInfo: UserInfo,
111
+ currentValues: any,
112
+ setValue: Function,
113
+ options: { preferExisting?: boolean; showPhone?: boolean } = {}
114
+ ) => {
115
+ const { preferExisting = true } = options;
116
+ const basicFields = {
117
+ customer_name: userInfo.name || userInfo.fullName,
118
+ customer_email: userInfo.email,
119
+ customer_phone: formatPhone(userInfo.phone),
120
+ };
121
+ const addressFields: Record<string, any> = {
122
+ 'billing_address.state': userInfo.address?.state || userInfo.address?.province,
123
+ 'billing_address.line1': userInfo.address?.line1,
124
+ 'billing_address.line2': userInfo.address?.line2,
125
+ 'billing_address.city': userInfo.address?.city,
126
+ 'billing_address.postal_code': userInfo.address?.postal_code || userInfo.address?.postalCode,
127
+ 'billing_address.country': userInfo.address?.country,
128
+ };
129
+
130
+ if (options.showPhone) {
131
+ addressFields['billing_address.country'] = userInfo.metadata?.phone?.country || userInfo.address?.country;
132
+ }
133
+
134
+ const allFields = { ...addressFields, ...basicFields };
135
+
136
+ Object.entries(allFields).forEach(([field, value]) => {
137
+ if (!preferExisting || !currentValues[field.split('.')[0]]) {
138
+ setValue(field, value);
139
+ }
140
+ });
141
+ };
142
+
86
143
  PaymentForm.defaultProps = {
87
144
  onlyShowBtn: false,
88
145
  isDonation: false,
@@ -185,22 +242,30 @@ export default function PaymentForm({
185
242
  useEffect(() => {
186
243
  if (session?.user) {
187
244
  const values = getValues();
188
- if (!values.customer_name) {
189
- setValue('customer_name', session.user.fullName);
190
- }
191
- if (!values.customer_email) {
192
- setValue('customer_email', session.user.email);
193
- }
194
- if (!values.customer_phone) {
195
- setValue('customer_phone', session.user.phone);
196
- }
197
- }
198
- if (!session?.user) {
199
- setValue('customer_name', '');
200
- setValue('customer_email', '');
201
- setValue('customer_phone', '');
245
+ setUserFormValues(session.user, values, setValue, {
246
+ preferExisting: false,
247
+ showPhone: checkoutSession.phone_number_collection?.enabled,
248
+ });
249
+ } else {
250
+ setUserFormValues(
251
+ {
252
+ name: '',
253
+ email: '',
254
+ phone: '',
255
+ address: {
256
+ state: '',
257
+ line1: '',
258
+ line2: '',
259
+ city: '',
260
+ postal_code: '',
261
+ },
262
+ },
263
+ {},
264
+ setValue,
265
+ { preferExisting: false, showPhone: checkoutSession.phone_number_collection?.enabled }
266
+ );
202
267
  }
203
- }, [session?.user, getValues, setValue]);
268
+ }, [session?.user, getValues, setValue, checkoutSession.phone_number_collection?.enabled]);
204
269
 
205
270
  useEffect(() => {
206
271
  setValue('payment_method', (currencies[paymentCurrencyIndex] as any)?.method?.id);
@@ -248,7 +313,7 @@ export default function PaymentForm({
248
313
 
249
314
  const method = paymentMethods.find((x) => x.id === paymentMethod) as TPaymentMethodExpanded;
250
315
  const isDonationMode = checkoutSession?.submit_type === 'donate' && isDonation;
251
- const showForm = session?.user;
316
+ const showForm = !!session?.user;
252
317
  const skipBindWallet = method.type === 'stripe';
253
318
 
254
319
  const handleConnected = async () => {
@@ -275,33 +340,9 @@ export default function PaymentForm({
275
340
  const { data: profile } = await api.get('/api/customers/me?fallback=1');
276
341
  if (profile) {
277
342
  const values = getValues();
278
- if (!values.customer_name) {
279
- setValue('customer_name', profile.name);
280
- }
281
- if (!values.customer_email) {
282
- setValue('customer_email', profile.email);
283
- }
284
- if (!values.customer_phone) {
285
- setValue('customer_phone', profile.phone);
286
- }
287
- if (profile.address?.country) {
288
- setValue('billing_address.country', profile.address.country);
289
- }
290
- if (profile.address?.state) {
291
- setValue('billing_address.state', profile.address.state);
292
- }
293
- if (profile.address?.line1) {
294
- setValue('billing_address.line1', profile.address.line1);
295
- }
296
- if (profile.address?.line2) {
297
- setValue('billing_address.line2', profile.address.line2);
298
- }
299
- if (profile.address?.city) {
300
- setValue('billing_address.city', profile.address.city);
301
- }
302
- if (profile.address?.postal_code) {
303
- setValue('billing_address.postal_code', profile.address.postal_code);
304
- }
343
+ setUserFormValues(profile, values, setValue, {
344
+ showPhone: checkoutSession.phone_number_collection?.enabled,
345
+ });
305
346
  }
306
347
  };
307
348
 
@@ -473,18 +514,31 @@ export default function PaymentForm({
473
514
  </Button>
474
515
  </Box>
475
516
  {state.customerLimited && (
476
- <ConfirmDialog
477
- onConfirm={() =>
478
- window.open(
479
- joinURL(getPrefix(), `/customer/invoice/past-due?referer=${encodeURIComponent(window.location.href)}`),
480
- '_self'
481
- )
482
- }
483
- onCancel={() => setState({ customerLimited: false })}
484
- confirm={t('payment.customer.pastDue.alert.confirm')}
485
- title={t('payment.customer.pastDue.alert.title')}
486
- message={t('payment.customer.pastDue.alert.description')}
487
- color="primary"
517
+ <OverdueInvoicePayment
518
+ customerId={customer?.id || session?.user?.did}
519
+ onPaid={() => {
520
+ setState({ customerLimited: false });
521
+ onAction();
522
+ }}
523
+ alertMessage={t('payment.customer.pastDue.alert.customMessage')}
524
+ detailLinkOptions={{
525
+ enabled: true,
526
+ onClick: () => {
527
+ setState({ customerLimited: false });
528
+ window.open(
529
+ joinURL(
530
+ getPrefix(),
531
+ `/customer/invoice/past-due?referer=${encodeURIComponent(window.location.href)}`
532
+ ),
533
+ '_self'
534
+ );
535
+ },
536
+ }}
537
+ dialogProps={{
538
+ open: state.customerLimited,
539
+ onClose: () => setState({ customerLimited: false }),
540
+ title: t('payment.customer.pastDue.alert.title'),
541
+ }}
488
542
  />
489
543
  )}
490
544
  </>
@@ -619,18 +673,28 @@ export default function PaymentForm({
619
673
  </Stack>
620
674
  </Fade>
621
675
  {state.customerLimited && (
622
- <ConfirmDialog
623
- onConfirm={() =>
624
- window.open(
625
- joinURL(getPrefix(), `/customer/invoice/past-due?referer=${encodeURIComponent(window.location.href)}`),
626
- '_self'
627
- )
628
- }
629
- onCancel={() => setState({ customerLimited: false })}
630
- confirm={t('payment.customer.pastDue.alert.confirm')}
631
- title={t('payment.customer.pastDue.alert.title')}
632
- message={t('payment.customer.pastDue.alert.description')}
633
- color="primary"
676
+ <OverdueInvoicePayment
677
+ customerId={customer?.id || session?.user?.didssion?.user?.did}
678
+ onPaid={() => {
679
+ setState({ customerLimited: false });
680
+ onAction();
681
+ }}
682
+ alertMessage={t('payment.customer.pastDue.alert.customMessage')}
683
+ detailLinkOptions={{
684
+ enabled: true,
685
+ onClick: () => {
686
+ setState({ customerLimited: false });
687
+ window.open(
688
+ joinURL(getPrefix(), `/customer/invoice/past-due?referer=${encodeURIComponent(window.location.href)}`),
689
+ '_self'
690
+ );
691
+ },
692
+ }}
693
+ dialogProps={{
694
+ open: state.customerLimited,
695
+ onClose: () => setState({ customerLimited: false }),
696
+ title: t('payment.customer.pastDue.alert.title'),
697
+ }}
634
698
  />
635
699
  )}
636
700
  </>
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/prop-types */
2
2
  import { InputAdornment } from '@mui/material';
3
3
  import omit from 'lodash/omit';
4
- import { useEffect } from 'react';
4
+ import { useEffect, useRef, useCallback } from 'react';
5
5
  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';
@@ -15,28 +15,50 @@ export default function PhoneInput({ ...props }) {
15
15
  const { control, getValues, setValue } = useFormContext();
16
16
  const values = getValues();
17
17
 
18
+ const isUpdatingRef = useRef(false);
19
+
20
+ const safeUpdate = useCallback((callback: () => void) => {
21
+ if (isUpdatingRef.current) return;
22
+
23
+ try {
24
+ isUpdatingRef.current = true;
25
+ callback();
26
+ } finally {
27
+ requestAnimationFrame(() => {
28
+ isUpdatingRef.current = false;
29
+ });
30
+ }
31
+ }, []);
32
+
18
33
  const { phone, handlePhoneValueChange, inputRef, country, setCountry } = usePhoneInput({
19
34
  defaultCountry: isValidCountry(values[countryFieldName]) ? values[countryFieldName] : 'us',
20
35
  value: values[props.name] || '',
21
36
  countries: defaultCountries,
22
- onChange: (data: any) => {
23
- setValue(props.name, data.phone);
24
- setValue(countryFieldName, data.country);
37
+ onChange: (data) => {
38
+ safeUpdate(() => {
39
+ setValue(props.name, data.phone);
40
+ setValue(countryFieldName, data.country);
41
+ });
25
42
  },
26
43
  });
27
44
 
28
45
  const userCountry = useWatch({ control, name: countryFieldName });
29
46
  useEffect(() => {
30
- if (userCountry !== country) {
47
+ if (!userCountry || userCountry === country) return;
48
+
49
+ safeUpdate(() => {
31
50
  setCountry(userCountry);
32
- }
33
- // eslint-disable-next-line react-hooks/exhaustive-deps
34
- }, [userCountry]);
51
+ });
52
+ }, [userCountry, country, setCountry, safeUpdate]);
35
53
 
36
- const onCountryChange = (v: CountryIso2) => {
37
- setCountry(v);
38
- setValue(countryFieldName, v);
39
- };
54
+ const onCountryChange = useCallback(
55
+ (v: CountryIso2) => {
56
+ safeUpdate(() => {
57
+ setCountry(v);
58
+ });
59
+ },
60
+ [setCountry, safeUpdate]
61
+ );
40
62
 
41
63
  return (
42
64
  // @ts-ignore
@@ -41,6 +41,7 @@ import PaymentSkeleton from './skeleton/payment';
41
41
  import PaymentSuccess from './success';
42
42
  import PaymentSummary from './summary';
43
43
  import { useMobile } from '../hooks/mobile';
44
+ import { formatPhone } from '../libs/phone-validator';
44
45
 
45
46
  // eslint-disable-next-line react/no-unused-prop-types
46
47
  type Props = CheckoutContext & CheckoutCallbacks & { completed?: boolean; error?: any; showCheckoutSummary?: boolean };
@@ -78,20 +79,24 @@ function PaymentInner({
78
79
  defaultValues: {
79
80
  customer_name: customer?.name || session?.user?.fullName || '',
80
81
  customer_email: customer?.email || session?.user?.email || '',
81
- customer_phone: customer?.phone || session?.user?.phone || '',
82
+ customer_phone: formatPhone(customer?.phone || session?.user?.phone || ''),
82
83
  payment_method: defaultMethodId,
83
84
  payment_currency: defaultCurrencyId,
84
85
  billing_address: Object.assign(
85
86
  {
86
- country: '',
87
- state: '',
88
- city: '',
89
- line1: '',
90
- line2: '',
91
- postal_code: '',
87
+ country: session?.user?.address?.country || '',
88
+ state: session?.user?.address?.province || '',
89
+ city: session?.user?.address?.city || '',
90
+ line1: session?.user?.address?.line1 || '',
91
+ line2: session?.user?.address?.line2 || '',
92
+ postal_code: session?.user?.address?.postalCode || '',
92
93
  },
93
94
  customer?.address || {},
94
- { country: isValidCountry(customer?.address?.country || '') ? customer?.address?.country : 'us' }
95
+ {
96
+ country: isValidCountry(customer?.address?.country || session?.user?.address?.country || '')
97
+ ? customer?.address?.country
98
+ : 'us',
99
+ }
95
100
  ),
96
101
  },
97
102
  });
@@ -48,10 +48,6 @@ export default function ProductDonation({
48
48
  };
49
49
 
50
50
  const getDefaultPreset = () => {
51
- if (settings?.amount?.preset) {
52
- return formatAmount(settings.amount.preset);
53
- }
54
-
55
51
  try {
56
52
  const savedPreset = localStorage.getItem(getUserStorageKey(DONATION_PRESET_KEY_BASE));
57
53
  if (savedPreset) {
@@ -67,7 +63,13 @@ export default function ProductDonation({
67
63
  }
68
64
  if (presets.length > 0) {
69
65
  const middleIndex = Math.floor(presets.length / 2);
70
- return presets[middleIndex] || presets[0];
66
+ return presets[middleIndex];
67
+ }
68
+ if (settings?.amount?.preset) {
69
+ return formatAmount(settings.amount.preset);
70
+ }
71
+ if (presets.length > 0) {
72
+ return presets[0];
71
73
  }
72
74
  return '0';
73
75
  };