payment-kit 1.17.4 → 1.17.6

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 (62) hide show
  1. package/api/src/crons/currency.ts +1 -1
  2. package/api/src/integrations/arcblock/stake.ts +4 -3
  3. package/api/src/libs/constants.ts +3 -0
  4. package/api/src/libs/invoice.ts +6 -5
  5. package/api/src/libs/notification/template/subscription-renew-failed.ts +4 -3
  6. package/api/src/libs/notification/template/subscription-renewed.ts +4 -3
  7. package/api/src/libs/notification/template/subscription-succeeded.ts +4 -3
  8. package/api/src/libs/notification/template/subscription-trial-start.ts +11 -3
  9. package/api/src/libs/notification/template/subscription-upgraded.ts +2 -1
  10. package/api/src/libs/payment.ts +5 -4
  11. package/api/src/libs/product.ts +24 -1
  12. package/api/src/queues/payment.ts +7 -5
  13. package/api/src/queues/refund.ts +8 -6
  14. package/api/src/routes/connect/change-payment.ts +3 -2
  15. package/api/src/routes/connect/change-plan.ts +3 -2
  16. package/api/src/routes/connect/collect-batch.ts +5 -4
  17. package/api/src/routes/connect/collect.ts +6 -5
  18. package/api/src/routes/connect/pay.ts +9 -4
  19. package/api/src/routes/connect/recharge.ts +9 -4
  20. package/api/src/routes/connect/setup.ts +3 -2
  21. package/api/src/routes/connect/shared.ts +25 -7
  22. package/api/src/routes/connect/subscribe.ts +3 -2
  23. package/api/src/routes/payment-currencies.ts +71 -10
  24. package/api/src/routes/payment-methods.ts +37 -21
  25. package/api/src/routes/payment-stats.ts +9 -3
  26. package/api/src/routes/prices.ts +19 -1
  27. package/api/src/routes/products.ts +60 -28
  28. package/api/src/routes/subscriptions.ts +4 -3
  29. package/api/src/store/models/payment-currency.ts +31 -0
  30. package/api/src/store/models/payment-method.ts +11 -8
  31. package/api/src/store/models/types.ts +27 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +19 -19
  34. package/public/methods/base.png +0 -0
  35. package/src/components/payment-currency/add.tsx +1 -1
  36. package/src/components/payment-currency/edit.tsx +73 -0
  37. package/src/components/payment-currency/form.tsx +12 -1
  38. package/src/components/payment-method/base.tsx +79 -0
  39. package/src/components/payment-method/form.tsx +3 -0
  40. package/src/components/price/upsell-select.tsx +1 -0
  41. package/src/components/subscription/metrics.tsx +1 -1
  42. package/src/components/subscription/portal/actions.tsx +1 -1
  43. package/src/libs/util.ts +1 -1
  44. package/src/locales/en.tsx +30 -1
  45. package/src/locales/zh.tsx +28 -0
  46. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  47. package/src/pages/admin/customers/customers/detail.tsx +2 -2
  48. package/src/pages/admin/overview.tsx +15 -2
  49. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  50. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  51. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  52. package/src/pages/admin/products/links/detail.tsx +1 -0
  53. package/src/pages/admin/products/prices/actions.tsx +2 -1
  54. package/src/pages/admin/products/prices/detail.tsx +1 -0
  55. package/src/pages/admin/products/products/detail.tsx +1 -0
  56. package/src/pages/admin/settings/payment-methods/create.tsx +7 -0
  57. package/src/pages/admin/settings/payment-methods/index.tsx +180 -20
  58. package/src/pages/customer/index.tsx +1 -1
  59. package/src/pages/customer/invoice/detail.tsx +1 -1
  60. package/src/pages/customer/recharge.tsx +1 -1
  61. package/src/pages/customer/refund/list.tsx +7 -3
  62. package/src/pages/customer/subscription/change-payment.tsx +1 -1
@@ -85,7 +85,7 @@ function getTokenBalances(customer: TCustomerExpanded, paymentMethods: TPaymentM
85
85
  export default function CustomerDetail(props: { id: string }) {
86
86
  const { t } = useLocaleContext();
87
87
  const { isMobile } = useMobile();
88
- const { settings } = usePaymentContext();
88
+ const { settings, livemode } = usePaymentContext();
89
89
  const [state, setState] = useSetState({
90
90
  adding: {
91
91
  price: false,
@@ -282,7 +282,7 @@ export default function CustomerDetail(props: { id: string }) {
282
282
  <Stack>
283
283
  <InfoRow
284
284
  label={t('common.did')}
285
- value={<DidAddress did={data.customer.did} showQrcode />}
285
+ value={<DidAddress did={data.customer.did} chainId={livemode ? 'main' : 'beta'} showQrcode />}
286
286
  direction={InfoDirection}
287
287
  alignItems={InfoAlignItems}
288
288
  />
@@ -79,7 +79,7 @@ export const groupData = (data: TPaymentStat[], currencies: { [key: string]: any
79
79
 
80
80
  export default function Overview() {
81
81
  const { t, locale } = useLocaleContext();
82
- const { settings } = usePaymentContext();
82
+ const { settings, livemode } = usePaymentContext();
83
83
  const maxDate = dayjs().endOf('day').toDate();
84
84
  const [state, setState] = useSetState({
85
85
  anchorEl: null,
@@ -200,6 +200,13 @@ export default function Overview() {
200
200
  });
201
201
  }
202
202
 
203
+ const getChainId = (type: string) => {
204
+ if (type === 'arcblock') {
205
+ return livemode ? 'main' : 'beta';
206
+ }
207
+ return '';
208
+ };
209
+
203
210
  return (
204
211
  <Grid container gap={{ xs: 2, sm: 5, md: 8 }} sx={{ mb: 4 }}>
205
212
  <Grid item xs={12} sm={12} md={8}>
@@ -244,7 +251,13 @@ export default function Overview() {
244
251
  </Stack>
245
252
  <Stack direction="column" spacing={1} sx={{ mt: 2 }}>
246
253
  {Object.keys(summary.data.addresses).map((chain) => (
247
- <DID key={chain} did={summary.data?.addresses?.[chain] as string} copyable showQrcode />
254
+ <DID
255
+ key={chain}
256
+ did={summary.data?.addresses?.[chain] as string}
257
+ chainId={getChainId(chain)}
258
+ copyable
259
+ showQrcode
260
+ />
248
261
  ))}
249
262
  </Stack>
250
263
  </Box>
@@ -330,7 +330,7 @@ export default function PaymentIntentDetail(props: { id: string }) {
330
330
  direction={InfoDirection}
331
331
  alignItems={InfoAlignItems}
332
332
  />
333
- {!!data.payment_details?.ethereum && (
333
+ {(!!data.payment_details?.ethereum || !!data.payment_details?.base) && (
334
334
  <InfoRow
335
335
  label={t('common.txGas')}
336
336
  value={<TxGas details={data.payment_details as any} method={data.paymentMethod} />}
@@ -299,7 +299,7 @@ export default function PayoutDetail(props: { id: string }) {
299
299
  direction={InfoDirection}
300
300
  alignItems={InfoAlignItems}
301
301
  />
302
- {!!data.payment_details?.ethereum && (
302
+ {(!!data.payment_details?.ethereum || !!data.payment_details?.base) && (
303
303
  <InfoRow
304
304
  label={t('common.txGas')}
305
305
  value={<TxGas details={data.payment_details as any} method={data.paymentMethod} />}
@@ -317,7 +317,7 @@ export default function RefundDetail(props: { id: string }) {
317
317
  direction={InfoDirection}
318
318
  alignItems={InfoAlignItems}
319
319
  />
320
- {!!data.payment_details?.ethereum && (
320
+ {(!!data.payment_details?.ethereum || !!data.payment_details?.base) && (
321
321
  <InfoRow
322
322
  label={t('common.txGas')}
323
323
  value={<TxGas details={data.payment_details as any} method={data.paymentMethod} />}
@@ -82,6 +82,7 @@ export default function PaymentLinkDetail(props: { id: string }) {
82
82
  } catch (err) {
83
83
  console.error(err);
84
84
  Toast.error(formatError(err));
85
+ throw err;
85
86
  } finally {
86
87
  setState((prev) => ({ loading: { ...prev.loading, price: false } }));
87
88
  }
@@ -47,8 +47,9 @@ export default function PriceActions({ data, onChange, variant, setAsDefault }:
47
47
  } catch (err) {
48
48
  console.error(err);
49
49
  Toast.error(formatError(err));
50
+ throw err;
50
51
  } finally {
51
- setState({ loading: false, action: '' });
52
+ setState({ loading: false });
52
53
  }
53
54
  };
54
55
  const onArchivePrice = async () => {
@@ -65,6 +65,7 @@ export default function PriceDetail(props: { id: string }) {
65
65
  } catch (err) {
66
66
  console.error(err);
67
67
  Toast.error(formatError(err));
68
+ throw err;
68
69
  } finally {
69
70
  setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
70
71
  }
@@ -99,6 +99,7 @@ export default function ProductDetail(props: { id: string }) {
99
99
  } catch (err) {
100
100
  console.error(err);
101
101
  Toast.error(formatError(err));
102
+ throw err;
102
103
  } finally {
103
104
  setState((prev) => ({ loading: { ...prev.loading, price: false } }));
104
105
  }
@@ -39,6 +39,13 @@ export default function PaymentMethodCreate() {
39
39
  confirmation: 1,
40
40
  logo: '',
41
41
  },
42
+ base: {
43
+ api_host: '',
44
+ explorer_host: '',
45
+ native_symbol: 'ETH',
46
+ confirmation: 1,
47
+ logo: '',
48
+ },
42
49
  bitcoin: {
43
50
  chain_id: 0,
44
51
  api_host: '',
@@ -1,7 +1,15 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { Switch, api, formatError } from '@blocklet/payment-react';
3
- import type { TPaymentMethodExpanded } from '@blocklet/payment-types';
4
- import { AddOutlined, Check, Close, DeleteOutlined, EditOutlined } from '@mui/icons-material';
2
+ import { ConfirmDialog, Switch, api, formatError, usePaymentContext } from '@blocklet/payment-react';
3
+ import type { TPaymentCurrency, TPaymentMethodExpanded } from '@blocklet/payment-types';
4
+ import {
5
+ AddOutlined,
6
+ Check,
7
+ Close,
8
+ DeleteOutlined,
9
+ EditOutlined,
10
+ InfoOutlined,
11
+ QrCodeOutlined,
12
+ } from '@mui/icons-material';
5
13
  import {
6
14
  Alert,
7
15
  Avatar,
@@ -15,6 +23,7 @@ import {
15
23
  ListItemText,
16
24
  Stack,
17
25
  TextField,
26
+ Tooltip,
18
27
  Typography,
19
28
  } from '@mui/material';
20
29
  import { useRequest, useSetState } from 'ahooks';
@@ -22,12 +31,16 @@ import useBus from 'use-bus';
22
31
 
23
32
  import { useState } from 'react';
24
33
  import Toast from '@arcblock/ux/lib/Toast';
34
+ import { DIDDialog } from '@arcblock/ux/lib/DID';
25
35
  import IconCollapse from '../../../../components/collapse';
26
36
  import InfoCard from '../../../../components/info-card';
27
37
  import InfoRow from '../../../../components/info-row';
28
38
  import PaymentCurrencyAdd from '../../../../components/payment-currency/add';
39
+ import PaymentCurrencyEdit from '../../../../components/payment-currency/edit';
29
40
 
30
- const getMethods = (params: Record<string, any> = {}): Promise<TPaymentMethodExpanded[]> => {
41
+ const getMethods = (
42
+ params: Record<string, any> = {}
43
+ ): Promise<{ list: TPaymentMethodExpanded[]; addresses: { arcblock: string; ethereum: string } }> => {
31
44
  const search = new URLSearchParams();
32
45
  Object.keys(params).forEach((key) => {
33
46
  search.set(key, String(params[key]));
@@ -132,29 +145,124 @@ const groupByType = (methods: TPaymentMethodExpanded[]) => {
132
145
 
133
146
  export default function PaymentMethods() {
134
147
  const { t } = useLocaleContext();
135
- const { loading, error, data, runAsync } = useRequest(() => getMethods({}));
136
- const [state, setState] = useSetState({ method: '' });
148
+ const {
149
+ loading,
150
+ error,
151
+ data = { list: [], addresses: {} } as any,
152
+ runAsync,
153
+ } = useRequest(() =>
154
+ getMethods({
155
+ addresses: true,
156
+ })
157
+ );
158
+ const [didDialog, setDidDialog] = useSetState({ open: false, chainId: '', did: '' });
159
+ const { refresh } = usePaymentContext();
160
+ const [currencyDialog, setCurrencyDialog] = useSetState({ action: '', value: null, method: '' });
137
161
 
138
- useBus('paymentMethod.created', runAsync, []);
139
- useBus('paymentCurrency.added', () => runAsync().then(() => setState({ method: '' })), []);
162
+ const { list: methods, addresses } = data;
163
+ useBus(
164
+ 'paymentMethod.created',
165
+ () => {
166
+ runAsync();
167
+ refresh(true);
168
+ },
169
+ []
170
+ );
171
+ useBus(
172
+ 'paymentCurrency.added',
173
+ () => {
174
+ runAsync().then(() => {
175
+ setCurrencyDialog({ action: '', method: '' });
176
+ });
177
+ refresh(true);
178
+ },
179
+ []
180
+ );
181
+ useBus(
182
+ 'paymentCurrency.updated',
183
+ () => {
184
+ runAsync().then(() => {
185
+ setCurrencyDialog({ action: '', method: '' });
186
+ });
187
+ refresh(true);
188
+ },
189
+ []
190
+ );
140
191
 
141
192
  if (error) {
142
193
  return <Alert severity="error">{error.message}</Alert>;
143
194
  }
144
195
 
145
- if (loading || !data) {
196
+ if (loading || !data || methods?.length === 0) {
146
197
  return <CircularProgress />;
147
198
  }
148
199
 
149
- const groups = groupByType(data);
200
+ const groups = groupByType(methods);
201
+
202
+ const getAddress = (type: string) => {
203
+ if (['ethereum', 'base'].includes(type)) {
204
+ return addresses?.ethereum || '';
205
+ }
206
+ return addresses?.arcblock || '';
207
+ };
208
+
209
+ const handleDeleteCurrency = async (currency: TPaymentCurrency) => {
210
+ try {
211
+ await api.delete(`/api/payment-currencies/${currency.id}`);
212
+ runAsync();
213
+ refresh(true);
214
+ Toast.success(t('admin.paymentCurrency.deleted'));
215
+ } catch (err) {
216
+ Toast.error(formatError(err));
217
+ } finally {
218
+ // @ts-ignore
219
+ setCurrencyDialog({ action: '', method: '' });
220
+ }
221
+ };
150
222
 
151
223
  return (
152
224
  <>
153
225
  {Object.keys(groups).map((x) => (
154
226
  <Box key={x} mt={3}>
155
- <Typography variant="h6" sx={{ mb: 1, textTransform: 'uppercase' }}>
156
- {x}
157
- </Typography>
227
+ <Stack direction="row" alignItems="center" mb={1}>
228
+ <Typography variant="h6" sx={{ textTransform: 'uppercase' }}>
229
+ {x}
230
+ </Typography>
231
+ {['ethereum', 'base'].includes(x) && (
232
+ <Stack
233
+ direction="row"
234
+ alignItems="center"
235
+ spacing={0.5}
236
+ sx={{
237
+ ml: 1,
238
+ px: 0.5,
239
+ color: 'text.secondary',
240
+ }}>
241
+ <InfoOutlined sx={{ fontSize: 16 }} color="warning" />
242
+ <Typography variant="body2">
243
+ {t('admin.paymentMethod.gasTip', {
244
+ method: x,
245
+ address: '222',
246
+ chain: groups[x]?.[0]?.name || x,
247
+ })}
248
+ </Typography>
249
+ <Tooltip title={t('admin.paymentMethod.showQR')}>
250
+ <IconButton
251
+ size="small"
252
+ onClick={() =>
253
+ setDidDialog({
254
+ open: true,
255
+ chainId: groups[x]?.[0]?.id || '',
256
+ did: getAddress(x),
257
+ })
258
+ }
259
+ sx={{ p: 0.5 }}>
260
+ <QrCodeOutlined sx={{ fontSize: 16 }} />
261
+ </IconButton>
262
+ </Tooltip>
263
+ </Stack>
264
+ )}
265
+ </Stack>
158
266
  {(groups[x] as TPaymentMethodExpanded[]).map((method) => (
159
267
  <IconCollapse
160
268
  key={method.id}
@@ -189,14 +297,38 @@ export default function PaymentMethods() {
189
297
  value={
190
298
  <List>
191
299
  {method.payment_currencies.map((currency) => {
300
+ if (!currency) {
301
+ return null;
302
+ }
192
303
  return (
193
304
  <ListItem
194
305
  key={currency.id}
195
306
  disablePadding
196
307
  secondaryAction={
197
- <IconButton edge="end" disabled>
198
- <DeleteOutlined />
199
- </IconButton>
308
+ currency.locked || method.default_currency_id === currency.id ? null : (
309
+ <>
310
+ <IconButton
311
+ edge="end"
312
+ onClick={() => {
313
+ setCurrencyDialog({
314
+ action: 'edit',
315
+ // @ts-ignore
316
+ value: currency,
317
+ method: method.id,
318
+ });
319
+ }}>
320
+ <EditOutlined />
321
+ </IconButton>
322
+ <IconButton
323
+ edge="end"
324
+ onClick={() => {
325
+ // @ts-ignore
326
+ setCurrencyDialog({ action: 'delete', value: currency, method: method.id });
327
+ }}>
328
+ <DeleteOutlined />
329
+ </IconButton>
330
+ </>
331
+ )
200
332
  }>
201
333
  <ListItemAvatar>
202
334
  <Avatar src={currency.logo} alt={currency.name} />
@@ -210,17 +342,37 @@ export default function PaymentMethods() {
210
342
  key="add-currency"
211
343
  disablePadding
212
344
  sx={{ cursor: 'pointer' }}
213
- onClick={() => setState({ method: method.id })}>
345
+ onClick={() => {
346
+ setCurrencyDialog({ action: 'create', method: method.id });
347
+ }}>
214
348
  <ListItemAvatar>
215
349
  <Avatar>
216
350
  <AddOutlined fontSize="small" />
217
351
  </Avatar>
218
352
  </ListItemAvatar>
219
- <ListItemText primary="Add Currency" />
353
+ <ListItemText primary={t('admin.paymentCurrency.add')} />
220
354
  </ListItem>
221
355
  )}
222
- {state.method === method.id && (
223
- <PaymentCurrencyAdd method={method} onClose={() => setState({ method: '' })} />
356
+ {currencyDialog.method === method.id && currencyDialog.action === 'create' && (
357
+ <PaymentCurrencyAdd
358
+ method={method}
359
+ onClose={() => setCurrencyDialog({ action: '', method: '' })}
360
+ />
361
+ )}
362
+ {currencyDialog.method === method.id && currencyDialog.action === 'edit' && (
363
+ <PaymentCurrencyEdit
364
+ method={method}
365
+ onClose={() => setCurrencyDialog({ action: '', method: '' })}
366
+ value={currencyDialog.value!}
367
+ />
368
+ )}
369
+ {currencyDialog.method === method.id && currencyDialog.action === 'delete' && (
370
+ <ConfirmDialog
371
+ onConfirm={() => handleDeleteCurrency(currencyDialog.value!)}
372
+ onCancel={() => setCurrencyDialog({ action: '', method: '' })}
373
+ title={t('admin.paymentCurrency.delete')}
374
+ message={t('admin.paymentCurrency.deleteConfirm')}
375
+ />
224
376
  )}
225
377
  </List>
226
378
  }
@@ -231,6 +383,14 @@ export default function PaymentMethods() {
231
383
  ))}
232
384
  </Box>
233
385
  ))}
386
+ {didDialog.open && didDialog.did && (
387
+ <DIDDialog
388
+ open={didDialog.open}
389
+ onClose={() => setDidDialog({ open: false, chainId: '', did: '' })}
390
+ did={didDialog.did}
391
+ chainId={didDialog.chainId}
392
+ />
393
+ )}
234
394
  </>
235
395
  );
236
396
  }
@@ -360,7 +360,7 @@ export default function CustomerHome() {
360
360
  {data.name}
361
361
  </Typography>
362
362
  <Typography variant="body2" color="text.secondary">
363
- <DID did={data.did} copyable showQrcode />
363
+ <DID did={data.did} copyable showQrcode chainId={livemode ? 'main' : 'beta'} />
364
364
  </Typography>
365
365
  </Box>
366
366
  </Box>
@@ -268,7 +268,7 @@ export default function CustomerInvoiceDetail() {
268
268
  direction={InfoDirection}
269
269
  alignItems={InfoAlignItems}
270
270
  />
271
- {!!data.paymentIntent?.payment_details?.ethereum && (
271
+ {(!!data.paymentIntent?.payment_details?.ethereum || !!data.paymentIntent?.payment_details?.base) && (
272
272
  <InfoRow
273
273
  label={t('common.txGas')}
274
274
  value={<TxGas details={data.paymentIntent.payment_details as any} method={data.paymentMethod} />}
@@ -217,7 +217,7 @@ export default function RechargePage() {
217
217
  }
218
218
  const currentBalance = formatBNStr(payerValue?.token || '0', paymentCurrency?.decimal, 6, false);
219
219
 
220
- const supportRecharge = ['arcblock', 'ethereum'].includes(paymentMethod?.type || '');
220
+ const supportRecharge = ['arcblock', 'ethereum', 'base'].includes(paymentMethod?.type || '');
221
221
 
222
222
  const formatEstimatedDuration = (cycles: number) => {
223
223
  const { interval, interval_count: intervalCount } = subscription.pending_invoice_item_interval;
@@ -142,14 +142,16 @@ const RefundTable = memo(({ invoice_id }: Props) => {
142
142
  options: {
143
143
  customBodyRenderLite: (_: string, index: number) => {
144
144
  const item = data.list[index] as TRefundExpanded;
145
- return item.payment_details?.arcblock?.tx_hash || item.payment_details?.ethereum?.tx_hash ? (
145
+ const paymentDetails = item.payment_details as Record<string, { tx_hash?: string }>;
146
+ const txHash = ['arcblock', 'ethereum', 'base'].map((key) => !!paymentDetails?.[key]?.tx_hash).find(Boolean);
147
+ return txHash ? (
146
148
  <Box
147
149
  sx={{
148
150
  '.MuiTypography-root': {
149
151
  color: 'text.link',
150
152
  },
151
153
  }}>
152
- <TxLink details={item.payment_details} method={item.paymentMethod} mode="customer" />
154
+ <TxLink details={paymentDetails} method={item.paymentMethod} mode="customer" />
153
155
  </Box>
154
156
  ) : (
155
157
  t('common.none')
@@ -271,7 +273,9 @@ const RefundList = memo(({ invoice_id }: Props) => {
271
273
  </Typography>
272
274
  </Box>
273
275
  <Box flex={3} sx={{ minWidth: '220px' }}>
274
- {(item.payment_details?.arcblock?.tx_hash || item.payment_details?.ethereum?.tx_hash) && (
276
+ {(item.payment_details?.arcblock?.tx_hash ||
277
+ item.payment_details?.ethereum?.tx_hash ||
278
+ item.payment_details?.base?.tx_hash) && (
275
279
  <TxLink details={item.payment_details} method={item.paymentMethod} mode="customer" />
276
280
  )}
277
281
  </Box>
@@ -153,7 +153,7 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
153
153
  submitting: false,
154
154
  });
155
155
 
156
- if (['arcblock', 'ethereum'].includes(method.type)) {
156
+ if (['arcblock', 'ethereum', 'base'].includes(method.type)) {
157
157
  setState({ paying: true });
158
158
  if (result.data.delegation?.sufficient) {
159
159
  await handleConnected();