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.
- package/README.md +14 -0
- package/api/src/index.ts +17 -6
- package/api/src/integrations/stripe/handlers/index.ts +53 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +252 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +172 -0
- package/api/src/integrations/stripe/handlers/setup-intent.ts +42 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +61 -0
- package/api/src/integrations/stripe/resource.ts +317 -0
- package/api/src/integrations/stripe/setup.ts +50 -0
- package/api/src/jobs/invoice.ts +11 -0
- package/api/src/jobs/payment.ts +15 -7
- package/api/src/jobs/subscription.ts +18 -2
- package/api/src/libs/session.ts +104 -8
- package/api/src/libs/util.ts +47 -1
- package/api/src/routes/checkout-sessions.ts +134 -27
- package/api/src/routes/connect/collect.ts +12 -4
- package/api/src/routes/connect/pay.ts +30 -20
- package/api/src/routes/connect/setup.ts +12 -4
- package/api/src/routes/connect/shared.ts +28 -4
- package/api/src/routes/connect/subscribe.ts +12 -5
- package/api/src/routes/customers.ts +5 -5
- package/api/src/routes/events.ts +9 -6
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/integrations/stripe.ts +64 -0
- package/api/src/routes/invoices.ts +19 -9
- package/api/src/routes/payment-intents.ts +19 -9
- package/api/src/routes/payment-links.ts +57 -15
- package/api/src/routes/payment-methods.ts +98 -1
- package/api/src/routes/prices.ts +71 -14
- package/api/src/routes/products.ts +79 -22
- package/api/src/routes/settings.ts +10 -11
- package/api/src/routes/subscription-items.ts +5 -5
- package/api/src/routes/subscriptions.ts +61 -10
- package/api/src/routes/usage-records.ts +52 -18
- package/api/src/routes/webhook-attempts.ts +5 -5
- package/api/src/routes/webhook-endpoints.ts +5 -5
- package/api/src/store/migrations/20230905-genesis.ts +2 -2
- package/api/src/store/migrations/20230911-seeding.ts +4 -3
- package/api/src/store/models/checkout-session.ts +15 -7
- package/api/src/store/models/index.ts +31 -7
- package/api/src/store/models/invoice.ts +1 -1
- package/api/src/store/models/payment-intent.ts +2 -5
- package/api/src/store/models/payment-link.ts +1 -1
- package/api/src/store/models/payment-method.ts +54 -33
- package/api/src/store/models/price.ts +52 -17
- package/api/src/store/models/product.ts +0 -3
- package/api/src/store/models/subscription.ts +3 -5
- package/api/src/store/models/types.ts +56 -2
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +36 -29
- package/public/currencies/dai.png +0 -0
- package/public/currencies/dollar.png +0 -0
- package/public/currencies/usdc.png +0 -0
- package/public/currencies/usdt.png +0 -0
- package/public/methods/arcblock.png +0 -0
- package/public/methods/binance.png +0 -0
- package/public/methods/coinbase.png +0 -0
- package/public/methods/ethereum.jpg +0 -0
- package/public/methods/stripe.png +0 -0
- package/src/components/checkout/form/address.tsx +86 -10
- package/src/components/checkout/form/index.tsx +169 -83
- package/src/components/checkout/form/phone.tsx +96 -0
- package/src/components/checkout/form/stripe.tsx +195 -0
- package/src/components/checkout/pay.tsx +115 -34
- package/src/components/checkout/product-item.tsx +4 -3
- package/src/components/checkout/summary.tsx +5 -4
- package/src/components/drawer-form.tsx +4 -4
- package/src/components/input.tsx +22 -4
- package/src/components/invoice/table.tsx +8 -3
- package/src/components/payment-link/before-pay.tsx +11 -6
- package/src/components/payment-link/chrome.tsx +13 -0
- package/src/components/payment-link/preview.tsx +31 -0
- package/src/components/payment-link/product-select.tsx +8 -3
- package/src/components/payment-method/arcblock.tsx +53 -0
- package/src/components/payment-method/bitcoin.tsx +53 -0
- package/src/components/payment-method/ethereum.tsx +53 -0
- package/src/components/payment-method/form.tsx +54 -0
- package/src/components/payment-method/stripe.tsx +45 -0
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/list.tsx +1 -1
- package/src/components/price/currency-select.tsx +53 -0
- package/src/components/price/form.tsx +118 -24
- package/src/components/product/add-price.tsx +1 -1
- package/src/components/product/edit-price.tsx +6 -2
- package/src/components/subscription/items/index.tsx +7 -6
- package/src/components/subscription/items/usage-records.tsx +98 -0
- package/src/components/subscription/list.tsx +3 -2
- package/src/components/subscription/status.tsx +68 -0
- package/src/contexts/settings.tsx +2 -2
- package/src/env.d.ts +2 -0
- package/src/libs/util.ts +116 -21
- package/src/locales/en.tsx +71 -3
- package/src/pages/admin/billing/invoices/detail.tsx +5 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +6 -6
- package/src/pages/admin/customers/customers/detail.tsx +13 -1
- package/src/pages/admin/payments/intents/detail.tsx +8 -3
- package/src/pages/admin/payments/links/create.tsx +23 -3
- package/src/pages/admin/payments/links/detail.tsx +13 -26
- package/src/pages/admin/products/prices/detail.tsx +55 -11
- package/src/pages/admin/products/prices/list.tsx +7 -1
- package/src/pages/admin/products/products/create.tsx +1 -1
- package/src/pages/admin/products/products/detail.tsx +14 -7
- package/src/pages/admin/settings/index.tsx +16 -6
- package/src/pages/admin/settings/payment-methods/create.tsx +81 -0
- package/src/pages/admin/settings/{payment-methods.tsx → payment-methods/index.tsx} +9 -6
- package/src/pages/checkout/pay.tsx +3 -1
- package/src/pages/customer/index.tsx +12 -1
- 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
|
|
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 <
|
|
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,
|
|
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:
|
|
10
|
+
paymentMethods: TPaymentMethodExpanded[];
|
|
11
11
|
baseCurrency: TPaymentCurrency;
|
|
12
12
|
}
|
|
13
13
|
|
package/src/env.d.ts
CHANGED
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(
|
|
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(
|
|
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[]
|
|
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
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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.
|
|
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>
|