@blocklet/payment-react 1.18.15 → 1.18.17

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.
@@ -8,7 +8,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
8
8
  import Toast from '@arcblock/ux/lib/Toast';
9
9
  import type { Paginated, TInvoiceExpanded, TSubscription } from '@blocklet/payment-types';
10
10
  import { OpenInNewOutlined } from '@mui/icons-material';
11
- import { Box, Button, CircularProgress, Hidden, Stack, Typography } from '@mui/material';
11
+ import { Box, Button, CircularProgress, Stack, Typography, Tooltip } from '@mui/material';
12
12
  import { styled } from '@mui/system';
13
13
  import { useInfiniteScroll, useRequest, useSetState } from 'ahooks';
14
14
  import React, { useEffect, useRef, useState } from 'react';
@@ -69,6 +69,7 @@ type Props = {
69
69
  action?: string;
70
70
  type?: 'list' | 'table';
71
71
  onTableDataChange?: Function;
72
+ relatedSubscription?: boolean;
72
73
  };
73
74
 
74
75
  const getInvoiceLink = (invoice: TInvoiceExpanded, action?: string) => {
@@ -104,6 +105,7 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
104
105
  include_return_staking,
105
106
  include_recovered_from,
106
107
  onTableDataChange,
108
+ relatedSubscription,
107
109
  } = props;
108
110
  const listKey = 'invoice-table';
109
111
  const { t, locale } = useLocaleContext();
@@ -177,6 +179,12 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
177
179
  handleNavigation(e, link, navigate, { target: link.external ? '_blank' : target });
178
180
  };
179
181
 
182
+ const handleRelatedSubscriptionClick = (e: React.MouseEvent, invoice: TInvoiceExpanded) => {
183
+ if (invoice.subscription_id) {
184
+ handleNavigation(e, createLink(`/customer/subscription/${invoice.subscription_id}`), navigate);
185
+ }
186
+ };
187
+
180
188
  const columns = [
181
189
  {
182
190
  label: t('common.amount'),
@@ -186,9 +194,10 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
186
194
  options: {
187
195
  customBodyRenderLite: (_: string, index: number) => {
188
196
  const invoice = data?.list[index] as TInvoiceExpanded;
197
+ const isVoid = invoice.status === 'void';
189
198
  return (
190
199
  <Box onClick={(e) => handleLinkClick(e, invoice)} sx={linkStyle}>
191
- <Typography>
200
+ <Typography sx={isVoid ? { textDecoration: 'line-through' } : {}}>
192
201
  {formatBNStr(invoice.total, invoice.paymentCurrency.decimal)}&nbsp;
193
202
  {invoice.paymentCurrency.symbol}
194
203
  </Typography>
@@ -225,6 +234,30 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
225
234
  },
226
235
  },
227
236
  },
237
+ ...(relatedSubscription
238
+ ? [
239
+ {
240
+ label: t('common.relatedSubscription'),
241
+ name: 'subscription',
242
+ options: {
243
+ customBodyRenderLite: (_: string, index: number) => {
244
+ const invoice = data?.list[index] as TInvoiceExpanded;
245
+ return invoice.subscription_id ? (
246
+ <Box
247
+ onClick={(e) => handleRelatedSubscriptionClick(e, invoice)}
248
+ sx={{ color: 'text.link', cursor: 'pointer' }}>
249
+ {invoice.subscription?.description}
250
+ </Box>
251
+ ) : (
252
+ <Box onClick={(e) => handleLinkClick(e, invoice)} sx={{ ...linkStyle, color: 'text.lighter' }}>
253
+ {t('common.none')}
254
+ </Box>
255
+ );
256
+ },
257
+ },
258
+ },
259
+ ]
260
+ : []),
228
261
  {
229
262
  label: t('common.updatedAt'),
230
263
  name: 'name',
@@ -234,12 +267,17 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
234
267
 
235
268
  return (
236
269
  <Box onClick={(e) => handleLinkClick(e, invoice)} sx={linkStyle}>
237
- {formatToDate(invoice.created_at, locale, 'YYYY-MM-DD HH:mm:ss')}
270
+ {formatToDate(
271
+ invoice.created_at,
272
+ locale,
273
+ relatedSubscription ? 'YYYY-MM-DD HH:mm' : 'YYYY-MM-DD HH:mm:ss'
274
+ )}
238
275
  </Box>
239
276
  );
240
277
  },
241
278
  },
242
279
  },
280
+
243
281
  {
244
282
  label: t('common.description'),
245
283
  name: '',
@@ -264,6 +302,8 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
264
302
  const invoice = data?.list[index] as TInvoiceExpanded;
265
303
  const hidePay = invoice.billing_reason === 'overdraft-protection';
266
304
  const { connect } = getInvoiceLink(invoice, action);
305
+ const isVoid = invoice.status === 'void';
306
+
267
307
  if (action && !hidePay) {
268
308
  return connect ? (
269
309
  <Button variant="text" size="small" onClick={() => onPay(invoice.id)} sx={{ color: 'text.link' }}>
@@ -283,7 +323,15 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
283
323
  }
284
324
  return (
285
325
  <Box onClick={(e) => handleLinkClick(e, invoice)} sx={linkStyle}>
286
- <Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
326
+ {isVoid ? (
327
+ <Tooltip title={t('payment.customer.invoice.noPaymentRequired')} arrow placement="top">
328
+ <span>
329
+ <Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
330
+ </span>
331
+ </Tooltip>
332
+ ) : (
333
+ <Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
334
+ )}
287
335
  </Box>
288
336
  );
289
337
  },
@@ -448,6 +496,7 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
448
496
  <Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
449
497
  {invoices.map((invoice) => {
450
498
  const { link, connect } = getInvoiceLink(invoice, action);
499
+ const isVoid = invoice.status === 'void';
451
500
  return (
452
501
  <Stack
453
502
  key={invoice.id}
@@ -469,15 +518,19 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
469
518
  <Stack direction="row" alignItems="center" spacing={0.5}>
470
519
  <Typography component="span">{invoice.number}</Typography>
471
520
  {link.external && (
472
- <Hidden mdDown>
473
- <OpenInNewOutlined fontSize="small" sx={{ color: 'text.secondary' }} />
474
- </Hidden>
521
+ <OpenInNewOutlined
522
+ fontSize="small"
523
+ sx={{
524
+ color: 'text.secondary',
525
+ display: { xs: 'none', md: 'inline-flex' },
526
+ }}
527
+ />
475
528
  )}
476
529
  </Stack>
477
530
  </a>
478
531
  </Box>
479
532
  <Box flex={1} textAlign="right">
480
- <Typography>
533
+ <Typography sx={isVoid ? { textDecoration: 'line-through' } : {}}>
481
534
  {formatBNStr(invoice.total, invoice.paymentCurrency.decimal)}&nbsp;
482
535
  {invoice.paymentCurrency.symbol}
483
536
  </Typography>
@@ -486,11 +539,13 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
486
539
  <Typography>{formatToDate(invoice.created_at, locale, 'HH:mm:ss')}</Typography>
487
540
  </Box>
488
541
  {!action && (
489
- <Hidden mdDown>
490
- <Box flex={2} className="invoice-description" textAlign="right">
491
- <Typography>{invoice.description || invoice.id}</Typography>
492
- </Box>
493
- </Hidden>
542
+ <Box
543
+ flex={2}
544
+ className="invoice-description"
545
+ textAlign="right"
546
+ sx={{ display: { xs: 'none', lg: 'inline-flex' } }}>
547
+ <Typography>{invoice.description || invoice.id}</Typography>
548
+ </Box>
494
549
  )}
495
550
  <Box flex={1} textAlign="right">
496
551
  {action ? (
@@ -514,6 +569,12 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
514
569
  {t('payment.customer.invoice.pay')}
515
570
  </Button>
516
571
  )
572
+ ) : isVoid ? (
573
+ <Tooltip title={t('payment.customer.invoice.noPaymentRequired')} arrow placement="top">
574
+ <span>
575
+ <Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
576
+ </span>
577
+ </Tooltip>
517
578
  ) : (
518
579
  <Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
519
580
  )}
@@ -597,13 +658,11 @@ CustomerInvoiceList.defaultProps = {
597
658
  action: '',
598
659
  type: 'list',
599
660
  onTableDataChange: () => {},
661
+ relatedSubscription: false,
600
662
  };
601
663
 
602
664
  const Root = styled(Stack)`
603
665
  @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
604
- .invoice-description {
605
- display: none !important;
606
- }
607
666
  svg.MuiSvgIcon-root {
608
667
  display: none !important;
609
668
  }
@@ -96,6 +96,7 @@ export default flat({
96
96
  stakeAmount: 'Stake Amount',
97
97
  slashStakeAmount: 'Slash Stake Amount',
98
98
  know: 'I know',
99
+ relatedSubscription: 'Subscription',
99
100
  },
100
101
  payment: {
101
102
  checkout: {
@@ -244,14 +245,14 @@ export default flat({
244
245
  pastDue: {
245
246
  button: 'Pay',
246
247
  invoices: 'Past Due Invoices',
247
- warning:
248
- 'Past due invoices need to be paid immediately, otherwise you can not make new purchases anymore. Please pay these invoices one by one.',
248
+ warning: 'Past due invoices need to be paid immediately, otherwise you can not make new purchases anymore.',
249
249
  alert: {
250
250
  title: 'You have unpaid invoices',
251
251
  description:
252
252
  'Seems you have unpaid invoices from previous subscriptions, new purchases are not allowed unless you have paid all past due invoices.',
253
253
  confirm: 'Pay Now',
254
254
  },
255
+ view: 'View Due Invoices',
255
256
  },
256
257
  recover: {
257
258
  button: 'Resume Subscription',
@@ -305,6 +306,7 @@ export default flat({
305
306
  next: 'No invoices yet, next invoice will be generated on {date}',
306
307
  invoiceNumber: 'Invoice Number',
307
308
  emptyList: 'No Invoice',
309
+ noPaymentRequired: 'No Payment Required',
308
310
  },
309
311
  payment: {
310
312
  empty: 'There are no payments',
@@ -326,6 +328,12 @@ export default flat({
326
328
  trialLeft: 'Trail Left',
327
329
  owner: 'Subscription Owner',
328
330
  },
331
+ overdue: {
332
+ title:
333
+ 'You have {count} due invoices for {subscriptionCount} subscriptions, totaling {total} {symbol}{method}. Please pay immediately to avoid service disruption.',
334
+ simpleTitle: 'You have {count} due invoices. Please pay now to ensure uninterrupted service.',
335
+ empty: 'Great! You have no due invoices.',
336
+ },
329
337
  },
330
338
  invoice: {
331
339
  reason: {
@@ -357,16 +365,18 @@ export default flat({
357
365
  simpleTitle:
358
366
  'There are {count} due invoices for your subscription {name}, you need to pay them to activate your subscription or before making new purchases.',
359
367
  title:
360
- 'There are {count} due invoices for your subscription {name}, the total due amount is {total} {symbol}, you need to pay them to activate your subscription or before making new purchases.',
368
+ 'There are {count} due invoices for your subscription {name}, the total due amount is {total} {symbol}{method}, you need to pay them to activate your subscription or before making new purchases.',
361
369
  payNow: 'Pay Now',
362
370
  notSupport: 'This payment method is not supported',
363
- total: 'Total {total} {currency}',
371
+ total: 'Total {total} {currency}{method}',
364
372
  view: 'View Subscription Details',
365
373
  viewNow: 'View Now',
366
374
  pastDue: 'Past Due Invoices',
367
375
  description: 'If you have any questions, you can choose ',
368
376
  list: 'Past Due Invoices:',
369
377
  empty: 'There are no overdue invoices for your subscription {name}.',
378
+ retry: 'Retry',
379
+ paid: 'Paid',
370
380
  },
371
381
  },
372
382
  },
@@ -96,6 +96,7 @@ export default flat({
96
96
  stakeAmount: '质押金额',
97
97
  slashStakeAmount: '罚没金额',
98
98
  know: '我知道了',
99
+ relatedSubscription: '订阅',
99
100
  },
100
101
  payment: {
101
102
  checkout: {
@@ -239,12 +240,13 @@ export default flat({
239
240
  pastDue: {
240
241
  button: '续费',
241
242
  invoices: '欠费帐单',
242
- warning: '请尽快支付欠费账单,否则你将无法继续使用服务或购买新服务,请逐个打开账单详情并完成支付',
243
+ warning: '请尽快支付欠费账单,否则你将无法继续使用服务或购买新服务。',
243
244
  alert: {
244
245
  title: '你有欠费账单',
245
246
  description: '看起来你有欠费的账单,在你支付所有欠费账单之前,新的购买或者订阅将被禁止,请不要调皮。',
246
247
  confirm: '去支付',
247
248
  },
249
+ view: '查看欠费明细',
248
250
  },
249
251
  recover: {
250
252
  button: '恢复订阅',
@@ -295,6 +297,7 @@ export default flat({
295
297
  next: '还没有账单,下次账单将在 {date} 生成',
296
298
  invoiceNumber: '账单编号',
297
299
  emptyList: '没有账单',
300
+ noPaymentRequired: '无需支付',
298
301
  },
299
302
  payment: {
300
303
  empty: '没有支付记录',
@@ -316,6 +319,12 @@ export default flat({
316
319
  trialLeft: '剩余试用时长',
317
320
  owner: '订阅拥有者',
318
321
  },
322
+ overdue: {
323
+ title:
324
+ '您有 {count} 张欠费账单,涉及 {subscriptionCount} 个订阅,总金额 {total} {symbol}{method}。请立即支付,以免影响您的使用。',
325
+ simpleTitle: '您有 {count} 张欠费账单,请立即支付,以免影响您的使用。',
326
+ empty: '恭喜!您当前没有欠费账单。',
327
+ },
319
328
  },
320
329
  invoice: {
321
330
  reason: {
@@ -345,18 +354,20 @@ export default flat({
345
354
  subscription: {
346
355
  overdue: {
347
356
  title:
348
- '您的【{name}】订阅共有 {count} 张欠费账单,总计 {total} {symbol},您需要支付这些账单以激活您的订阅,或在进行新的购买之前完成支付。',
357
+ '您的【{name}】订阅共有 {count} 张欠费账单,总计 {total} {symbol}{method},您需要支付这些账单以激活您的订阅,或在进行新的购买之前完成支付。',
349
358
  simpleTitle:
350
359
  '您的【{name}】订阅共有 {count} 张欠费账单,您需要支付这些账单以激活您的订阅,或在进行新的购买之前完成支付。',
351
360
  payNow: '立即支付',
352
361
  notSupport: '暂不支持该支付方式',
353
- total: '总计 {total} {currency}',
362
+ total: '总计 {total} {currency}{method}',
354
363
  view: '查看订阅详情',
355
364
  pastDue: '欠费账单',
356
365
  viewNow: '立即查看',
357
366
  description: '如果您有任何疑问,可以选择 ',
358
367
  list: '欠费账单:',
359
368
  empty: '您的【{name}】订阅当前没有欠费账单',
369
+ retry: '重新支付',
370
+ paid: '已支付',
360
371
  },
361
372
  },
362
373
  },
@@ -343,6 +343,7 @@ function PaymentInner({
343
343
  mode={mode}
344
344
  action={action}
345
345
  onlyShowBtn
346
+ isDonation
346
347
  />
347
348
  </Box>
348
349
  )}
@@ -23,45 +23,7 @@ export default function AddressForm({ mode, stripe, sx = {} }: Props) {
23
23
  return (
24
24
  <Fade in>
25
25
  <Stack className="cko-payment-address cko-payment-form" sx={sx}>
26
- <FormLabel className="base-label">{t(`payment.checkout.billing.${mode}`)}</FormLabel>
27
26
  <Stack direction="column" className="cko-payment-form" spacing={0}>
28
- <Stack direction="row" spacing={0}>
29
- <FormInput
30
- name="billing_address.postal_code"
31
- rules={{ required: t('payment.checkout.required') }}
32
- errorPosition="right"
33
- variant="outlined"
34
- placeholder={t('payment.checkout.billing.postal_code')}
35
- InputProps={{
36
- startAdornment: (
37
- <InputAdornment position="start" style={{ marginRight: '2px', marginLeft: '-8px' }}>
38
- <Controller
39
- name="billing_address.country"
40
- control={control}
41
- render={({ field }) => (
42
- <CountrySelect
43
- {...field}
44
- sx={{
45
- '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
46
- borderColor: 'transparent',
47
- },
48
- }}
49
- />
50
- )}
51
- />
52
- </InputAdornment>
53
- ),
54
- }}
55
- />
56
- </Stack>
57
- <FormLabel className="base-label">{t('payment.checkout.billing.state')}</FormLabel>
58
- <FormInput
59
- name="billing_address.state"
60
- rules={{ required: t('payment.checkout.required') }}
61
- errorPosition="right"
62
- variant="outlined"
63
- placeholder={t('payment.checkout.billing.state')}
64
- />
65
27
  <FormLabel className="base-label">{t('payment.checkout.billing.line1')}</FormLabel>
66
28
  <FormInput
67
29
  name="billing_address.line1"
@@ -78,6 +40,42 @@ export default function AddressForm({ mode, stripe, sx = {} }: Props) {
78
40
  variant="outlined"
79
41
  placeholder={t('payment.checkout.billing.city')}
80
42
  />
43
+ <FormLabel className="base-label">{t('payment.checkout.billing.state')}</FormLabel>
44
+ <FormInput
45
+ name="billing_address.state"
46
+ rules={{ required: t('payment.checkout.required') }}
47
+ errorPosition="right"
48
+ variant="outlined"
49
+ placeholder={t('payment.checkout.billing.state')}
50
+ />
51
+ <FormLabel className="base-label">{t('payment.checkout.billing.postal_code')}</FormLabel>
52
+ <FormInput
53
+ name="billing_address.postal_code"
54
+ rules={{ required: t('payment.checkout.required') }}
55
+ errorPosition="right"
56
+ variant="outlined"
57
+ placeholder={t('payment.checkout.billing.postal_code')}
58
+ InputProps={{
59
+ startAdornment: (
60
+ <InputAdornment position="start" style={{ marginRight: '2px', marginLeft: '-8px' }}>
61
+ <Controller
62
+ name="billing_address.country"
63
+ control={control}
64
+ render={({ field }) => (
65
+ <CountrySelect
66
+ {...field}
67
+ sx={{
68
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
69
+ borderColor: 'transparent',
70
+ },
71
+ }}
72
+ />
73
+ )}
74
+ />
75
+ </InputAdornment>
76
+ ),
77
+ }}
78
+ />
81
79
  </Stack>
82
80
  </Stack>
83
81
  </Fade>
@@ -80,10 +80,12 @@ export const hasDidWallet = (user: any) => {
80
80
  type PageData = CheckoutContext &
81
81
  CheckoutCallbacks & {
82
82
  onlyShowBtn?: boolean;
83
+ isDonation?: boolean;
83
84
  };
84
85
 
85
86
  PaymentForm.defaultProps = {
86
87
  onlyShowBtn: false,
88
+ isDonation: false,
87
89
  };
88
90
 
89
91
  // FIXME: https://stripe.com/docs/elements/address-element
@@ -103,6 +105,7 @@ export default function PaymentForm({
103
105
  action,
104
106
  currencyId,
105
107
  onlyShowBtn,
108
+ isDonation = false,
106
109
  }: PageData) {
107
110
  // const theme = useTheme();
108
111
  const { t } = useLocaleContext();
@@ -241,10 +244,10 @@ export default function PaymentForm({
241
244
  } else {
242
245
  buttonText = t(`payment.checkout.${checkoutSession.mode}`);
243
246
  }
244
- buttonText = session?.user ? buttonText : t('payment.checkout.connect', { action: buttonText });
247
+ buttonText = session?.user || isDonation ? buttonText : t('payment.checkout.connect', { action: buttonText });
245
248
 
246
249
  const method = paymentMethods.find((x) => x.id === paymentMethod) as TPaymentMethodExpanded;
247
-
250
+ const isDonationMode = checkoutSession?.submit_type === 'donate' && isDonation;
248
251
  const showForm = session?.user;
249
252
  const skipBindWallet = method.type === 'stripe';
250
253
 
@@ -305,7 +308,12 @@ export default function PaymentForm({
305
308
  const onFormSubmit = async (data: any) => {
306
309
  setState({ submitting: true });
307
310
  try {
308
- const result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, data);
311
+ let result;
312
+ if (isDonationMode) {
313
+ result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`, data);
314
+ } else {
315
+ result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, data);
316
+ }
309
317
 
310
318
  setState({
311
319
  paymentIntent: result.data.paymentIntent,
@@ -317,7 +325,7 @@ export default function PaymentForm({
317
325
 
318
326
  if (['arcblock', 'ethereum', 'base'].includes(method.type)) {
319
327
  setState({ paying: true });
320
- if (result.data.balance?.sufficient || result.data.delegation?.sufficient) {
328
+ if ((result.data.balance?.sufficient || result.data.delegation?.sufficient) && !isDonationMode) {
321
329
  await handleConnected();
322
330
  } else {
323
331
  connect.open({
@@ -396,6 +404,10 @@ export default function PaymentForm({
396
404
  });
397
405
  }
398
406
  } else {
407
+ if (isDonationMode) {
408
+ handleSubmit(onFormSubmit, onFormError)();
409
+ return;
410
+ }
399
411
  session?.login(() => {
400
412
  setState({ submitting: true });
401
413
  onUserLoggedIn()