payment-kit 1.18.18 → 1.18.20

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 (36) hide show
  1. package/api/src/libs/subscription.ts +116 -0
  2. package/api/src/routes/checkout-sessions.ts +28 -1
  3. package/api/src/routes/customers.ts +5 -1
  4. package/api/src/store/migrations/20250318-donate-invoice.ts +45 -0
  5. package/api/tests/libs/subscription.spec.ts +311 -0
  6. package/blocklet.yml +1 -1
  7. package/package.json +8 -8
  8. package/src/components/currency.tsx +11 -4
  9. package/src/components/customer/link.tsx +54 -14
  10. package/src/components/customer/overdraft-protection.tsx +36 -2
  11. package/src/components/info-card.tsx +55 -7
  12. package/src/components/info-row-group.tsx +122 -0
  13. package/src/components/info-row.tsx +14 -1
  14. package/src/components/payouts/portal/list.tsx +7 -2
  15. package/src/components/subscription/items/index.tsx +1 -1
  16. package/src/components/subscription/metrics.tsx +14 -6
  17. package/src/contexts/info-row.tsx +4 -0
  18. package/src/libs/dayjs.ts +4 -3
  19. package/src/locales/en.tsx +1 -0
  20. package/src/locales/zh.tsx +1 -0
  21. package/src/pages/admin/billing/invoices/detail.tsx +54 -76
  22. package/src/pages/admin/billing/subscriptions/detail.tsx +34 -71
  23. package/src/pages/admin/customers/customers/detail.tsx +41 -64
  24. package/src/pages/admin/payments/intents/detail.tsx +28 -42
  25. package/src/pages/admin/payments/payouts/detail.tsx +27 -36
  26. package/src/pages/admin/payments/refunds/detail.tsx +27 -41
  27. package/src/pages/admin/products/links/detail.tsx +30 -55
  28. package/src/pages/admin/products/prices/detail.tsx +43 -50
  29. package/src/pages/admin/products/pricing-tables/detail.tsx +23 -25
  30. package/src/pages/admin/products/products/detail.tsx +52 -81
  31. package/src/pages/customer/index.tsx +189 -107
  32. package/src/pages/customer/invoice/detail.tsx +49 -50
  33. package/src/pages/customer/payout/detail.tsx +16 -22
  34. package/src/pages/customer/recharge/account.tsx +119 -34
  35. package/src/pages/customer/recharge/subscription.tsx +11 -1
  36. package/src/pages/customer/subscription/detail.tsx +176 -94
@@ -32,6 +32,7 @@ import MetadataList from '../../../../components/metadata/list';
32
32
  import SectionHeader from '../../../../components/section/header';
33
33
  import { getAppInfo, getCustomerProfileUrl, goBackOrFallback } from '../../../../libs/util';
34
34
  import InfoCard from '../../../../components/info-card';
35
+ import InfoRowGroup from '../../../../components/info-row-group';
35
36
 
36
37
  const fetchData = (
37
38
  id: string
@@ -284,23 +285,34 @@ export default function PayoutDetail(props: { id: string }) {
284
285
  },
285
286
  }}>
286
287
  <Box flex={1} className="payment-link-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
287
- <Box className="section">
288
+ <Box className="section" sx={{ containerType: 'inline-size' }}>
288
289
  <SectionHeader title={t('admin.details')} />
289
- <Stack
290
+ <InfoRowGroup
290
291
  sx={{
291
292
  display: 'grid',
292
293
  gridTemplateColumns: {
293
294
  xs: 'repeat(1, 1fr)',
294
- sm: 'repeat(1, 1fr)',
295
- md: 'repeat(2, 1fr)',
296
- lg: 'repeat(3, 1fr)',
295
+ xl: 'repeat(2, 1fr)',
296
+ },
297
+ '@container (min-width: 1000px)': {
298
+ gridTemplateColumns: 'repeat(2, 1fr)',
297
299
  },
298
- gap: {
299
- xs: 0,
300
- md: 2,
300
+ '.info-row-wrapper': {
301
+ gap: 1,
302
+ flexDirection: {
303
+ xs: 'column',
304
+ xl: 'row',
305
+ },
306
+ alignItems: {
307
+ xs: 'flex-start',
308
+ xl: 'center',
309
+ },
310
+ '@container (min-width: 1000px)': {
311
+ flexDirection: 'row',
312
+ alignItems: 'center',
313
+ },
301
314
  },
302
315
  }}>
303
- <InfoRow label={t('common.amount')} value={total} direction={InfoDirection} alignItems={InfoAlignItems} />
304
316
  <InfoRow
305
317
  label={t('common.status')}
306
318
  value={
@@ -318,34 +330,13 @@ export default function PayoutDetail(props: { id: string }) {
318
330
  )}
319
331
  </Stack>
320
332
  }
321
- direction={InfoDirection}
322
- alignItems={InfoAlignItems}
323
333
  />
324
- <InfoRow
325
- label={t('common.description')}
326
- value={data.description}
327
- direction={InfoDirection}
328
- alignItems={InfoAlignItems}
329
- />
330
- <InfoRow
331
- label={t('common.createdAt')}
332
- value={formatTime(data.created_at)}
333
- direction={InfoDirection}
334
- alignItems={InfoAlignItems}
335
- />
336
- <InfoRow
337
- label={t('common.updatedAt')}
338
- value={formatTime(data.updated_at)}
339
- direction={InfoDirection}
340
- alignItems={InfoAlignItems}
341
- />
342
- <InfoRow
343
- label={t('common.customer')}
344
- value={renderCustomer()}
345
- direction={InfoDirection}
346
- alignItems={InfoAlignItems}
347
- />
348
- </Stack>
334
+ <InfoRow label={t('common.amount')} value={total} />
335
+ <InfoRow label={t('common.customer')} value={renderCustomer()} />
336
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
337
+ <InfoRow label={t('common.description')} value={data.description} />
338
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
339
+ </InfoRowGroup>
349
340
  </Box>
350
341
  <Divider />
351
342
  <Box className="section">
@@ -31,6 +31,7 @@ import MetadataList from '../../../../components/metadata/list';
31
31
  import RefundActions from '../../../../components/refund/actions';
32
32
  import SectionHeader from '../../../../components/section/header';
33
33
  import { goBackOrFallback } from '../../../../libs/util';
34
+ import InfoRowGroup from '../../../../components/info-row-group';
34
35
 
35
36
  const fetchData = (id: string): Promise<TRefundExpanded> => {
36
37
  return api.get(`/api/refunds/${id}`).then((res) => res.data);
@@ -207,28 +208,34 @@ export default function RefundDetail(props: { id: string }) {
207
208
  },
208
209
  }}>
209
210
  <Box flex={1} className="payment-link-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
210
- <Box className="section">
211
+ <Box className="section" sx={{ containerType: 'inline-size' }}>
211
212
  <SectionHeader title={t('admin.details')} />
212
- <Stack
213
+ <InfoRowGroup
213
214
  sx={{
214
215
  display: 'grid',
215
216
  gridTemplateColumns: {
216
217
  xs: 'repeat(1, 1fr)',
217
- sm: 'repeat(1, 1fr)',
218
- md: 'repeat(2, 1fr)',
219
- lg: 'repeat(3, 1fr)',
218
+ xl: 'repeat(2, 1fr)',
219
+ },
220
+ '@container (min-width: 1000px)': {
221
+ gridTemplateColumns: 'repeat(2, 1fr)',
220
222
  },
221
- gap: {
222
- xs: 0,
223
- md: 2,
223
+ '.info-row-wrapper': {
224
+ gap: 1,
225
+ flexDirection: {
226
+ xs: 'column',
227
+ xl: 'row',
228
+ },
229
+ alignItems: {
230
+ xs: 'flex-start',
231
+ xl: 'center',
232
+ },
233
+ '@container (min-width: 1000px)': {
234
+ flexDirection: 'row',
235
+ alignItems: 'center',
236
+ },
224
237
  },
225
238
  }}>
226
- <InfoRow
227
- label={t('common.amount')}
228
- value={amount}
229
- direction={InfoDirection}
230
- alignItems={InfoAlignItems}
231
- />
232
239
  <InfoRow
233
240
  label={t('common.status')}
234
241
  value={
@@ -255,34 +262,13 @@ export default function RefundDetail(props: { id: string }) {
255
262
  )}
256
263
  </Stack>
257
264
  }
258
- direction={InfoDirection}
259
- alignItems={InfoAlignItems}
260
- />
261
- <InfoRow
262
- label={t('common.type')}
263
- value={t(`refund.type.${data.type}`)}
264
- direction={InfoDirection}
265
- alignItems={InfoAlignItems}
266
265
  />
267
- <InfoRow
268
- label={t('common.description')}
269
- value={data.description}
270
- direction={InfoDirection}
271
- alignItems={InfoAlignItems}
272
- />
273
- <InfoRow
274
- label={t('common.createdAt')}
275
- value={formatTime(data.created_at)}
276
- direction={InfoDirection}
277
- alignItems={InfoAlignItems}
278
- />
279
- <InfoRow
280
- label={t('common.updatedAt')}
281
- value={formatTime(data.updated_at)}
282
- direction={InfoDirection}
283
- alignItems={InfoAlignItems}
284
- />
285
- </Stack>
266
+ <InfoRow label={t('common.amount')} value={amount} />
267
+ <InfoRow label={t('common.type')} value={t(`refund.type.${data.type}`)} />
268
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
269
+ <InfoRow label={t('common.description')} value={data.description} />
270
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
271
+ </InfoRowGroup>
286
272
  </Box>
287
273
  <Divider />
288
274
  <Box className="section">
@@ -22,14 +22,12 @@ import AddPrice from '../../../../components/product/add-price';
22
22
  import SectionHeader from '../../../../components/section/header';
23
23
  import { formatPaymentLinkPricing, formatProductPrice, goBackOrFallback } from '../../../../libs/util';
24
24
  import InfoMetric from '../../../../components/info-metric';
25
+ import InfoRowGroup from '../../../../components/info-row-group';
25
26
 
26
27
  const fetchData = (id: string): Promise<TPaymentLinkExpanded> => {
27
28
  return api.get(`/api/payment-links/${id}`).then((res) => res.data);
28
29
  };
29
30
 
30
- const InfoDirection = 'column';
31
- const InfoAlignItems = 'flex-start';
32
-
33
31
  export default function PaymentLinkDetail(props: { id: string }) {
34
32
  const { t, locale } = useLocaleContext();
35
33
  const navigate = useNavigate();
@@ -296,77 +294,54 @@ export default function PaymentLinkDetail(props: { id: string }) {
296
294
  <Divider />
297
295
  <Box className="section">
298
296
  <SectionHeader title={t('admin.details')} />
299
- <Stack
297
+ <InfoRowGroup
300
298
  sx={{
301
299
  display: 'grid',
302
300
  gridTemplateColumns: {
303
301
  xs: 'repeat(1, 1fr)',
304
- sm: 'repeat(1, 1fr)',
305
- md: 'repeat(2, 1fr)',
306
- lg: 'repeat(3, 1fr)',
302
+ xl: 'repeat(2, 1fr)',
303
+ },
304
+ '@container (min-width: 1000px)': {
305
+ gridTemplateColumns: 'repeat(2, 1fr)',
307
306
  },
308
- gap: {
309
- xs: 0,
310
- md: 2,
307
+ '.info-row-wrapper': {
308
+ gap: 1,
309
+ flexDirection: {
310
+ xs: 'column',
311
+ xl: 'row',
312
+ },
313
+ alignItems: {
314
+ xs: 'flex-start',
315
+ xl: 'center',
316
+ },
317
+ '@container (min-width: 1000px)': {
318
+ flexDirection: 'row',
319
+ alignItems: 'center',
320
+ },
311
321
  },
312
322
  }}>
313
323
  <InfoRow
314
- sizes={[1, 1]}
315
324
  label={t('admin.paymentLink.allowPromotionCodes')}
316
325
  value={data.allow_promotion_codes ? t('common.yes') : t('common.no')}
317
- direction={InfoDirection}
318
- alignItems={InfoAlignItems}
319
- />
320
- <InfoRow
321
- sizes={[1, 1]}
322
- label={t('admin.paymentLink.requireBillingAddress')}
323
- value={data.billing_address_collection ? t('common.yes') : t('common.no')}
324
- direction={InfoDirection}
325
- alignItems={InfoAlignItems}
326
- />
327
- <InfoRow
328
- sizes={[1, 1]}
329
- label={t('admin.paymentLink.requirePhoneNumber')}
330
- value={data.phone_number_collection?.enabled ? t('common.yes') : t('common.no')}
331
- direction={InfoDirection}
332
- alignItems={InfoAlignItems}
333
326
  />
334
327
  <InfoRow
335
- sizes={[1, 1]}
336
328
  label={t('admin.paymentLink.includeFreeTrial')}
337
329
  value={data.subscription_data?.trial_period_days ? t('common.yes') : t('common.no')}
338
- direction={InfoDirection}
339
- alignItems={InfoAlignItems}
340
- />
341
- <InfoRow
342
- sizes={[1, 1]}
343
- label={t('admin.paymentLink.showConfirmPage')}
344
- value={data.after_completion?.type}
345
- direction={InfoDirection}
346
- alignItems={InfoAlignItems}
347
- />
348
- <InfoRow
349
- sizes={[1, 1]}
350
- label={t('admin.paymentLink.mintNft')}
351
- value={data.nft_mint_settings?.factory || ''}
352
- direction={InfoDirection}
353
- alignItems={InfoAlignItems}
354
330
  />
355
331
  <InfoRow
356
- sizes={[1, 1]}
357
- label={t('common.createdAt')}
358
- value={formatTime(data.created_at)}
359
- direction={InfoDirection}
360
- alignItems={InfoAlignItems}
332
+ label={t('admin.paymentLink.requireBillingAddress')}
333
+ value={data.billing_address_collection ? t('common.yes') : t('common.no')}
361
334
  />
362
335
  <InfoRow
363
- sizes={[1, 1]}
364
- label={t('common.updatedAt')}
365
- value={formatTime(data.updated_at)}
366
- direction={InfoDirection}
367
- alignItems={InfoAlignItems}
336
+ label={t('admin.paymentLink.requirePhoneNumber')}
337
+ value={data.phone_number_collection?.enabled ? t('common.yes') : t('common.no')}
368
338
  />
369
- </Stack>
339
+
340
+ <InfoRow label={t('admin.paymentLink.showConfirmPage')} value={data.after_completion?.type} />
341
+ <InfoRow label={t('admin.paymentLink.mintNft')} value={data.nft_mint_settings?.factory || ''} />
342
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
343
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
344
+ </InfoRowGroup>
370
345
  </Box>
371
346
  <Divider />
372
347
  <Box className="section">
@@ -1,7 +1,7 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
- import { api, formatError, formatPrice, formatTime, Table, useMobile } from '@blocklet/payment-react';
4
- import type { TPrice, TPriceExpanded } from '@blocklet/payment-types';
3
+ import { api, formatError, formatPrice, formatRecurring, formatTime, Table, useMobile } from '@blocklet/payment-react';
4
+ import type { PriceRecurring, TPrice, TPriceExpanded } from '@blocklet/payment-types';
5
5
  import { ArrowBackOutlined } from '@mui/icons-material';
6
6
  import { Alert, AlertTitle, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
7
7
  import { styled } from '@mui/system';
@@ -13,6 +13,7 @@ import Currency from '../../../../components/currency';
13
13
  import EventList from '../../../../components/event/list';
14
14
  import InfoMetric from '../../../../components/info-metric';
15
15
  import InfoRow from '../../../../components/info-row';
16
+ import InfoRowGroup from '../../../../components/info-row-group';
16
17
  import MetadataEditor from '../../../../components/metadata/editor';
17
18
  import MetadataList from '../../../../components/metadata/list';
18
19
  import PriceUpsell from '../../../../components/price/upsell';
@@ -25,9 +26,6 @@ const fetchData = (id: string): Promise<TPriceExpanded> => {
25
26
  return api.get(`/api/prices/${id}`).then((res) => res.data);
26
27
  };
27
28
 
28
- const InfoDirection = 'column';
29
- const InfoAlignItems = 'flex-start';
30
-
31
29
  export default function PriceDetail(props: { id: string }) {
32
30
  const { t, locale } = useLocaleContext();
33
31
  const { isMobile } = useMobile();
@@ -85,6 +83,16 @@ export default function PriceDetail(props: { id: string }) {
85
83
  setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
86
84
  };
87
85
 
86
+ const renderType = (type: string) => {
87
+ if (type === 'recurring') {
88
+ return t('admin.price.types.recurring');
89
+ }
90
+ if (type === 'one_time') {
91
+ return t('admin.price.types.onetime');
92
+ }
93
+ return type;
94
+ };
95
+
88
96
  return (
89
97
  <Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
90
98
  <Box>
@@ -204,7 +212,7 @@ export default function PriceDetail(props: { id: string }) {
204
212
  },
205
213
  }}>
206
214
  <Box flex={1} className="payment-link-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
207
- <Box className="section">
215
+ <Box className="section" sx={{ containerType: 'inline-size' }}>
208
216
  <SectionHeader title={t('admin.details')}>
209
217
  <Button
210
218
  variant="text"
@@ -215,56 +223,41 @@ export default function PriceDetail(props: { id: string }) {
215
223
  {t('common.edit')}
216
224
  </Button>
217
225
  </SectionHeader>
218
- <Stack
226
+ <InfoRowGroup
219
227
  sx={{
220
228
  display: 'grid',
221
229
  gridTemplateColumns: {
222
230
  xs: 'repeat(1, 1fr)',
223
- sm: 'repeat(1, 1fr)',
224
- md: 'repeat(2, 1fr)',
225
- lg: 'repeat(3, 1fr)',
231
+ xl: 'repeat(2, 1fr)',
232
+ },
233
+ '@container (min-width: 1000px)': {
234
+ gridTemplateColumns: 'repeat(2, 1fr)',
226
235
  },
227
- gap: {
228
- xs: 0,
229
- md: 2,
236
+ '.info-row-wrapper': {
237
+ gap: 1,
238
+ flexDirection: {
239
+ xs: 'column',
240
+ xl: 'row',
241
+ },
242
+ alignItems: {
243
+ xs: 'flex-start',
244
+ xl: 'center',
245
+ },
246
+ '@container (min-width: 1000px)': {
247
+ flexDirection: 'row',
248
+ alignItems: 'center',
249
+ },
230
250
  },
231
251
  }}>
232
- <InfoRow
233
- label={t('admin.price.type')}
234
- value={data.type}
235
- direction={InfoDirection}
236
- alignItems={InfoAlignItems}
237
- />
238
- <InfoRow
239
- label={t('admin.price.recurring.interval')}
240
- value={data.recurring?.interval}
241
- direction={InfoDirection}
242
- alignItems={InfoAlignItems}
243
- />
244
- <InfoRow
245
- label={t('admin.price.nickname.label')}
246
- value={data.nickname}
247
- direction={InfoDirection}
248
- alignItems={InfoAlignItems}
249
- />
250
- <InfoRow
251
- label={t('admin.price.lookupKey')}
252
- value={data.lookup_key}
253
- direction={InfoDirection}
254
- alignItems={InfoAlignItems}
255
- />
256
- <InfoRow
257
- label={t('common.createdAt')}
258
- value={formatTime(data.created_at)}
259
- direction={InfoDirection}
260
- alignItems={InfoAlignItems}
261
- />
262
- <InfoRow
263
- label={t('common.updatedAt')}
264
- value={formatTime(data.updated_at)}
265
- direction={InfoDirection}
266
- alignItems={InfoAlignItems}
267
- />
252
+ <InfoRow label={t('admin.price.type')} value={renderType(data.type)} />
253
+ {data.type === 'recurring' && (
254
+ <InfoRow
255
+ label={t('admin.price.recurring.interval')}
256
+ value={formatRecurring(data.recurring as PriceRecurring, true, 'per', locale)}
257
+ />
258
+ )}
259
+ <InfoRow label={t('admin.price.lookupKey')} value={data.lookup_key} />
260
+ <InfoRow label={t('admin.price.nickname.label')} value={data.nickname} />
268
261
  {state.editing.price && (
269
262
  <EditPrice
270
263
  // @ts-ignore
@@ -274,7 +267,7 @@ export default function PriceDetail(props: { id: string }) {
274
267
  onCancel={() => setState((prev) => ({ editing: { ...prev.editing, price: false } }))}
275
268
  />
276
269
  )}
277
- </Stack>
270
+ </InfoRowGroup>
278
271
  </Box>
279
272
 
280
273
  <Divider />
@@ -21,14 +21,12 @@ import PricingTablePreview from '../../../../components/pricing-table/preview';
21
21
  import SectionHeader from '../../../../components/section/header';
22
22
  import { formatProductPrice, goBackOrFallback } from '../../../../libs/util';
23
23
  import InfoMetric from '../../../../components/info-metric';
24
+ import InfoRowGroup from '../../../../components/info-row-group';
24
25
 
25
26
  const fetchData = (id: string): Promise<TPricingTableExpanded> => {
26
27
  return api.get(`/api/pricing-tables/${id}`).then((res) => res.data);
27
28
  };
28
29
 
29
- const InfoDirection = 'column';
30
- const InfoAlignItems = 'flex-start';
31
-
32
30
  export default function PricingTableDetail(props: { id: string }) {
33
31
  const { t, locale } = useLocaleContext();
34
32
  const navigate = useNavigate();
@@ -256,35 +254,35 @@ export default function PricingTableDetail(props: { id: string }) {
256
254
  <Divider />
257
255
  <Box className="section">
258
256
  <SectionHeader title={t('admin.details')} />
259
- <Stack
257
+ <InfoRowGroup
260
258
  sx={{
261
259
  display: 'grid',
262
260
  gridTemplateColumns: {
263
261
  xs: 'repeat(1, 1fr)',
264
- sm: 'repeat(1, 1fr)',
265
- md: 'repeat(2, 1fr)',
266
- lg: 'repeat(3, 1fr)',
262
+ xl: 'repeat(2, 1fr)',
267
263
  },
268
- gap: {
269
- xs: 0,
270
- md: 2,
264
+ '@container (min-width: 1000px)': {
265
+ gridTemplateColumns: 'repeat(2, 1fr)',
266
+ },
267
+ '.info-row-wrapper': {
268
+ gap: 1,
269
+ flexDirection: {
270
+ xs: 'column',
271
+ xl: 'row',
272
+ },
273
+ alignItems: {
274
+ xs: 'flex-start',
275
+ xl: 'center',
276
+ },
277
+ '@container (min-width: 1000px)': {
278
+ flexDirection: 'row',
279
+ alignItems: 'center',
280
+ },
271
281
  },
272
282
  }}>
273
- <InfoRow
274
- sizes={[1, 1]}
275
- label={t('common.createdAt')}
276
- value={formatTime(data.created_at)}
277
- direction={InfoDirection}
278
- alignItems={InfoAlignItems}
279
- />
280
- <InfoRow
281
- sizes={[1, 1]}
282
- label={t('common.updatedAt')}
283
- value={formatTime(data.updated_at)}
284
- direction={InfoDirection}
285
- alignItems={InfoAlignItems}
286
- />
287
- </Stack>
283
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
284
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
285
+ </InfoRowGroup>
288
286
  </Box>
289
287
  <Divider />
290
288
  <Box className="section">