payment-kit 1.15.32 → 1.15.34

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 (40) hide show
  1. package/api/src/integrations/stripe/handlers/setup-intent.ts +3 -1
  2. package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
  3. package/api/src/integrations/stripe/resource.ts +0 -11
  4. package/api/src/libs/invoice.ts +202 -1
  5. package/api/src/libs/notification/template/subscription-canceled.ts +11 -2
  6. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
  7. package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
  8. package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
  9. package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
  10. package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
  11. package/api/src/libs/payment.ts +3 -2
  12. package/api/src/libs/subscription.ts +33 -14
  13. package/api/src/queues/invoice.ts +1 -0
  14. package/api/src/queues/payment.ts +3 -1
  15. package/api/src/queues/refund.ts +9 -8
  16. package/api/src/queues/subscription.ts +109 -38
  17. package/api/src/routes/checkout-sessions.ts +20 -4
  18. package/api/src/routes/connect/change-payment.ts +51 -34
  19. package/api/src/routes/connect/change-plan.ts +25 -3
  20. package/api/src/routes/connect/setup.ts +27 -6
  21. package/api/src/routes/connect/shared.ts +135 -1
  22. package/api/src/routes/connect/subscribe.ts +25 -3
  23. package/api/src/routes/invoices.ts +23 -105
  24. package/api/src/routes/subscriptions.ts +66 -17
  25. package/api/src/store/models/invoice.ts +2 -1
  26. package/blocklet.yml +1 -1
  27. package/package.json +4 -4
  28. package/src/components/invoice/list.tsx +47 -24
  29. package/src/components/pricing-table/payment-settings.tsx +1 -1
  30. package/src/components/subscription/actions/cancel.tsx +10 -7
  31. package/src/components/subscription/metrics.tsx +1 -1
  32. package/src/pages/admin/billing/invoices/detail.tsx +15 -0
  33. package/src/pages/admin/billing/invoices/index.tsx +1 -1
  34. package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
  35. package/src/pages/admin/customers/customers/detail.tsx +1 -0
  36. package/src/pages/customer/index.tsx +1 -1
  37. package/src/pages/customer/invoice/detail.tsx +28 -14
  38. package/src/pages/customer/subscription/change-plan.tsx +8 -1
  39. package/src/pages/customer/subscription/detail.tsx +4 -4
  40. package/src/pages/customer/subscription/embed.tsx +3 -1
@@ -8,6 +8,7 @@ import {
8
8
  getInvoiceStatusColor,
9
9
  Table,
10
10
  useDefaultPageSize,
11
+ getInvoiceDescriptionAndReason,
11
12
  } from '@blocklet/payment-react';
12
13
  import type { TInvoiceExpanded } from '@blocklet/payment-types';
13
14
  import { CircularProgress, Typography } from '@mui/material';
@@ -29,6 +30,7 @@ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TInvoiceEx
29
30
  }
30
31
  search.set(key, String(v));
31
32
  });
33
+
32
34
  return api.get(`/api/invoices?${search.toString()}`).then((res) => res.data);
33
35
  };
34
36
 
@@ -54,6 +56,7 @@ type ListProps = {
54
56
  status?: string;
55
57
  ignore_zero?: boolean;
56
58
  include_staking?: boolean;
59
+ include_return_staking?: boolean;
57
60
 
58
61
  mode?: 'admin' | 'customer';
59
62
  };
@@ -78,6 +81,7 @@ InvoiceList.defaultProps = {
78
81
  subscription_id: '',
79
82
  status: '',
80
83
  include_staking: false,
84
+ include_return_staking: false,
81
85
  ignore_zero: false,
82
86
 
83
87
  mode: 'admin',
@@ -89,32 +93,38 @@ export default function InvoiceList({
89
93
  features,
90
94
  status,
91
95
  include_staking,
96
+ include_return_staking,
92
97
  ignore_zero,
93
98
  mode,
94
99
  }: ListProps) {
95
100
  const listKey = getListKey({ customer_id, subscription_id });
96
101
 
97
- const { t } = useLocaleContext();
102
+ const { t, locale } = useLocaleContext();
98
103
  const defaultPageSize = useDefaultPageSize(20);
99
- const [search, setSearch] = useLocalStorageState<SearchProps & { ignore_zero?: boolean; include_staking?: boolean }>(
100
- listKey,
101
- {
102
- defaultValue: {
103
- status: status as string,
104
- customer_id,
105
- subscription_id,
106
- pageSize: defaultPageSize,
107
- page: 1,
108
- ignore_zero: !!ignore_zero,
109
- include_staking: !!include_staking,
110
- },
111
- }
112
- );
104
+ const [search, setSearch] = useLocalStorageState<
105
+ SearchProps & { ignore_zero?: boolean; include_staking?: boolean; include_return_staking?: boolean }
106
+ >(listKey, {
107
+ defaultValue: {
108
+ status: status as string,
109
+ customer_id,
110
+ subscription_id,
111
+ pageSize: defaultPageSize,
112
+ page: 1,
113
+ ignore_zero: !!ignore_zero,
114
+ include_staking: !!include_staking,
115
+ include_return_staking: !!include_return_staking,
116
+ },
117
+ });
113
118
 
114
119
  const [data, setData] = useState({}) as any;
115
120
 
116
121
  const refresh = () =>
117
- fetchData(search).then((res: any) => {
122
+ fetchData({
123
+ ...search,
124
+ include_staking: !!include_staking,
125
+ ignore_zero: !!ignore_zero,
126
+ include_return_staking: !!include_return_staking,
127
+ }).then((res: any) => {
118
128
  setData(res);
119
129
  });
120
130
 
@@ -160,6 +170,20 @@ export default function InvoiceList({
160
170
  },
161
171
  },
162
172
  },
173
+ {
174
+ label: t('common.type'),
175
+ name: 'billing_reason',
176
+ options: {
177
+ customBodyRenderLite: (_: string, index: number) => {
178
+ const item = data.list[index] as TInvoiceExpanded;
179
+ return (
180
+ <Link to={`/admin/billing/${item.id}`}>
181
+ <Status label={getInvoiceDescriptionAndReason(item, locale)?.type} />
182
+ </Link>
183
+ );
184
+ },
185
+ },
186
+ },
163
187
  {
164
188
  label: t('admin.invoice.number'),
165
189
  name: 'number',
@@ -176,14 +200,11 @@ export default function InvoiceList({
176
200
  options: {
177
201
  customBodyRenderLite: (_: string, index: number) => {
178
202
  const item = data.list[index] as TInvoiceExpanded;
179
- let desc = item?.description || item?.id;
180
- if (desc.startsWith('Slash stake')) {
181
- desc = t('payment.invoice.reason.slashStake');
182
- }
183
- if (desc.startsWith('Subscription ')) {
184
- desc = t(`payment.invoice.reason.${desc.replace('Subscription ', '')}`);
185
- }
186
- return <Link to={`/admin/billing/${item.id}`}>{desc}</Link>;
203
+ return (
204
+ <Link to={`/admin/billing/${item.id}`}>
205
+ {getInvoiceDescriptionAndReason(item, locale)?.description || item?.id}
206
+ </Link>
207
+ );
187
208
  },
188
209
  },
189
210
  },
@@ -270,6 +291,7 @@ export default function InvoiceList({
270
291
  onSearchChange: (text: string) => {
271
292
  if (text) {
272
293
  setSearch({
294
+ ...search!,
273
295
  q: {
274
296
  'like-description': text,
275
297
  'like-metadata': text,
@@ -280,6 +302,7 @@ export default function InvoiceList({
280
302
  });
281
303
  } else {
282
304
  setSearch({
305
+ ...search!,
283
306
  status: '',
284
307
  customer_id,
285
308
  subscription_id,
@@ -22,7 +22,7 @@ export function PricePaymentSettings({ index }: { index: number }) {
22
22
  formState: { errors },
23
23
  } = useFormContext();
24
24
  const type = useWatch({ control, name: getFieldName('after_completion.type') });
25
- const nftMintEnabled = useWatch({ control, name: 'nft_mint_settings.enabled' });
25
+ const nftMintEnabled = useWatch({ control, name: getFieldName('nft_mint_settings.enabled') });
26
26
 
27
27
  const values = getValues();
28
28
 
@@ -1,12 +1,15 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { api, dayjs, formatAmount, formatTime } from '@blocklet/payment-react';
3
- import type { TSubscriptionExpanded } from '@blocklet/payment-types';
3
+ import type { TPaymentCurrency, TSubscriptionExpanded } from '@blocklet/payment-types';
4
4
  import { Box, Divider, FormControlLabel, Radio, RadioGroup, Stack, TextField, Typography, styled } from '@mui/material';
5
5
  import { useRequest } from 'ahooks';
6
6
  import { useEffect, useMemo } from 'react';
7
7
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
8
8
 
9
- const fetchData = (id: string, time: string): Promise<{ total: string; unused: string }> => {
9
+ const fetchData = (
10
+ id: string,
11
+ time: string
12
+ ): Promise<{ total: string; unused: string; paymentCurrency: TPaymentCurrency }> => {
10
13
  return api.get(`/api/subscriptions/${id}/proration?time=${encodeURIComponent(time)}`).then((res: any) => res.data);
11
14
  };
12
15
 
@@ -128,8 +131,8 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
128
131
  onClick={() => !(loading || refundDisabled) && setValue('cancel.refund', 'last')}
129
132
  control={<Radio checked={refundType === 'last'} />}
130
133
  label={t('admin.subscription.cancel.refund.last', {
131
- symbol,
132
- total: formatAmount(refund?.total || '0', decimal),
134
+ symbol: refund?.paymentCurrency?.symbol || symbol,
135
+ total: formatAmount(refund?.total || '0', refund?.paymentCurrency?.decimal || decimal),
133
136
  })}
134
137
  />
135
138
  <FormControlLabel
@@ -138,9 +141,9 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
138
141
  onClick={() => !(loading || refundDisabled) && setValue('cancel.refund', 'proration')}
139
142
  control={<Radio checked={refundType === 'proration'} />}
140
143
  label={t('admin.subscription.cancel.refund.proration', {
141
- symbol,
142
- total: formatAmount(refund?.total || '0', decimal),
143
- unused: formatAmount(refund?.unused || '0', decimal),
144
+ symbol: refund?.paymentCurrency?.symbol || symbol,
145
+ total: formatAmount(refund?.total || '0', refund?.paymentCurrency?.decimal || decimal),
146
+ unused: formatAmount(refund?.unused || '0', refund?.paymentCurrency?.decimal || decimal),
144
147
  })}
145
148
  />
146
149
  </RadioGroup>
@@ -40,7 +40,7 @@ export default function SubscriptionMetrics({ subscription }: Props) {
40
40
  divider
41
41
  />
42
42
  )}
43
- {upcoming?.amount && upcoming.amount !== '0' && (
43
+ {['active', 'trialing'].includes(subscription.status) && upcoming?.amount && upcoming.amount !== '0' && (
44
44
  <InfoMetric
45
45
  label={t('admin.subscription.nextInvoiceAmount')}
46
46
  value={`${formatBNStr(upcoming.amount, subscription.paymentCurrency.decimal)} ${
@@ -322,6 +322,21 @@ export default function InvoiceDetail(props: { id: string }) {
322
322
  alignItems={InfoAlignItems}
323
323
  />
324
324
  )}
325
+ {data.billing_reason === 'stake' && data?.metadata?.payment_details?.arcblock?.tx_hash && (
326
+ <InfoRow
327
+ label={t('common.stakeTxHash')}
328
+ value={
329
+ <TxLink
330
+ details={{
331
+ arcblock: { tx_hash: data.metadata?.payment_details?.arcblock?.tx_hash, payer: '' },
332
+ }}
333
+ method={data.paymentMethod}
334
+ />
335
+ }
336
+ direction={InfoDirection}
337
+ alignItems={InfoAlignItems}
338
+ />
339
+ )}
325
340
  {data.subscription && (
326
341
  <InfoRow
327
342
  label={t('admin.subscription.name')}
@@ -1,5 +1,5 @@
1
1
  import InvoiceList from '../../../../components/invoice/list';
2
2
 
3
3
  export default function InvoicesList() {
4
- return <InvoiceList features={{ customer: true, toolbar: true, filter: true }} />;
4
+ return <InvoiceList features={{ customer: true, toolbar: true, filter: true }} include_staking />;
5
5
  }
@@ -1,7 +1,15 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { TxLink, api, formatError, formatSubscriptionProduct, formatTime, useMobile } from '@blocklet/payment-react';
4
+ import {
5
+ TxLink,
6
+ api,
7
+ formatError,
8
+ formatSubscriptionProduct,
9
+ formatTime,
10
+ useMobile,
11
+ hasDelegateTxHash,
12
+ } from '@blocklet/payment-react';
5
13
  import type { TProduct, TSubscriptionExpanded } from '@blocklet/payment-types';
6
14
  import { ArrowBackOutlined } from '@mui/icons-material';
7
15
  import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
@@ -268,7 +276,7 @@ export default function SubscriptionDetail(props: { id: string }) {
268
276
  direction={InfoDirection}
269
277
  alignItems={InfoAlignItems}
270
278
  />
271
- {(data.payment_details?.arcblock?.tx_hash || data.payment_details?.ethereum?.tx_hash) && (
279
+ {data.payment_details && hasDelegateTxHash(data.payment_details, data.paymentMethod) && (
272
280
  <InfoRow
273
281
  label={t('common.delegateTxHash')}
274
282
  value={<TxLink details={data.payment_details} method={data.paymentMethod} />}
@@ -276,7 +284,7 @@ export default function SubscriptionDetail(props: { id: string }) {
276
284
  alignItems={InfoAlignItems}
277
285
  />
278
286
  )}
279
- {data.payment_details?.arcblock?.staking?.tx_hash && (
287
+ {data.paymentMethod?.type === 'arcblock' && data.payment_details?.arcblock?.staking?.tx_hash && (
280
288
  <InfoRow
281
289
  label={t('common.stakeTxHash')}
282
290
  value={
@@ -322,7 +330,7 @@ export default function SubscriptionDetail(props: { id: string }) {
322
330
  <Box className="section">
323
331
  <SectionHeader title={t('admin.invoices')} />
324
332
  <Box className="section-body">
325
- <InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
333
+ <InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} include_staking />
326
334
  </Box>
327
335
  </Box>
328
336
  <Divider />
@@ -367,6 +367,7 @@ export default function CustomerDetail(props: { id: string }) {
367
367
  features={{ customer: false, toolbar: false }}
368
368
  customer_id={data.customer.id}
369
369
  status={['open', 'paid', 'uncollectible', 'draft', 'void'].join(',')}
370
+ include_staking
370
371
  />
371
372
  </Box>
372
373
  </Box>
@@ -318,7 +318,7 @@ export default function CustomerHome() {
318
318
  )}
319
319
  </Box>
320
320
  <Box className="section-body">
321
- <CustomerInvoiceList customer_id={data.id} type="table" />
321
+ <CustomerInvoiceList customer_id={data.id} type="table" include_staking />
322
322
  </Box>
323
323
  </Box>
324
324
  );
@@ -6,6 +6,7 @@ import {
6
6
  TxGas,
7
7
  TxLink,
8
8
  api,
9
+ formatAmount,
9
10
  formatError,
10
11
  formatTime,
11
12
  getInvoiceStatusColor,
@@ -101,9 +102,10 @@ export default function CustomerInvoiceDetail() {
101
102
  return <CircularProgress />;
102
103
  }
103
104
 
104
- const isSlash =
105
- data.paymentMethod?.type === 'arcblock' && data.paymentIntent?.payment_details?.arcblock?.type === 'slash';
105
+ const isStake = data.paymentMethod?.type === 'arcblock' && data.billing_reason === 'stake';
106
+ const isSlashStake = data.paymentMethod?.type === 'arcblock' && data.billing_reason.includes('slash_stake');
106
107
 
108
+ const paymentDetails = data.paymentIntent?.payment_details || data.metadata?.payment_details;
107
109
  return (
108
110
  <InvoiceDetailRoot direction="column" spacing={3} sx={{ my: 2 }}>
109
111
  <Stack direction="row" justifyContent="space-between">
@@ -263,12 +265,20 @@ export default function CustomerInvoiceDetail() {
263
265
  alignItems={InfoAlignItems}
264
266
  />
265
267
  )}
266
- {data.paymentIntent && data.paymentIntent.payment_details && (
268
+ {paymentDetails && (
267
269
  <InfoRow
268
- label={t(`common.${data.paymentIntent.payment_details?.arcblock?.type || 'transfer'}TxHash`)}
269
- value={
270
- <TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />
270
+ label={
271
+ isStake ? t('common.stakeTxHash') : t(`common.${paymentDetails?.arcblock?.type || 'transfer'}TxHash`)
271
272
  }
273
+ value={<TxLink details={paymentDetails} method={data.paymentMethod} mode="customer" />}
274
+ direction={InfoDirection}
275
+ alignItems={InfoAlignItems}
276
+ />
277
+ )}
278
+ {(isStake || isSlashStake) && (
279
+ <InfoRow
280
+ label={isSlashStake ? t('common.slashStakeAmount') : t('common.stakeAmount')}
281
+ value={`${formatAmount(data.total, data.paymentCurrency.decimal)} ${data.paymentCurrency.symbol}`}
272
282
  direction={InfoDirection}
273
283
  alignItems={InfoAlignItems}
274
284
  />
@@ -289,15 +299,19 @@ export default function CustomerInvoiceDetail() {
289
299
  )}
290
300
  </Stack>
291
301
  </Box>
292
- {!isSlash && (
302
+ {!isSlashStake && (
293
303
  <>
294
- <Divider />
295
- <Box className="section">
296
- <Typography variant="h3" mb={1.5} className="section-header">
297
- {t('payment.customer.products')}
298
- </Typography>
299
- <InvoiceTable invoice={data} simple />
300
- </Box>
304
+ {!isStake && (
305
+ <>
306
+ <Divider />
307
+ <Box className="section">
308
+ <Typography variant="h3" mb={1.5} className="section-header">
309
+ {t('payment.customer.products')}
310
+ </Typography>
311
+ <InvoiceTable invoice={data} simple />
312
+ </Box>
313
+ </>
314
+ )}
301
315
  <Divider />
302
316
  <Box className="section">
303
317
  <Typography variant="h3" className="section-header">
@@ -243,7 +243,14 @@ export default function CustomerSubscriptionChangePlan() {
243
243
  </Stack>
244
244
  <Stack direction="column" sx={{ marginTop: 32 }}>
245
245
  <SectionHeader title={t('payment.customer.changePlan.config')} />
246
- <PricingTable mode="select" alignItems="left" interval={interval} table={table} onSelect={handleSelect} />
246
+ <PricingTable
247
+ mode="select"
248
+ alignItems="left"
249
+ interval={interval}
250
+ table={table}
251
+ onSelect={handleSelect}
252
+ hideCurrency
253
+ />
247
254
  </Stack>
248
255
  {!data.table && <Alert severity="error">{t('payment.customer.changePlan.tableNotFound')}</Alert>}
249
256
  {state.priceId && state.total && state.setup && (
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { CustomerInvoiceList, TxLink, api, formatTime } from '@blocklet/payment-react';
3
+ import { CustomerInvoiceList, TxLink, api, formatTime, hasDelegateTxHash } from '@blocklet/payment-react';
4
4
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
5
5
  import { ArrowBackOutlined } from '@mui/icons-material';
6
6
  import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
@@ -243,7 +243,7 @@ export default function CustomerSubscriptionDetail() {
243
243
  direction={InfoDirection}
244
244
  alignItems={InfoAlignItems}
245
245
  />
246
- {(data.payment_details?.arcblock?.tx_hash || data.payment_details?.ethereum?.tx_hash) && (
246
+ {data.payment_details && hasDelegateTxHash(data.payment_details, data.paymentMethod) && (
247
247
  <InfoRow
248
248
  label={t('common.delegateTxHash')}
249
249
  value={<TxLink details={data.payment_details} method={data.paymentMethod} />}
@@ -251,7 +251,7 @@ export default function CustomerSubscriptionDetail() {
251
251
  alignItems={InfoAlignItems}
252
252
  />
253
253
  )}
254
- {data.payment_details?.arcblock?.staking?.tx_hash && (
254
+ {data.paymentMethod?.type === 'arcblock' && data.payment_details?.arcblock?.staking?.tx_hash && (
255
255
  <InfoRow
256
256
  label={t('common.stakeTxHash')}
257
257
  value={
@@ -300,7 +300,7 @@ export default function CustomerSubscriptionDetail() {
300
300
  {t('customer.invoiceHistory')}
301
301
  </Typography>
302
302
  <Box className="section-body">
303
- <CustomerInvoiceList subscription_id={data.id} include_staking type="table" />
303
+ <CustomerInvoiceList subscription_id={data.id} type="table" include_staking />
304
304
  </Box>
305
305
  </Box>
306
306
  </Root>
@@ -11,6 +11,7 @@ import {
11
11
  formatToDate,
12
12
  formatToDatetime,
13
13
  getDidConnectQueryParams,
14
+ getInvoiceDescriptionAndReason,
14
15
  getInvoiceStatusColor,
15
16
  getPrefix,
16
17
  getSubscriptionStatusColor,
@@ -210,8 +211,9 @@ export default function SubscriptionEmbed() {
210
211
  {(invoices as any).map((item: any) => {
211
212
  return (
212
213
  <ListItem key={item.id} disableGutters sx={{ display: 'flex', justifyContent: 'space-between' }}>
213
- <Typography component="span" sx={{ flex: 3 }}>
214
+ <Typography component="div" sx={{ flex: 3, gap: 1, display: 'flex', alignItems: 'center' }}>
214
215
  {formatToDate(item.created_at, locale, 'YYYY-MM-DD')}
216
+ <Status label={getInvoiceDescriptionAndReason(item, locale)?.type} />
215
217
  </Typography>
216
218
  <Typography component="span" sx={{ flex: 1, textAlign: 'right' }}>
217
219
  {formatBNStr(item.total, item.paymentCurrency.decimal)}&nbsp;