@blocklet/payment-react 1.18.33 → 1.18.35

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.
@@ -97,6 +97,12 @@ export default flat({
97
97
  slashStakeAmount: 'Slash Stake Amount',
98
98
  know: 'I know',
99
99
  relatedSubscription: 'Subscription',
100
+ connect: {
101
+ defaultScan: 'Use following methods to complete this action',
102
+ scan: 'Use following methods to complete this {action}',
103
+ confirm: 'Confirm',
104
+ cancel: 'Cancel',
105
+ },
100
106
  },
101
107
  payment: {
102
108
  checkout: {
@@ -169,7 +175,12 @@ export default flat({
169
175
  donate: 'Thanks for your tip',
170
176
  tip: 'A payment to {payee} has been completed. You can view the details of this payment in your account.',
171
177
  },
172
- confirm: 'Confirming allows {payee} to charge or reduce your staking. You can cancel or revoke staking anytime.',
178
+ confirm: {
179
+ withStake:
180
+ 'By confirming, you allow {payee} to charge your account for future payments and, if necessary, slash your stake. You can cancel your subscription or withdraw your stake at any time.',
181
+ withoutStake:
182
+ 'By confirming, you allow {payee} to charge your account for future payments. You can cancel your subscription at any time.',
183
+ },
173
184
  required: 'Required',
174
185
  invalid: 'Invalid',
175
186
  billing: {
@@ -215,6 +226,23 @@ export default flat({
215
226
  orderSummary: 'Order Summary',
216
227
  paymentDetails: 'Payment Details',
217
228
  productListTotal: 'Includes {total} items',
229
+ connectModal: {
230
+ title: '{action}',
231
+ scan: 'Use following methods to complete this payment',
232
+ confirm: 'Confirm',
233
+ cancel: 'Cancel',
234
+ },
235
+ fastPay: {
236
+ title: 'Confirm Payment',
237
+ confirmMessage: 'You will pay {amount} {symbol} from {sourceType}.',
238
+ autoPaymentReason:
239
+ 'This payment will be processed automatically due to prior delegation or sufficient balance.',
240
+ confirmPrompt: 'Please confirm the details before proceeding.',
241
+ payer: 'Account',
242
+ amount: 'Amount',
243
+ failed: 'Account changed, please pay manually.',
244
+ balanceLink: 'View Balance',
245
+ },
218
246
  },
219
247
  customer: {
220
248
  payments: 'Payment History',
@@ -287,6 +315,7 @@ export default flat({
287
315
  'By confirming this change, you allow {payee} to charge {symbol} from your account for future payments in accordance with their terms. You can always change your payment method.',
288
316
  completed:
289
317
  'Your payment method has been successfully updated, all future payments will use the new payment method unless you change it again.',
318
+ title: 'Payment Method Change',
290
319
  },
291
320
  invoice: {
292
321
  summary: 'Summary',
@@ -308,6 +337,7 @@ export default flat({
308
337
  invoiceNumber: 'Invoice Number',
309
338
  emptyList: 'No Invoice',
310
339
  noPaymentRequired: 'No Payment Required',
340
+ payBatch: 'Pay Due Invoices',
311
341
  },
312
342
  payment: {
313
343
  empty: 'There are no payments',
@@ -97,6 +97,12 @@ export default flat({
97
97
  slashStakeAmount: '罚没金额',
98
98
  know: '我知道了',
99
99
  relatedSubscription: '订阅',
100
+ connect: {
101
+ defaultScan: '使用以下方式完成本次操作',
102
+ scan: '使用以下方式完成本次{action}',
103
+ confirm: '确认',
104
+ cancel: '取消',
105
+ },
100
106
  },
101
107
  payment: {
102
108
  checkout: {
@@ -168,7 +174,11 @@ export default flat({
168
174
  donate: '感谢您的支持',
169
175
  tip: '向{payee}的付款已完成。您可以在您的账户中查看此付款的详细信息。',
170
176
  },
171
- confirm: '确认允许{payee}对您的账户进行付款或者罚没您的质押。您随时可以取消您的订阅,或者撤销质押。',
177
+ confirm: {
178
+ withStake:
179
+ '确认订阅,即表示您授权 {payee} 从您的账户扣取未来款项,并在必要时罚没质押。您可随时取消订阅或撤销质押。',
180
+ withoutStake: '确认订阅,即表示您授权 {payee} 从您的账户扣取未来款项。您可随时取消订阅。',
181
+ },
172
182
  required: '必填项',
173
183
  invalid: '无效',
174
184
  billing: {
@@ -212,6 +222,22 @@ export default flat({
212
222
  orderSummary: '订单概览',
213
223
  paymentDetails: '支付信息',
214
224
  productListTotal: '包括 {total} 项',
225
+ connectModal: {
226
+ title: '{action}',
227
+ scan: '使用以下方式完成本次支付',
228
+ confirm: '确认',
229
+ cancel: '取消',
230
+ },
231
+ fastPay: {
232
+ title: '确认支付',
233
+ confirmMessage: '您将支付 {amount} {symbol},从{sourceType}扣除。',
234
+ autoPaymentReason: '由于授权或余额充足,系统将无需钱包确认自动完成支付。',
235
+ confirmPrompt: '请确认支付信息无误后继续。',
236
+ payer: '账户地址',
237
+ amount: '支付金额',
238
+ failed: '账户发生变化,无法自动完成支付,请手动支付。',
239
+ balanceLink: '查看余额',
240
+ },
215
241
  },
216
242
  customer: {
217
243
  payments: '支付历史',
@@ -280,6 +306,7 @@ export default flat({
280
306
  submit: '确认变更',
281
307
  confirm: '确认变更方式意味着你允许 {payee} 使用新的支付方式支付你的未来账单。你可以随时再次变更支付方式。',
282
308
  completed: '你的支付方式 已经更新成功。你可以在你的账户中查看此支付方式的详细信息。',
309
+ title: '支付方式变更',
283
310
  },
284
311
  invoice: {
285
312
  summary: '摘要',
@@ -301,6 +328,7 @@ export default flat({
301
328
  invoiceNumber: '账单编号',
302
329
  emptyList: '没有账单',
303
330
  noPaymentRequired: '无需支付',
331
+ payBatch: '支付欠款',
304
332
  },
305
333
  payment: {
306
334
  empty: '没有支付记录',
@@ -4,19 +4,23 @@ import type { SxProps } from '@mui/material';
4
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
+ import { getFieldValidation, validatePostalCode } from '../../libs/validator';
8
8
 
9
9
  type Props = {
10
10
  mode: string;
11
11
  stripe: boolean;
12
12
  sx?: SxProps;
13
+ fieldValidation?: Record<string, any>;
14
+ errorPosition?: 'right' | 'bottom';
13
15
  };
14
16
 
15
17
  AddressForm.defaultProps = {
16
18
  sx: {},
19
+ fieldValidation: {},
20
+ errorPosition: 'right',
17
21
  };
18
22
 
19
- export default function AddressForm({ mode, stripe, sx = {} }: Props) {
23
+ export default function AddressForm({ mode, stripe, sx = {}, fieldValidation, errorPosition }: Props) {
20
24
  const { t } = useLocaleContext();
21
25
  const { control } = useFormContext();
22
26
  const country = useWatch({ control, name: 'billing_address.country' });
@@ -28,24 +32,33 @@ export default function AddressForm({ mode, stripe, sx = {} }: Props) {
28
32
  <FormLabel className="base-label">{t('payment.checkout.billing.line1')}</FormLabel>
29
33
  <FormInput
30
34
  name="billing_address.line1"
31
- rules={{ required: t('payment.checkout.required') }}
32
- errorPosition="right"
35
+ rules={{
36
+ required: t('payment.checkout.required'),
37
+ ...getFieldValidation('billing_address.line1', fieldValidation),
38
+ }}
39
+ errorPosition={errorPosition}
33
40
  variant="outlined"
34
41
  placeholder={t('payment.checkout.billing.line1')}
35
42
  />
36
43
  <FormLabel className="base-label">{t('payment.checkout.billing.city')}</FormLabel>
37
44
  <FormInput
38
45
  name="billing_address.city"
39
- rules={{ required: t('payment.checkout.required') }}
40
- errorPosition="right"
46
+ rules={{
47
+ required: t('payment.checkout.required'),
48
+ ...getFieldValidation('billing_address.city', fieldValidation),
49
+ }}
50
+ errorPosition={errorPosition}
41
51
  variant="outlined"
42
52
  placeholder={t('payment.checkout.billing.city')}
43
53
  />
44
54
  <FormLabel className="base-label">{t('payment.checkout.billing.state')}</FormLabel>
45
55
  <FormInput
46
56
  name="billing_address.state"
47
- rules={{ required: t('payment.checkout.required') }}
48
- errorPosition="right"
57
+ rules={{
58
+ required: t('payment.checkout.required'),
59
+ ...getFieldValidation('billing_address.state', fieldValidation),
60
+ }}
61
+ errorPosition={errorPosition}
49
62
  variant="outlined"
50
63
  placeholder={t('payment.checkout.billing.state')}
51
64
  />
@@ -58,8 +71,9 @@ export default function AddressForm({ mode, stripe, sx = {} }: Props) {
58
71
  const isValid = validatePostalCode(x, country);
59
72
  return isValid ? true : t('payment.checkout.invalid');
60
73
  },
74
+ ...getFieldValidation('billing_address.postal_code', fieldValidation),
61
75
  }}
62
- errorPosition="right"
76
+ errorPosition={errorPosition}
63
77
  variant="outlined"
64
78
  placeholder={t('payment.checkout.billing.postal_code')}
65
79
  InputProps={{
@@ -104,8 +118,9 @@ export default function AddressForm({ mode, stripe, sx = {} }: Props) {
104
118
  const isValid = validatePostalCode(x, country);
105
119
  return isValid ? true : t('payment.checkout.invalid');
106
120
  },
121
+ ...getFieldValidation('billing_address.postal_code', fieldValidation),
107
122
  }}
108
- errorPosition="right"
123
+ errorPosition={errorPosition}
109
124
  variant="outlined"
110
125
  placeholder={t('payment.checkout.billing.postal_code')}
111
126
  wrapperStyle={{ height: '40px' }}
@@ -5,7 +5,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
5
  // import { useTheme } from '@arcblock/ux/lib/Theme';
6
6
  import Toast from '@arcblock/ux/lib/Toast';
7
7
  import type { TCheckoutSession, TCustomer, TPaymentIntent, TPaymentMethodExpanded } from '@blocklet/payment-types';
8
- import { Box, Button, CircularProgress, Divider, Fade, FormLabel, Stack, Typography } from '@mui/material';
8
+ import { Box, Button, CircularProgress, Divider, Fade, FormLabel, Stack, Tooltip, Typography } from '@mui/material';
9
9
  import { useMemoizedFn, useSetState } from 'ahooks';
10
10
  import pWaitFor from 'p-wait-for';
11
11
  import { useEffect, useMemo, useRef } from 'react';
@@ -13,8 +13,11 @@ import { Controller, useFormContext, useWatch } from 'react-hook-form';
13
13
  import { joinURL } from 'ufo';
14
14
  import { dispatch } from 'use-bus';
15
15
  import isEmail from 'validator/es/lib/isEmail';
16
+ import { fromUnitToToken } from '@ocap/util';
17
+ import DID from '@arcblock/ux/lib/DID';
16
18
 
17
19
  import isEmpty from 'lodash/isEmpty';
20
+ import { HelpOutline, OpenInNew } from '@mui/icons-material';
18
21
  import FormInput from '../../components/input';
19
22
  import { usePaymentContext } from '../../contexts/payment';
20
23
  import { useSubscription } from '../../hooks/subscription';
@@ -25,6 +28,7 @@ import {
25
28
  formatQuantityInventory,
26
29
  getPrefix,
27
30
  getStatementDescriptor,
31
+ getTokenBalanceLink,
28
32
  isCrossOrigin,
29
33
  } from '../../libs/util';
30
34
  import type { CheckoutCallbacks, CheckoutContext } from '../../types';
@@ -37,6 +41,8 @@ import { formatPhone, validatePhoneNumber } from '../../libs/phone-validator';
37
41
  import LoadingButton from '../../components/loading-button';
38
42
  import OverdueInvoicePayment from '../../components/over-due-invoice-payment';
39
43
  import { saveCurrencyPreference } from '../../libs/currency';
44
+ import ConfirmDialog from '../../components/confirm';
45
+ import { getFieldValidation } from '../../libs/validator';
40
46
 
41
47
  export const waitForCheckoutComplete = async (sessionId: string) => {
42
48
  let result: CheckoutContext;
@@ -101,6 +107,14 @@ type UserInfo = {
101
107
  };
102
108
  };
103
109
 
110
+ type FastCheckoutInfo = {
111
+ open: boolean;
112
+ loading: boolean;
113
+ sourceType: 'balance' | 'delegation';
114
+ amount: string;
115
+ payer?: string;
116
+ };
117
+
104
118
  const setUserFormValues = (
105
119
  userInfo: UserInfo,
106
120
  currentValues: any,
@@ -159,10 +173,11 @@ export default function PaymentForm({
159
173
  isDonation = false,
160
174
  }: PageData) {
161
175
  // const theme = useTheme();
162
- const { t } = useLocaleContext();
176
+ const { t, locale } = useLocaleContext();
163
177
  const { isMobile } = useMobile();
164
178
  const { session, connect, payable } = usePaymentContext();
165
179
  const subscription = useSubscription('events');
180
+ const formErrorPosition = isMobile ? 'bottom' : 'right';
166
181
  const {
167
182
  control,
168
183
  getValues,
@@ -194,6 +209,7 @@ export default function PaymentForm({
194
209
  customer?: TCustomer;
195
210
  customerLimited?: boolean;
196
211
  stripePaying: boolean;
212
+ fastCheckoutInfo: FastCheckoutInfo | null;
197
213
  }>({
198
214
  submitting: false,
199
215
  paying: false,
@@ -203,6 +219,7 @@ export default function PaymentForm({
203
219
  customer,
204
220
  customerLimited: false,
205
221
  stripePaying: false,
222
+ fastCheckoutInfo: null,
206
223
  });
207
224
 
208
225
  const currencies = flattenPaymentMethods(paymentMethods);
@@ -288,6 +305,7 @@ export default function PaymentForm({
288
305
  }, [session?.user, checkoutSession.phone_number_collection?.enabled]);
289
306
 
290
307
  const paymentMethod = useWatch({ control, name: 'payment_method' });
308
+ const paymentCurrencyId = useWatch({ control, name: 'payment_currency' });
291
309
 
292
310
  // const domSize = useSize(document.body);
293
311
 
@@ -327,6 +345,9 @@ export default function PaymentForm({
327
345
  buttonText = session?.user || isDonation ? buttonText : t('payment.checkout.connect', { action: buttonText });
328
346
 
329
347
  const method = paymentMethods.find((x) => x.id === paymentMethod) as TPaymentMethodExpanded;
348
+ const paymentCurrency = currencies.find((x) => x.id === paymentCurrencyId);
349
+ const showStake = method.type === 'arcblock' && !checkoutSession.subscription_data?.no_stake;
350
+
330
351
  const isDonationMode = checkoutSession?.submit_type === 'donate' && isDonation;
331
352
  const showForm = !!session?.user;
332
353
  const skipBindWallet = method.type === 'stripe';
@@ -364,6 +385,84 @@ export default function PaymentForm({
364
385
  }
365
386
  };
366
387
 
388
+ const handleFastCheckoutConfirm = async () => {
389
+ if (!state.fastCheckoutInfo) return;
390
+
391
+ setState({
392
+ fastCheckoutInfo: {
393
+ ...state.fastCheckoutInfo,
394
+ loading: true,
395
+ },
396
+ });
397
+
398
+ try {
399
+ const result = await api.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`);
400
+ if (result.data.fastPaid) {
401
+ setState({
402
+ fastCheckoutInfo: null,
403
+ paying: true,
404
+ });
405
+ await handleConnected();
406
+ } else {
407
+ Toast.error(t('payment.checkout.fastPay.failed'));
408
+ setState({
409
+ fastCheckoutInfo: null,
410
+ paying: true,
411
+ });
412
+ openConnect();
413
+ }
414
+ } catch (err) {
415
+ console.error(err);
416
+ Toast.error(formatError(err));
417
+ setState({
418
+ fastCheckoutInfo: null,
419
+ });
420
+ }
421
+ };
422
+
423
+ const handleFastCheckoutCancel = () => {
424
+ setState({ fastCheckoutInfo: null });
425
+ };
426
+
427
+ const openConnect = () => {
428
+ try {
429
+ if (!['arcblock', 'ethereum', 'base'].includes(method.type)) {
430
+ return;
431
+ }
432
+ setState({ paying: true });
433
+ connect.open({
434
+ locale,
435
+ containerEl: undefined as unknown as Element,
436
+ action: checkoutSession.mode,
437
+ prefix: joinURL(getPrefix(), '/api/did'),
438
+ saveConnect: false,
439
+ useSocket: isCrossOrigin() === false,
440
+ extraParams: { checkoutSessionId: checkoutSession.id, sessionUserDid: session?.user?.did },
441
+ onSuccess: async () => {
442
+ connect.close();
443
+ await handleConnected();
444
+ },
445
+ onClose: () => {
446
+ connect.close();
447
+ setState({ submitting: false, paying: false });
448
+ },
449
+ onError: (err: any) => {
450
+ console.error(err);
451
+ setState({ submitting: false, paying: false });
452
+ onError(err);
453
+ },
454
+ messages: {
455
+ title: t('payment.checkout.connectModal.title', { action: buttonText }),
456
+ scan: t('payment.checkout.connectModal.scan'),
457
+ confirm: t('payment.checkout.connectModal.confirm'),
458
+ cancel: t('payment.checkout.connectModal.cancel'),
459
+ },
460
+ } as any);
461
+ } catch (err) {
462
+ Toast.error(formatError(err));
463
+ }
464
+ };
465
+
367
466
  const onFormSubmit = async (data: any) => {
368
467
  setState({ submitting: true });
369
468
  try {
@@ -383,36 +482,22 @@ export default function PaymentForm({
383
482
  });
384
483
 
385
484
  if (['arcblock', 'ethereum', 'base'].includes(method.type)) {
386
- setState({ paying: true });
387
- if ((result.data.balance?.sufficient || result.data.delegation?.sufficient) && !isDonationMode) {
388
- await handleConnected();
389
- } else {
390
- connect.open({
391
- containerEl: undefined as unknown as Element,
392
- action: checkoutSession.mode,
393
- prefix: joinURL(getPrefix(), '/api/did'),
394
- saveConnect: false,
395
- useSocket: isCrossOrigin() === false,
396
- extraParams: { checkoutSessionId: checkoutSession.id, sessionUserDid: session?.user?.did },
397
- onSuccess: async () => {
398
- connect.close();
399
- await handleConnected();
400
- },
401
- onClose: () => {
402
- connect.close();
403
- setState({ submitting: false, paying: false });
404
- },
405
- onError: (err: any) => {
406
- console.error(err);
407
- setState({ submitting: false, paying: false });
408
- onError(err);
409
- },
410
- messages: {
411
- title: 'DID Connect',
412
- scan: 'Use following methods to complete this payment',
413
- confirm: 'Confirm',
485
+ if (
486
+ (result.data.balance?.sufficient || result.data.delegation?.sufficient) &&
487
+ !isDonationMode &&
488
+ result.data.fastPayInfo
489
+ ) {
490
+ setState({
491
+ fastCheckoutInfo: {
492
+ open: true,
493
+ loading: false,
494
+ sourceType: result.data.fastPayInfo.type,
495
+ amount: result.data.fastPayInfo.amount,
496
+ payer: result.data.fastPayInfo.payer,
414
497
  },
415
- } as any);
498
+ });
499
+ } else {
500
+ openConnect();
416
501
  }
417
502
  }
418
503
  if (['stripe'].includes(method.type)) {
@@ -514,6 +599,56 @@ export default function PaymentForm({
514
599
  };
515
600
  }, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, payable]); // eslint-disable-line react-hooks/exhaustive-deps
516
601
 
602
+ const balanceLink = getTokenBalanceLink(method, state.fastCheckoutInfo?.payer || '');
603
+ const FastCheckoutConfirmDialog = state.fastCheckoutInfo && (
604
+ <ConfirmDialog
605
+ onConfirm={handleFastCheckoutConfirm}
606
+ onCancel={handleFastCheckoutCancel}
607
+ title={t('payment.checkout.fastPay.title')}
608
+ message={
609
+ <Stack>
610
+ <Typography>{t('payment.checkout.fastPay.autoPaymentReason')}</Typography>
611
+ <Typography>{t('payment.checkout.fastPay.confirmPrompt')}</Typography>
612
+ <Divider sx={{ mt: 1.5, mb: 1.5 }} />
613
+ <Stack spacing={1}>
614
+ <Stack flexDirection="row" alignItems="center" justifyContent="space-between">
615
+ <Typography color="text.primary" sx={{ whiteSpace: 'nowrap' }}>
616
+ {t('payment.checkout.fastPay.payer')}
617
+ </Typography>
618
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
619
+ <DID did={state.fastCheckoutInfo.payer || ''} compact responsive={false} />
620
+ {balanceLink && (
621
+ <Tooltip title={t('payment.checkout.fastPay.balanceLink')} placement="top">
622
+ <OpenInNew
623
+ sx={{
624
+ color: 'text.lighter',
625
+ fontSize: '0.85rem',
626
+ cursor: 'pointer',
627
+ '&:hover': { color: 'text.primary' },
628
+ }}
629
+ onClick={() => {
630
+ window.open(balanceLink, '_blank');
631
+ }}
632
+ />
633
+ </Tooltip>
634
+ )}
635
+ </Box>
636
+ </Stack>
637
+ <Stack flexDirection="row" alignItems="center" justifyContent="space-between">
638
+ <Typography color="text.primary">{t('payment.checkout.fastPay.amount')}</Typography>
639
+ <Typography>
640
+ {fromUnitToToken(state.fastCheckoutInfo.amount, paymentCurrency?.decimal || 18).toString()}{' '}
641
+ {paymentCurrency?.symbol}
642
+ </Typography>
643
+ </Stack>
644
+ </Stack>
645
+ </Stack>
646
+ }
647
+ loading={state.fastCheckoutInfo.loading}
648
+ color="primary"
649
+ />
650
+ );
651
+
517
652
  if (onlyShowBtn) {
518
653
  return (
519
654
  <>
@@ -565,6 +700,7 @@ export default function PaymentForm({
565
700
  }}
566
701
  />
567
702
  )}
703
+ {FastCheckoutConfirmDialog}
568
704
  </>
569
705
  );
570
706
  }
@@ -631,19 +767,25 @@ export default function PaymentForm({
631
767
  <FormInput
632
768
  name="customer_name"
633
769
  variant="outlined"
634
- errorPosition="right"
770
+ errorPosition={formErrorPosition}
635
771
  rules={{
636
772
  required: t('payment.checkout.required'),
773
+ ...getFieldValidation('customer_name', checkoutSession.metadata?.page_info?.field_validation, locale),
637
774
  }}
638
775
  />
639
776
  <FormLabel className="base-label">{t('payment.checkout.customer.email')}</FormLabel>
640
777
  <FormInput
641
778
  name="customer_email"
642
779
  variant="outlined"
643
- errorPosition="right"
780
+ errorPosition={formErrorPosition}
644
781
  rules={{
645
782
  required: t('payment.checkout.required'),
646
783
  validate: (x) => (isEmail(x) ? true : t('payment.checkout.invalid')),
784
+ ...getFieldValidation(
785
+ 'customer_email',
786
+ checkoutSession.metadata?.page_info?.field_validation,
787
+ locale
788
+ ),
647
789
  }}
648
790
  />
649
791
  {checkoutSession.phone_number_collection?.enabled && (
@@ -652,7 +794,7 @@ export default function PaymentForm({
652
794
  <PhoneInput
653
795
  name="customer_phone"
654
796
  variant="outlined"
655
- errorPosition="right"
797
+ errorPosition={formErrorPosition}
656
798
  placeholder="Phone number"
657
799
  rules={{
658
800
  required: t('payment.checkout.required'),
@@ -660,6 +802,11 @@ export default function PaymentForm({
660
802
  const isValid = await validatePhoneNumber(x);
661
803
  return isValid ? true : t('payment.checkout.invalid');
662
804
  },
805
+ ...getFieldValidation(
806
+ 'customer_phone',
807
+ checkoutSession.metadata?.page_info?.field_validation,
808
+ locale
809
+ ),
663
810
  }}
664
811
  />
665
812
  </>
@@ -668,6 +815,8 @@ export default function PaymentForm({
668
815
  mode={checkoutSession.billing_address_collection as string}
669
816
  stripe={method?.type === 'stripe'}
670
817
  sx={{ marginTop: '0 !important' }}
818
+ fieldValidation={checkoutSession.metadata?.page_info?.field_validation}
819
+ errorPosition={formErrorPosition}
671
820
  />
672
821
  </Stack>
673
822
  )}
@@ -695,9 +844,21 @@ export default function PaymentForm({
695
844
 
696
845
  {['subscription', 'setup'].includes(checkoutSession.mode) && (
697
846
  <Typography sx={{ mt: 2.5, color: 'text.lighter', fontSize: '0.7875rem', lineHeight: '0.9625rem' }}>
698
- {t('payment.checkout.confirm', { payee })}
847
+ {showStake
848
+ ? t('payment.checkout.confirm.withStake', { payee })
849
+ : t('payment.checkout.confirm.withoutStake', { payee })}
699
850
  </Typography>
700
851
  )}
852
+ {checkoutSession.metadata?.page_info?.form_purpose_description && (
853
+ <Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 0.5 }}>
854
+ <HelpOutline sx={{ color: 'text.lighter', fontSize: '0.75rem' }} />
855
+ <Typography variant="body2" sx={{ fontSize: '0.75rem', color: 'text.lighter' }}>
856
+ {locale === 'zh'
857
+ ? checkoutSession.metadata.page_info.form_purpose_description.zh
858
+ : checkoutSession.metadata.page_info.form_purpose_description.en}
859
+ </Typography>
860
+ </Box>
861
+ )}
701
862
  </Stack>
702
863
  </Fade>
703
864
  {state.customerLimited && (
@@ -725,6 +886,7 @@ export default function PaymentForm({
725
886
  }}
726
887
  />
727
888
  )}
889
+ {FastCheckoutConfirmDialog}
728
890
  </>
729
891
  );
730
892
  }