payment-kit 1.13.17 → 1.13.19

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 (109) hide show
  1. package/README.md +14 -0
  2. package/api/src/index.ts +17 -6
  3. package/api/src/integrations/stripe/handlers/index.ts +53 -0
  4. package/api/src/integrations/stripe/handlers/invoice.ts +252 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +172 -0
  6. package/api/src/integrations/stripe/handlers/setup-intent.ts +42 -0
  7. package/api/src/integrations/stripe/handlers/subscription.ts +61 -0
  8. package/api/src/integrations/stripe/resource.ts +317 -0
  9. package/api/src/integrations/stripe/setup.ts +50 -0
  10. package/api/src/jobs/invoice.ts +11 -0
  11. package/api/src/jobs/payment.ts +15 -7
  12. package/api/src/jobs/subscription.ts +18 -2
  13. package/api/src/libs/session.ts +104 -8
  14. package/api/src/libs/util.ts +47 -1
  15. package/api/src/routes/checkout-sessions.ts +134 -27
  16. package/api/src/routes/connect/collect.ts +12 -4
  17. package/api/src/routes/connect/pay.ts +30 -20
  18. package/api/src/routes/connect/setup.ts +12 -4
  19. package/api/src/routes/connect/shared.ts +28 -4
  20. package/api/src/routes/connect/subscribe.ts +12 -5
  21. package/api/src/routes/customers.ts +5 -5
  22. package/api/src/routes/events.ts +9 -6
  23. package/api/src/routes/index.ts +2 -0
  24. package/api/src/routes/integrations/stripe.ts +64 -0
  25. package/api/src/routes/invoices.ts +19 -9
  26. package/api/src/routes/payment-intents.ts +19 -9
  27. package/api/src/routes/payment-links.ts +57 -15
  28. package/api/src/routes/payment-methods.ts +98 -1
  29. package/api/src/routes/prices.ts +71 -14
  30. package/api/src/routes/products.ts +79 -22
  31. package/api/src/routes/settings.ts +10 -11
  32. package/api/src/routes/subscription-items.ts +5 -5
  33. package/api/src/routes/subscriptions.ts +61 -10
  34. package/api/src/routes/usage-records.ts +52 -18
  35. package/api/src/routes/webhook-attempts.ts +5 -5
  36. package/api/src/routes/webhook-endpoints.ts +5 -5
  37. package/api/src/store/migrations/20230905-genesis.ts +2 -2
  38. package/api/src/store/migrations/20230911-seeding.ts +4 -3
  39. package/api/src/store/models/checkout-session.ts +15 -7
  40. package/api/src/store/models/index.ts +31 -7
  41. package/api/src/store/models/invoice.ts +1 -1
  42. package/api/src/store/models/payment-intent.ts +2 -5
  43. package/api/src/store/models/payment-link.ts +1 -1
  44. package/api/src/store/models/payment-method.ts +54 -33
  45. package/api/src/store/models/price.ts +52 -17
  46. package/api/src/store/models/product.ts +0 -3
  47. package/api/src/store/models/subscription.ts +3 -5
  48. package/api/src/store/models/types.ts +56 -2
  49. package/api/third.d.ts +2 -0
  50. package/blocklet.yml +1 -1
  51. package/package.json +36 -29
  52. package/public/currencies/dai.png +0 -0
  53. package/public/currencies/dollar.png +0 -0
  54. package/public/currencies/usdc.png +0 -0
  55. package/public/currencies/usdt.png +0 -0
  56. package/public/methods/arcblock.png +0 -0
  57. package/public/methods/binance.png +0 -0
  58. package/public/methods/coinbase.png +0 -0
  59. package/public/methods/ethereum.jpg +0 -0
  60. package/public/methods/stripe.png +0 -0
  61. package/src/components/checkout/form/address.tsx +86 -10
  62. package/src/components/checkout/form/index.tsx +169 -83
  63. package/src/components/checkout/form/phone.tsx +96 -0
  64. package/src/components/checkout/form/stripe.tsx +195 -0
  65. package/src/components/checkout/pay.tsx +115 -34
  66. package/src/components/checkout/product-item.tsx +4 -3
  67. package/src/components/checkout/summary.tsx +5 -4
  68. package/src/components/drawer-form.tsx +4 -4
  69. package/src/components/input.tsx +22 -4
  70. package/src/components/invoice/table.tsx +8 -3
  71. package/src/components/payment-link/before-pay.tsx +11 -6
  72. package/src/components/payment-link/chrome.tsx +13 -0
  73. package/src/components/payment-link/preview.tsx +31 -0
  74. package/src/components/payment-link/product-select.tsx +8 -3
  75. package/src/components/payment-method/arcblock.tsx +53 -0
  76. package/src/components/payment-method/bitcoin.tsx +53 -0
  77. package/src/components/payment-method/ethereum.tsx +53 -0
  78. package/src/components/payment-method/form.tsx +54 -0
  79. package/src/components/payment-method/stripe.tsx +45 -0
  80. package/src/components/portal/invoice/list.tsx +1 -1
  81. package/src/components/portal/subscription/list.tsx +1 -1
  82. package/src/components/price/currency-select.tsx +53 -0
  83. package/src/components/price/form.tsx +118 -24
  84. package/src/components/product/add-price.tsx +1 -1
  85. package/src/components/product/edit-price.tsx +6 -2
  86. package/src/components/subscription/items/index.tsx +7 -6
  87. package/src/components/subscription/items/usage-records.tsx +98 -0
  88. package/src/components/subscription/list.tsx +3 -2
  89. package/src/components/subscription/status.tsx +68 -0
  90. package/src/contexts/settings.tsx +2 -2
  91. package/src/env.d.ts +2 -0
  92. package/src/libs/util.ts +116 -21
  93. package/src/locales/en.tsx +71 -3
  94. package/src/pages/admin/billing/invoices/detail.tsx +5 -2
  95. package/src/pages/admin/billing/subscriptions/detail.tsx +6 -6
  96. package/src/pages/admin/customers/customers/detail.tsx +13 -1
  97. package/src/pages/admin/payments/intents/detail.tsx +8 -3
  98. package/src/pages/admin/payments/links/create.tsx +23 -3
  99. package/src/pages/admin/payments/links/detail.tsx +13 -26
  100. package/src/pages/admin/products/prices/detail.tsx +55 -11
  101. package/src/pages/admin/products/prices/list.tsx +7 -1
  102. package/src/pages/admin/products/products/create.tsx +1 -1
  103. package/src/pages/admin/products/products/detail.tsx +14 -7
  104. package/src/pages/admin/settings/index.tsx +16 -6
  105. package/src/pages/admin/settings/payment-methods/create.tsx +81 -0
  106. package/src/pages/admin/settings/{payment-methods.tsx → payment-methods/index.tsx} +9 -6
  107. package/src/pages/checkout/pay.tsx +3 -1
  108. package/src/pages/customer/index.tsx +12 -1
  109. package/public/.gitkeep +0 -0
@@ -0,0 +1,98 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import type { TUsageRecord } from '@did-pay/types';
3
+ import { Alert, Box, Button, CircularProgress } from '@mui/material';
4
+ import { useRequest } from 'ahooks';
5
+ import { useState } from 'react';
6
+
7
+ import api from '../../../libs/api';
8
+ import { formatToDatetime } from '../../../libs/util';
9
+ import ConfirmDialog from '../../confirm';
10
+ import Table from '../../table';
11
+
12
+ const fetchData = (id: string): Promise<{ list: TUsageRecord[]; count: number }> => {
13
+ return api.get(`/api/usage-records?subscription_item_id=${id}&pageSize=100`).then((res) => res.data);
14
+ };
15
+
16
+ export function UsageRecordDialog(props: { id: string; onConfirm: any }) {
17
+ const { t } = useLocaleContext();
18
+ const { loading, error, data } = useRequest(() => fetchData(props.id), { refreshDeps: [props.id] });
19
+
20
+ if (error) {
21
+ return (
22
+ <ConfirmDialog
23
+ title={t('admin.subscription.usage.current')}
24
+ message={<Alert severity="error">{error.message}</Alert>}
25
+ onConfirm={props.onConfirm}
26
+ onCancel={props.onConfirm}
27
+ />
28
+ );
29
+ }
30
+
31
+ if (loading || !data) {
32
+ return (
33
+ <ConfirmDialog
34
+ title={t('admin.subscription.usage.current')}
35
+ message={<CircularProgress />}
36
+ onConfirm={props.onConfirm}
37
+ onCancel={props.onConfirm}
38
+ />
39
+ );
40
+ }
41
+
42
+ const columns = [
43
+ {
44
+ label: t('common.createdAt'),
45
+ name: 'id',
46
+ options: {
47
+ customBodyRenderLite: (_: string, index: number) => {
48
+ const item = data.list[index] as TUsageRecord;
49
+ return formatToDatetime(item.created_at);
50
+ },
51
+ },
52
+ },
53
+ {
54
+ label: t('admin.subscription.usage.used'),
55
+ name: 'quantity',
56
+ align: 'center',
57
+ },
58
+ ];
59
+
60
+ return (
61
+ <ConfirmDialog
62
+ title={t('admin.subscription.usage.current')}
63
+ message={
64
+ <Table
65
+ data={data.list}
66
+ columns={columns}
67
+ loading={false}
68
+ footer={false}
69
+ toolbar={false}
70
+ components={{
71
+ TableToolbar: () => null,
72
+ TableFooter: () => null,
73
+ }}
74
+ options={{
75
+ count: data.count,
76
+ page: 0,
77
+ rowsPerPage: 100,
78
+ }}
79
+ />
80
+ }
81
+ onConfirm={props.onConfirm}
82
+ onCancel={props.onConfirm}
83
+ />
84
+ );
85
+ }
86
+
87
+ export default function UsageRecords({ id }: { id: string }) {
88
+ const { t } = useLocaleContext();
89
+ const [open, setOpen] = useState(false);
90
+ return (
91
+ <Box>
92
+ <Button size="small" variant="text" color="info" onClick={() => setOpen(true)}>
93
+ {t('admin.subscription.usage.view')}
94
+ </Button>
95
+ {open && <UsageRecordDialog id={id} onConfirm={() => setOpen(false)} />}
96
+ </Box>
97
+ );
98
+ }
@@ -8,10 +8,11 @@ import { useEffect, useState } from 'react';
8
8
  import { useNavigate } from 'react-router-dom';
9
9
 
10
10
  import api from '../../libs/api';
11
- import { formatSubscriptionProduct, formatTime, getSubscriptionStatusColor } from '../../libs/util';
11
+ import { formatSubscriptionProduct, formatTime } from '../../libs/util';
12
12
  import Status from '../status';
13
13
  import Table from '../table';
14
14
  import SubscriptionActions from './actions';
15
+ import SubscriptionStatus from './status';
15
16
 
16
17
  const fetchData = (params: Record<string, any> = {}): Promise<{ list: TSubscriptionExpanded[]; count: number }> => {
17
18
  const search = new URLSearchParams();
@@ -98,7 +99,7 @@ export default function SubscriptionList({ customer_id, features, status }: List
98
99
  options: {
99
100
  customBodyRenderLite: (_: string, index: number) => {
100
101
  const item = data.list[index] as TSubscriptionExpanded;
101
- return <Status label={item.status} color={getSubscriptionStatusColor(item.status)} />;
102
+ return <SubscriptionStatus subscription={item} />;
102
103
  },
103
104
  },
104
105
  },
@@ -0,0 +1,68 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import type { TSubscription } from '@did-pay/types';
3
+ import { AccessTimeOutlined } from '@mui/icons-material';
4
+
5
+ import { formatToDate, getSubscriptionStatusColor } from '../../libs/util';
6
+ import Status from '../status';
7
+
8
+ export default function SubscriptionStatus({
9
+ subscription,
10
+ ...rest
11
+ }: {
12
+ subscription: TSubscription;
13
+ [key: string]: any;
14
+ }) {
15
+ const { t } = useLocaleContext();
16
+ if (subscription.cancel_at_period_end) {
17
+ return (
18
+ <Status
19
+ icon={<AccessTimeOutlined />}
20
+ label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.current_period_end * 1000) })}
21
+ color="default"
22
+ {...rest}
23
+ />
24
+ );
25
+ }
26
+
27
+ if (subscription.cancel_at) {
28
+ return (
29
+ <Status
30
+ icon={<AccessTimeOutlined />}
31
+ label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.cancel_at * 1000) })}
32
+ color="default"
33
+ {...rest}
34
+ />
35
+ );
36
+ }
37
+
38
+ if (subscription.pause_collection) {
39
+ if (subscription.pause_collection.resumes_at) {
40
+ return (
41
+ <Status
42
+ icon={<AccessTimeOutlined />}
43
+ label={t('admin.subscription.pause.until.custom', {
44
+ date: formatToDate(subscription.pause_collection.resumes_at * 1000),
45
+ })}
46
+ color="default"
47
+ {...rest}
48
+ />
49
+ );
50
+ }
51
+
52
+ return <Status label={t('admin.subscription.pause.until.never')} color="default" />;
53
+ }
54
+
55
+ if (subscription.trail_end && subscription.trail_end > Date.now() / 1000) {
56
+ return (
57
+ <Status
58
+ label={t('admin.subscription.trailEnd', {
59
+ date: formatToDate(subscription.trail_end * 1000),
60
+ })}
61
+ color="info"
62
+ {...rest}
63
+ />
64
+ );
65
+ }
66
+
67
+ return <Status label={subscription.status} color={getSubscriptionStatusColor(subscription.status)} {...rest} />;
68
+ }
@@ -1,4 +1,4 @@
1
- import type { TPaymentCurrency, TPaymentMethod } from '@did-pay/types';
1
+ import type { TPaymentCurrency, TPaymentMethodExpanded } from '@did-pay/types';
2
2
  import { Alert, CircularProgress } from '@mui/material';
3
3
  import { useLocalStorageState, useRequest } from 'ahooks';
4
4
  import type { Axios } from 'axios';
@@ -7,7 +7,7 @@ import { createContext, useContext } from 'react';
7
7
  import api from '../libs/api';
8
8
 
9
9
  export interface Settings {
10
- paymentMethods: TPaymentMethod[];
10
+ paymentMethods: TPaymentMethodExpanded[];
11
11
  baseCurrency: TPaymentCurrency;
12
12
  }
13
13
 
package/src/env.d.ts CHANGED
@@ -15,3 +15,5 @@ declare module '@arcblock/*';
15
15
  declare module '@blocklet/*';
16
16
 
17
17
  declare module 'pretty-ms-i18n';
18
+
19
+ declare module 'google-libphonenumber';
package/src/libs/util.ts CHANGED
@@ -1,16 +1,20 @@
1
+ /* eslint-disable @typescript-eslint/indent */
1
2
  import type {
2
3
  LineItem,
4
+ PriceCurrency,
3
5
  PriceRecurring,
4
6
  TCheckoutSessionExpanded,
5
7
  TLineItemExpanded,
6
8
  TPaymentCurrency,
7
9
  TPaymentLinkExpanded,
10
+ TPaymentMethodExpanded,
8
11
  TPrice,
9
12
  TProductExpanded,
10
13
  TSubscriptionItemExpanded,
11
14
  } from '@did-pay/types';
12
15
  import { BN, fromUnitToToken } from '@ocap/util';
13
16
  import cloneDeep from 'lodash/cloneDeep';
17
+ import isEqual from 'lodash/isEqual';
14
18
 
15
19
  import dayjs from './dayjs';
16
20
 
@@ -128,7 +132,10 @@ export const formatProductPrice = (
128
132
  };
129
133
 
130
134
  export const formatPrice = (price: TPrice, currency: TPaymentCurrency, unit_label?: string, quantity: number = 1) => {
131
- const amount = fromUnitToToken(new BN(price.unit_amount).mul(new BN(quantity)), currency.decimal).toString();
135
+ const amount = fromUnitToToken(
136
+ new BN(getPriceUintAmountByCurrency(price, currency)).mul(new BN(quantity)),
137
+ currency.decimal
138
+ ).toString();
132
139
  if (price?.type === 'recurring' && price.recurring) {
133
140
  const recurring = formatRecurring(price.recurring, false, '/');
134
141
 
@@ -192,12 +199,33 @@ export function formatRecurring(recurring: PriceRecurring, translate: boolean =
192
199
  return `every ${recurring.interval_count} ${recurring.interval}s`;
193
200
  }
194
201
 
202
+ export function getPriceUintAmountByCurrency(price: TPrice, currency: TPaymentCurrency) {
203
+ const options = getPriceCurrencyOptions(price);
204
+ const option = options.find((x) => x.currency_id === currency.id);
205
+ if (option) {
206
+ return option.unit_amount;
207
+ }
208
+
209
+ return price.unit_amount;
210
+ }
211
+
212
+ export function getPriceCurrencyOptions(price: TPrice): PriceCurrency[] {
213
+ if (Array.isArray(price.currency_options)) {
214
+ return price.currency_options;
215
+ }
216
+
217
+ return [{ currency_id: price.currency_id, unit_amount: price.unit_amount, tiers: null, custom_unit_amount: null }];
218
+ }
219
+
195
220
  export function formatLineItemPricing(
196
221
  item: TLineItemExpanded,
197
222
  currency: TPaymentCurrency,
198
223
  trial: number
199
224
  ): { primary: string; secondary?: string } {
200
- const amount = fromUnitToToken(new BN(item.price.unit_amount).mul(new BN(item.quantity))).toString();
225
+ const amount = fromUnitToToken(
226
+ new BN(getPriceUintAmountByCurrency(item.price, currency)).mul(new BN(item.quantity)),
227
+ currency.decimal
228
+ ).toString();
201
229
 
202
230
  if (item.price.type === 'recurring' && item.price.recurring) {
203
231
  if (trial > 0) {
@@ -222,7 +250,7 @@ export function formatLineItemPricing(
222
250
  };
223
251
  }
224
252
 
225
- export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTrial = false) {
253
+ export function getCheckoutAmount(items: TLineItemExpanded[], currency: TPaymentCurrency, includeFreeTrial = false) {
226
254
  const subtotal = items
227
255
  .reduce((acc, x) => {
228
256
  if (x.price.type === 'recurring') {
@@ -233,7 +261,7 @@ export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTr
233
261
  return acc;
234
262
  }
235
263
  }
236
- return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
264
+ return acc.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
237
265
  }, new BN(0))
238
266
  .toString();
239
267
 
@@ -247,7 +275,7 @@ export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTr
247
275
  return acc;
248
276
  }
249
277
  }
250
- return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
278
+ return acc.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
251
279
  }, new BN(0))
252
280
  .toString();
253
281
 
@@ -255,31 +283,36 @@ export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTr
255
283
  }
256
284
 
257
285
  export function formatPaymentLinkPricing(link: TPaymentLinkExpanded, currency: TPaymentCurrency) {
258
- const amount = getCheckoutAmount(link.line_items, !!link.subscription_data?.trial_period_days);
259
- return formatCheckoutHeadlines({
260
- mode: 'payment',
261
- status: 'open',
262
- payment_status: 'unpaid',
263
- currency,
264
- amount_total: amount.total,
265
- amount_subtotal: amount.subtotal,
266
- expires_at: dayjs().add(30, 'days').unix(),
267
- ...link,
268
- });
286
+ const amount = getCheckoutAmount(link.line_items, currency, !!link.subscription_data?.trial_period_days);
287
+ return formatCheckoutHeadlines(
288
+ {
289
+ mode: 'payment',
290
+ status: 'open',
291
+ payment_status: 'unpaid',
292
+ currency,
293
+ amount_total: amount.total,
294
+ amount_subtotal: amount.subtotal,
295
+ expires_at: dayjs().add(30, 'days').unix(),
296
+ ...link,
297
+ } as any,
298
+ currency
299
+ );
269
300
  }
270
301
 
271
- export function formatCheckoutHeadlines(session: TCheckoutSessionExpanded): {
302
+ export function formatCheckoutHeadlines(
303
+ session: TCheckoutSessionExpanded,
304
+ currency: TPaymentCurrency
305
+ ): {
272
306
  action: string;
273
307
  amount: string;
274
308
  then?: string;
275
309
  secondary?: string;
276
310
  } {
277
311
  const items = session.line_items as TLineItemExpanded[];
278
- const total = session.amount_total;
279
312
  const trial = session.subscription_data?.trial_period_days || 0;
280
- const currency = session.currency as TPaymentCurrency;
281
313
 
282
314
  const brand = getStatementDescriptor(items);
315
+ const { total } = getCheckoutAmount(items, currency, !!trial);
283
316
  const amount = `${fromUnitToToken(total, currency.decimal)} ${currency.symbol}`;
284
317
 
285
318
  // empty
@@ -313,7 +346,7 @@ export function formatCheckoutHeadlines(session: TCheckoutSessionExpanded): {
313
346
  if (x.price.recurring?.usage_type === 'metered') {
314
347
  return acc;
315
348
  }
316
- return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
349
+ return acc.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
317
350
  }, new BN(0)),
318
351
  currency.decimal
319
352
  ),
@@ -360,7 +393,7 @@ export function formatCheckoutHeadlines(session: TCheckoutSessionExpanded): {
360
393
  if (x.price.recurring?.usage_type === 'metered') {
361
394
  return acc;
362
395
  }
363
- return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
396
+ return acc.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
364
397
  }, new BN(0)),
365
398
  currency.decimal
366
399
  );
@@ -432,6 +465,29 @@ export function getWebhookStatusColor(status: string) {
432
465
  }
433
466
  }
434
467
 
468
+ export function isPriceCurrencyAligned(list: LineItem[], products: TProductExpanded[], index: number) {
469
+ const prices = list.map((x) => {
470
+ const product = getProductByPriceId(products, x.price_id);
471
+ const price = product?.prices.find((p) => p.id === x.price_id);
472
+ return price;
473
+ });
474
+
475
+ const current = getPriceCurrencyOptions(prices[index] as TPrice)
476
+ .map((x) => x.currency_id)
477
+ .sort();
478
+
479
+ for (let i = 0; i < index; i++) {
480
+ const previous = getPriceCurrencyOptions(prices[i] as TPrice)
481
+ .map((x) => x.currency_id)
482
+ .sort();
483
+ if (isEqual(current, previous) === false) {
484
+ return false;
485
+ }
486
+ }
487
+
488
+ return true;
489
+ }
490
+
435
491
  export function isPriceRecurringAligned(list: LineItem[], products: TProductExpanded[], index: number) {
436
492
  const prices = list.map((x) => {
437
493
  const product = getProductByPriceId(products, x.price_id);
@@ -462,6 +518,16 @@ export function isPriceRecurringAligned(list: LineItem[], products: TProductExpa
462
518
  });
463
519
  }
464
520
 
521
+ export function isPriceAligned(list: LineItem[], products: TProductExpanded[], index: number) {
522
+ const currency = isPriceCurrencyAligned(list, products, index);
523
+ const recurring = isPriceRecurringAligned(list, products, index);
524
+ return {
525
+ currency,
526
+ recurring,
527
+ aligned: currency && recurring,
528
+ };
529
+ }
530
+
465
531
  export function formatSubscriptionProduct(items: TSubscriptionItemExpanded[], maxLength = 2) {
466
532
  const names = items.map((x) => x.price.product.name);
467
533
  return (
@@ -472,3 +538,32 @@ export function formatSubscriptionProduct(items: TSubscriptionItemExpanded[], ma
472
538
  export function formatAmount(amount: string, decimals: number, points = 2) {
473
539
  return Number(fromUnitToToken(amount, decimals)).toFixed(points);
474
540
  }
541
+
542
+ export function findCurrency(methods: TPaymentMethodExpanded[], currencyId: string) {
543
+ for (const method of methods) {
544
+ for (const currency of method.payment_currencies) {
545
+ if (currency.id === currencyId) {
546
+ return currency;
547
+ }
548
+ }
549
+ }
550
+
551
+ return null;
552
+ }
553
+
554
+ export function filterCurrencies(method: TPaymentMethodExpanded, hasSelected: (currency: any) => boolean) {
555
+ method.payment_currencies = method.payment_currencies.filter((x) => !hasSelected(x));
556
+ return method;
557
+ }
558
+
559
+ export function getSupportedPaymentMethods(methods: TPaymentMethodExpanded[], hasSelected: (currency: any) => boolean) {
560
+ const filtered = cloneDeep(methods).map((x) => filterCurrencies(x, hasSelected));
561
+ return filtered.filter((x) => x.payment_currencies.length);
562
+ }
563
+
564
+ export function getSupportedPaymentCurrencies(items: TLineItemExpanded[]) {
565
+ const currencies = items.reduce((acc, x: any) => {
566
+ return acc.concat(x.price.currency_options.map((c: any) => c.currency_id));
567
+ }, []);
568
+ return Array.from(new Set(currencies));
569
+ }
@@ -92,6 +92,7 @@ export default flat({
92
92
  archived: 'This product has been archived',
93
93
  archivedTip:
94
94
  'This product can’t be added to new invoices, subscriptions, payment links, or pricing tables. Any existing subscriptions with this product remain active until canceled and any existing payment links or pricing tables are deactivated.',
95
+ locked: 'This product is locked because at least one of its prices is used by a subscription or a payment.',
95
96
  image: {
96
97
  label: 'Image',
97
98
  add: 'Add image',
@@ -131,6 +132,8 @@ export default flat({
131
132
  additional: 'Additional options',
132
133
  model: 'Pricing model',
133
134
  amount: 'Price',
135
+ locked: 'This price is locked because it is used by a subscription or a payment.',
136
+ amountTip: 'Choose recurring for subscriptions and one-time for everything else.',
134
137
  duplicate: 'Duplicate price',
135
138
  edit: 'Edit price',
136
139
  archive: 'Archive price',
@@ -152,8 +155,14 @@ export default flat({
152
155
  recurring: {
153
156
  interval: 'Billing period',
154
157
  metered: 'Usage is metered?',
158
+ meteredTip:
159
+ 'Metered billing lets you charge customers based on reported usage at the end of each billing period.',
155
160
  aggregate: 'Charge for metered usage by',
156
161
  },
162
+ currency: {
163
+ add: 'Add more currencies',
164
+ list: 'Currencies',
165
+ },
157
166
  },
158
167
  coupon: {
159
168
  create: 'Create Coupon',
@@ -186,7 +195,8 @@ export default flat({
186
195
  noProducts: 'Payment link must have at least one product',
187
196
  noRedirectUrl: 'Payment link must have a redirect url',
188
197
  noSubscriptionTrialDays: 'You must specify a trial period for subscription',
189
- notAligned: 'The prices on all line items must have the same recurring interval',
198
+ recurringNotAligned: 'The prices on all line items must have the same recurring interval',
199
+ currencyNotAligned: 'The prices on all line items must have the same currency settings',
190
200
  edit: 'Edit payment link',
191
201
  rename: 'Change name',
192
202
  archive: 'Archive payment link',
@@ -207,8 +217,48 @@ export default flat({
207
217
  refund: 'Refund payment',
208
218
  },
209
219
  paymentMethod: {
210
- name: 'Payment Method',
220
+ _name: 'Payment Method',
211
221
  type: 'Type',
222
+ add: 'Add payment method',
223
+ save: 'Save payment method',
224
+ saved: 'Payment method successfully saved',
225
+ settings: 'Settings',
226
+ name: {
227
+ label: 'Name',
228
+ tip: 'Consumer facing',
229
+ },
230
+ description: {
231
+ label: 'Description',
232
+ tip: 'Not consumer facing',
233
+ },
234
+ stripe: {
235
+ publishable_key: {
236
+ label: 'Publishable Key',
237
+ tip: 'Publishable Key, See Dashboard > Developers > API Keys',
238
+ },
239
+ secret_key: {
240
+ label: 'Secret Key',
241
+ tip: 'Secret Key, See Dashboard > Developers > API Keys',
242
+ },
243
+ webhook_signing_secret: {
244
+ label: 'Webhook Signing Secret',
245
+ tip: 'Webhook Signing Secret, See Dashboard > Developers > Webhooks > Signing Secret',
246
+ },
247
+ },
248
+ arcblock: {
249
+ chain_id: {
250
+ label: 'Chain ID',
251
+ tip: 'Just a name',
252
+ },
253
+ api_host: {
254
+ label: 'API Host',
255
+ tip: 'The graphql endpoint to send transaction to',
256
+ },
257
+ explorer_host: {
258
+ label: 'Explorer Host',
259
+ tip: 'The webapp endpoint to view transaction details',
260
+ },
261
+ },
212
262
  },
213
263
  paymentCurrency: {
214
264
  name: 'Payment Currency',
@@ -245,6 +295,7 @@ export default flat({
245
295
  collectionMethod: 'Billing',
246
296
  currentPeriod: 'Current Period',
247
297
  trialingPeriod: 'Trial Period',
298
+ trailEnd: 'Trial ends {date}',
248
299
  discount: 'Discount',
249
300
  startedAt: 'Started',
250
301
  nextInvoice: 'Next Invoice',
@@ -257,6 +308,7 @@ export default flat({
257
308
  schedule: 'Scheduled to cancel',
258
309
  title: 'Cancel subscription',
259
310
  required: 'Custom cancel time is required',
311
+ will: 'Cancels on {date}',
260
312
  at: {
261
313
  title: 'Cancel',
262
314
  now: 'Immediately ({date})',
@@ -281,6 +333,17 @@ export default flat({
281
333
  void: 'Void invoices',
282
334
  voidTip: 'For businesses not currently offering services.',
283
335
  },
336
+ until: {
337
+ never: 'Collection paused',
338
+ custom: 'Collection paused until {date}',
339
+ },
340
+ },
341
+ usage: {
342
+ title: 'Usage records',
343
+ current: 'Usage records for current period',
344
+ view: 'View usage',
345
+ vary: 'Varies with usage',
346
+ used: 'Unit used',
284
347
  },
285
348
  },
286
349
  customer: {
@@ -331,8 +394,11 @@ export default flat({
331
394
  payment: 'Pay',
332
395
  subscription: 'Subscribe',
333
396
  setup: 'Subscribe',
397
+ continue: 'Confirm {action}',
334
398
  connect: 'Connect and {action}',
399
+ login: 'Login to load and save contact information',
335
400
  portal: 'Manage subscriptions',
401
+ cardPay: '{action} with card',
336
402
  completed: {
337
403
  payment: 'Thanks for your purchase',
338
404
  subscription: 'Thanks for your subscribing',
@@ -341,6 +407,8 @@ export default flat({
341
407
  },
342
408
  confirm:
343
409
  'By confirming your subscription, you allow {payee} to charge your account for this and future payments in accordance with their terms. You can always cancel your subscription.',
410
+ required: 'Required',
411
+ invalid: 'Invalid',
344
412
  billing: {
345
413
  auto: 'Country',
346
414
  required: 'Billing address',
@@ -349,7 +417,7 @@ export default flat({
349
417
  city: 'City or town',
350
418
  line1: 'Address',
351
419
  line2: 'Line2',
352
- postal_code: 'Postal Code',
420
+ postal_code: 'Postal code',
353
421
  },
354
422
  customer: {
355
423
  name: 'Name',
@@ -128,7 +128,7 @@ export default function InvoiceDetail(props: { id: string }) {
128
128
  )}
129
129
  <InfoRow label={t('admin.invoice.billing')} value={data.collection_method} />
130
130
  <InfoRow
131
- label={t('admin.paymentMethod.name')}
131
+ label={t('admin.paymentMethod._name')}
132
132
  value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
133
133
  />
134
134
  <InfoRow
@@ -205,7 +205,10 @@ export default function InvoiceDetail(props: { id: string }) {
205
205
  <Box className="section">
206
206
  <SectionHeader title={t('admin.events')} />
207
207
  <Box className="section-body">
208
- <EventList features={{ toolbar: false }} object_id={data.id} />
208
+ <EventList
209
+ features={{ toolbar: false }}
210
+ object_id={[data.id, data.payment_intent_id].filter(Boolean).join(',')}
211
+ />
209
212
  </Box>
210
213
  </Box>
211
214
  </Root>
@@ -18,11 +18,11 @@ import InfoRow from '../../../../components/info-row';
18
18
  import InvoiceList from '../../../../components/invoice/list';
19
19
  import MetadataEditor from '../../../../components/metadata/editor';
20
20
  import SectionHeader from '../../../../components/section/header';
21
- import Status from '../../../../components/status';
22
21
  import SubscriptionActions from '../../../../components/subscription/actions';
23
22
  import SubscriptionItemList from '../../../../components/subscription/items';
23
+ import SubscriptionStatus from '../../../../components/subscription/status';
24
24
  import api from '../../../../libs/api';
25
- import { formatError, formatSubscriptionProduct, formatTime, getSubscriptionStatusColor } from '../../../../libs/util';
25
+ import { formatError, formatSubscriptionProduct, formatTime } from '../../../../libs/util';
26
26
 
27
27
  const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
28
28
  return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
@@ -95,7 +95,7 @@ export default function SubscriptionDetail(props: { id: string }) {
95
95
  <Typography variant="h5" sx={{ fontWeight: 600 }}>
96
96
  {formatSubscriptionProduct(data.items)}
97
97
  </Typography>
98
- <Status label={data.status} color={getSubscriptionStatusColor(data.status)} sx={{ ml: 1 }} />
98
+ <SubscriptionStatus subscription={data} sx={{ ml: 1 }} />
99
99
  </Stack>
100
100
  <SubscriptionActions data={data} onChange={runAsync} variant="normal" />
101
101
  </Stack>
@@ -166,17 +166,17 @@ export default function SubscriptionDetail(props: { id: string }) {
166
166
  <InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
167
167
  <InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
168
168
  <InfoRow
169
- label={t('admin.paymentMethod.name')}
169
+ label={t('admin.paymentMethod._name')}
170
170
  value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
171
171
  />
172
172
  <InfoRow
173
173
  label={t('admin.paymentCurrency.name')}
174
174
  value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
175
175
  />
176
- {data.payment_details?.tx_hash && (
176
+ {data.payment_details?.arcblock?.tx_hash && (
177
177
  <InfoRow
178
178
  label={t('common.txHash')}
179
- value={<TxLink hash={data.payment_details?.tx_hash} method={data.paymentMethod} />}
179
+ value={<TxLink hash={data.payment_details.arcblock?.tx_hash} method={data.paymentMethod} />}
180
180
  />
181
181
  )}
182
182
  </Stack>