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,54 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
|
|
3
|
+
import { styled } from '@mui/system';
|
|
4
|
+
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
5
|
+
|
|
6
|
+
import ArcBlockMethodForm from './arcblock';
|
|
7
|
+
import BitcoinMethodForm from './bitcoin';
|
|
8
|
+
import EthereumMethodForm from './ethereum';
|
|
9
|
+
import StripeMethodForm from './stripe';
|
|
10
|
+
|
|
11
|
+
export default function PaymentMethodForm() {
|
|
12
|
+
const { t } = useLocaleContext();
|
|
13
|
+
const { control, setValue } = useFormContext();
|
|
14
|
+
|
|
15
|
+
const type = useWatch({ control, name: 'type' });
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Root direction="column" alignItems="flex-start" spacing={2}>
|
|
19
|
+
<Controller
|
|
20
|
+
name="type"
|
|
21
|
+
control={control}
|
|
22
|
+
render={({ field }) => (
|
|
23
|
+
<ToggleButtonGroup {...field} onChange={(_, value: string) => setValue(field.name, value)} exclusive>
|
|
24
|
+
<ToggleButton value="arcblock">ArcBlock</ToggleButton>
|
|
25
|
+
<ToggleButton value="stripe">Stripe</ToggleButton>
|
|
26
|
+
<ToggleButton value="ethereum" disabled>
|
|
27
|
+
Ethereum
|
|
28
|
+
</ToggleButton>
|
|
29
|
+
<ToggleButton value="bitcoin" disabled>
|
|
30
|
+
Bitcoin
|
|
31
|
+
</ToggleButton>
|
|
32
|
+
</ToggleButtonGroup>
|
|
33
|
+
)}
|
|
34
|
+
/>
|
|
35
|
+
<Typography variant="h6" sx={{ mb: 3, fontWeight: 600 }}>
|
|
36
|
+
{t('admin.paymentMethod.settings')}
|
|
37
|
+
</Typography>
|
|
38
|
+
{type === 'stripe' && <StripeMethodForm />}
|
|
39
|
+
{type === 'arcblock' && <ArcBlockMethodForm />}
|
|
40
|
+
{type === 'ethereum' && <EthereumMethodForm />}
|
|
41
|
+
{type === 'bitcoin' && <BitcoinMethodForm />}
|
|
42
|
+
</Root>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const Root = styled(Stack)`
|
|
47
|
+
select {
|
|
48
|
+
border: none;
|
|
49
|
+
&:active,
|
|
50
|
+
&:focus {
|
|
51
|
+
border: none;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* eslint-disable no-nested-ternary */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
|
|
4
|
+
import FormInput from '../input';
|
|
5
|
+
|
|
6
|
+
export default function StripeMethodForm() {
|
|
7
|
+
const { t } = useLocaleContext();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<FormInput
|
|
12
|
+
key="name"
|
|
13
|
+
name="name"
|
|
14
|
+
type="text"
|
|
15
|
+
rules={{ required: true }}
|
|
16
|
+
label={t('admin.paymentMethod.name.label')}
|
|
17
|
+
placeholder={t('admin.paymentMethod.name.tip')}
|
|
18
|
+
/>
|
|
19
|
+
<FormInput
|
|
20
|
+
key="description"
|
|
21
|
+
name="description"
|
|
22
|
+
type="text"
|
|
23
|
+
rules={{ required: true }}
|
|
24
|
+
label={t('admin.paymentMethod.description.label')}
|
|
25
|
+
placeholder={t('admin.paymentMethod.description.tip')}
|
|
26
|
+
/>
|
|
27
|
+
<FormInput
|
|
28
|
+
key="publishable_key"
|
|
29
|
+
name="settings.stripe.publishable_key"
|
|
30
|
+
type="text"
|
|
31
|
+
rules={{ required: true }}
|
|
32
|
+
label={t('admin.paymentMethod.stripe.publishable_key.label')}
|
|
33
|
+
placeholder={t('admin.paymentMethod.stripe.publishable_key.tip')}
|
|
34
|
+
/>
|
|
35
|
+
<FormInput
|
|
36
|
+
key="secret_key"
|
|
37
|
+
name="settings.stripe.secret_key"
|
|
38
|
+
type="password"
|
|
39
|
+
rules={{ required: true }}
|
|
40
|
+
label={t('admin.paymentMethod.stripe.secret_key.label')}
|
|
41
|
+
placeholder={t('admin.paymentMethod.stripe.secret_key.tip')}
|
|
42
|
+
/>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -43,7 +43,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
|
|
|
43
43
|
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TInvoiceExpanded>>(
|
|
44
44
|
(d) => {
|
|
45
45
|
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
46
|
-
return fetchData({ page,
|
|
46
|
+
return fetchData({ page, pageSize, status: 'open,paid', customer_id });
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
reloadDeps: [customer_id],
|
|
@@ -41,7 +41,7 @@ export function CurrentSubscriptionsInner({ id, onChange }: Props) {
|
|
|
41
41
|
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
|
|
42
42
|
(d) => {
|
|
43
43
|
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
44
|
-
return fetchData({ page,
|
|
44
|
+
return fetchData({ page, pageSize, status: 'active,trialing,paused', customer_id: id });
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
reloadDeps: [id],
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { AddOutlined } from '@mui/icons-material';
|
|
3
|
+
import { ListSubheader, MenuItem, Select, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import type { LiteralUnion } from 'type-fest';
|
|
6
|
+
|
|
7
|
+
import { useSettingsContext } from '../../contexts/settings';
|
|
8
|
+
import { getSupportedPaymentMethods } from '../../libs/util';
|
|
9
|
+
import Currency from '../currency';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
mode: LiteralUnion<'waiting' | 'selecting', string>;
|
|
13
|
+
hasSelected: (currency: any) => boolean;
|
|
14
|
+
onSelect: (currencyId: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default function CurrencySelect({ mode: initialMode, hasSelected, onSelect }: Props) {
|
|
18
|
+
const { t } = useLocaleContext();
|
|
19
|
+
const { settings } = useSettingsContext();
|
|
20
|
+
const [mode, setMode] = useState(initialMode);
|
|
21
|
+
|
|
22
|
+
const handleSelect = (e: any) => {
|
|
23
|
+
setMode('waiting');
|
|
24
|
+
onSelect(e.target.value);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (mode === 'selecting') {
|
|
28
|
+
return (
|
|
29
|
+
<Select value="" sx={{ width: 260 }} size="small" onChange={handleSelect}>
|
|
30
|
+
{getSupportedPaymentMethods(settings.paymentMethods, hasSelected).map((method) => [
|
|
31
|
+
<ListSubheader key={method.id} sx={{ fontSize: '1rem', color: 'text.secondary', lineHeight: '2.5rem' }}>
|
|
32
|
+
{method.name}
|
|
33
|
+
</ListSubheader>,
|
|
34
|
+
...method.payment_currencies.map((currency) => (
|
|
35
|
+
<MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
|
|
36
|
+
<Stack direction="row" justifyContent="space-between" sx={{ width: '100%' }}>
|
|
37
|
+
<Currency logo={currency.logo} name={currency.name} />
|
|
38
|
+
<Typography fontWeight="bold">{currency.symbol}</Typography>
|
|
39
|
+
</Stack>
|
|
40
|
+
</MenuItem>
|
|
41
|
+
)),
|
|
42
|
+
])}
|
|
43
|
+
</Select>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Stack sx={{ cursor: 'pointer' }} direction="row" alignItems="center" onClick={() => setMode('selecting')}>
|
|
49
|
+
<AddOutlined color="primary" />
|
|
50
|
+
<Typography color="primary">{t('admin.price.currency.add')}</Typography>
|
|
51
|
+
</Stack>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
-
import type { InferFormType, PriceRecurring, TPriceExpanded } from '@did-pay/types';
|
|
3
|
+
import type { InferFormType, PriceRecurring, TPaymentMethodExpanded, TPriceExpanded } from '@did-pay/types';
|
|
4
|
+
import { DeleteOutlineOutlined, InfoOutlined } from '@mui/icons-material';
|
|
4
5
|
import {
|
|
6
|
+
Alert,
|
|
5
7
|
Box,
|
|
6
8
|
Checkbox,
|
|
7
9
|
FormControlLabel,
|
|
8
10
|
FormLabel,
|
|
11
|
+
IconButton,
|
|
9
12
|
InputAdornment,
|
|
10
13
|
MenuItem,
|
|
11
14
|
Select,
|
|
@@ -13,36 +16,39 @@ import {
|
|
|
13
16
|
TextField,
|
|
14
17
|
ToggleButton,
|
|
15
18
|
ToggleButtonGroup,
|
|
19
|
+
Tooltip,
|
|
20
|
+
Typography,
|
|
16
21
|
} from '@mui/material';
|
|
17
22
|
import { styled } from '@mui/system';
|
|
18
|
-
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
23
|
+
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
|
19
24
|
import type { LiteralUnion } from 'type-fest';
|
|
20
25
|
|
|
21
26
|
import { useSettingsContext } from '../../contexts/settings';
|
|
27
|
+
import { findCurrency } from '../../libs/util';
|
|
22
28
|
import Collapse from '../collapse';
|
|
29
|
+
import CurrencySelect from './currency-select';
|
|
23
30
|
|
|
24
|
-
export type Price = Omit<InferFormType<TPriceExpanded>, 'product_id' | '
|
|
31
|
+
export type Price = Omit<InferFormType<TPriceExpanded>, 'product_id' | 'object'> & {
|
|
25
32
|
model: LiteralUnion<'standard' | 'package' | 'graduated' | 'volume' | 'custom', string>;
|
|
26
33
|
recurring: Omit<PriceRecurring, 'usage_type'> & {
|
|
27
34
|
interval_config: string;
|
|
28
|
-
metered: boolean;
|
|
29
35
|
};
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency'> = {
|
|
39
|
+
locked: false,
|
|
33
40
|
model: 'standard',
|
|
34
41
|
billing_scheme: '',
|
|
35
42
|
currency_id: '',
|
|
36
43
|
nickname: '',
|
|
37
|
-
type: '
|
|
44
|
+
type: 'recurring',
|
|
38
45
|
unit_amount: '0',
|
|
39
46
|
lookup_key: '',
|
|
40
47
|
recurring: {
|
|
41
48
|
interval_config: 'month_1',
|
|
42
49
|
interval: 'month',
|
|
43
50
|
interval_count: 1,
|
|
44
|
-
|
|
45
|
-
usage_type: '',
|
|
51
|
+
usage_type: 'licensed',
|
|
46
52
|
aggregate_usage: 'sum',
|
|
47
53
|
},
|
|
48
54
|
transform_quantity: {
|
|
@@ -51,6 +57,9 @@ export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency'> = {
|
|
|
51
57
|
},
|
|
52
58
|
tiers: [],
|
|
53
59
|
metadata: [],
|
|
60
|
+
custom_unit_amount: null,
|
|
61
|
+
currency_options: [],
|
|
62
|
+
tiers_mode: null,
|
|
54
63
|
};
|
|
55
64
|
|
|
56
65
|
type PriceFormProps = {
|
|
@@ -63,6 +72,12 @@ PriceForm.defaultProps = {
|
|
|
63
72
|
simple: false,
|
|
64
73
|
};
|
|
65
74
|
|
|
75
|
+
const INPUT_WIDTH = 260;
|
|
76
|
+
|
|
77
|
+
const hasMoreCurrency = (methods: TPaymentMethodExpanded[]) => {
|
|
78
|
+
return methods.every((method) => method.payment_currencies.length > 1) || methods.length > 1;
|
|
79
|
+
};
|
|
80
|
+
|
|
66
81
|
// FIXME: @wangshijun i18n
|
|
67
82
|
export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
68
83
|
const getFieldName = (name: string) => (prefix ? `${prefix}.${name}` : name);
|
|
@@ -71,19 +86,24 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
71
86
|
const { control, setValue, getFieldState } = useFormContext();
|
|
72
87
|
const { settings, livemode } = useSettingsContext();
|
|
73
88
|
|
|
89
|
+
const currencies = useFieldArray({ control, name: getFieldName('currency_options') });
|
|
90
|
+
|
|
91
|
+
const isLocked = useWatch({ control, name: getFieldName('locked') });
|
|
74
92
|
const isRecurring = useWatch({ control, name: getFieldName('type') }) === 'recurring';
|
|
75
|
-
const isMetered = useWatch({ control, name: getFieldName('recurring.
|
|
93
|
+
const isMetered = useWatch({ control, name: getFieldName('recurring.usage_type') }) === 'metered';
|
|
76
94
|
const isCustomInterval = useWatch({ control, name: getFieldName('recurring.interval_config') }) === 'month_2';
|
|
77
95
|
const model = useWatch({ control, name: getFieldName('model') });
|
|
78
96
|
|
|
79
97
|
return (
|
|
80
98
|
<Root direction="column" alignItems="flex-start" spacing={2}>
|
|
99
|
+
{isLocked && <Alert severity="info">{t('admin.price.locked')}</Alert>}
|
|
81
100
|
<Controller
|
|
82
|
-
rules={{ required: true }}
|
|
83
101
|
name={getFieldName('model')}
|
|
84
102
|
control={control}
|
|
103
|
+
rules={{ required: true }}
|
|
104
|
+
disabled={isLocked}
|
|
85
105
|
render={({ field }) => (
|
|
86
|
-
<Box sx={{ width:
|
|
106
|
+
<Box sx={{ width: INPUT_WIDTH }}>
|
|
87
107
|
<FormLabel>{t('admin.price.model')}</FormLabel>
|
|
88
108
|
<Select {...field} fullWidth size="small">
|
|
89
109
|
<MenuItem value="standard">Standard Pricing</MenuItem>
|
|
@@ -106,14 +126,24 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
106
126
|
name={getFieldName('unit_amount')}
|
|
107
127
|
control={control}
|
|
108
128
|
rules={{ required: t('admin.price.unit_amount.required'), min: t('admin.price.unit_amount.positive') }}
|
|
129
|
+
disabled={isLocked}
|
|
109
130
|
render={({ field }) => (
|
|
110
131
|
<Box>
|
|
111
|
-
<FormLabel>
|
|
132
|
+
<FormLabel>
|
|
133
|
+
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
134
|
+
<Typography component="span" color="text.primary">
|
|
135
|
+
{t('admin.price.amount')}
|
|
136
|
+
</Typography>
|
|
137
|
+
<Tooltip title={t('admin.price.amountTip')} placement="top" arrow>
|
|
138
|
+
<InfoOutlined fontSize="small" sx={{ color: 'text.secondary' }} />
|
|
139
|
+
</Tooltip>
|
|
140
|
+
</Stack>
|
|
141
|
+
</FormLabel>
|
|
112
142
|
<TextField
|
|
113
143
|
{...field}
|
|
114
144
|
type="number"
|
|
115
145
|
size="small"
|
|
116
|
-
sx={{ width:
|
|
146
|
+
sx={{ width: INPUT_WIDTH }}
|
|
117
147
|
error={!!getFieldState(getFieldName('unit_amount')).error}
|
|
118
148
|
helperText={getFieldState(getFieldName('unit_amount')).error?.message}
|
|
119
149
|
InputProps={{
|
|
@@ -127,6 +157,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
127
157
|
<Controller
|
|
128
158
|
name={getFieldName('transform_quantity.divide_by')}
|
|
129
159
|
control={control}
|
|
160
|
+
disabled={isLocked}
|
|
130
161
|
render={({ field }) => (
|
|
131
162
|
<Box ml={2}>
|
|
132
163
|
<FormLabel> </FormLabel>
|
|
@@ -134,7 +165,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
134
165
|
{...field}
|
|
135
166
|
type="number"
|
|
136
167
|
size="small"
|
|
137
|
-
sx={{ width:
|
|
168
|
+
sx={{ width: INPUT_WIDTH }}
|
|
138
169
|
InputProps={{
|
|
139
170
|
startAdornment: <InputAdornment position="start">{t('common.per')}</InputAdornment>,
|
|
140
171
|
endAdornment: <InputAdornment position="end">{t('common.unit')}</InputAdornment>,
|
|
@@ -145,13 +176,64 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
145
176
|
/>
|
|
146
177
|
)}
|
|
147
178
|
</Stack>
|
|
179
|
+
{hasMoreCurrency(settings.paymentMethods) && (
|
|
180
|
+
<Stack direction="column" spacing={2}>
|
|
181
|
+
{currencies.fields.map((item: any, index: number) => {
|
|
182
|
+
if (item.currency_id === settings.baseCurrency.id) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const fieldName = getFieldName(`currency_options.${index}.unit_amount`);
|
|
186
|
+
return (
|
|
187
|
+
<Stack key={item.currency_id} direction="row" alignItems="center" spacing={1}>
|
|
188
|
+
<Controller
|
|
189
|
+
name={fieldName}
|
|
190
|
+
control={control}
|
|
191
|
+
rules={{ required: t('admin.price.unit_amount.required') }}
|
|
192
|
+
disabled={isLocked}
|
|
193
|
+
render={({ field }) => (
|
|
194
|
+
<TextField
|
|
195
|
+
{...field}
|
|
196
|
+
type="number"
|
|
197
|
+
size="small"
|
|
198
|
+
sx={{ width: INPUT_WIDTH }}
|
|
199
|
+
error={!!getFieldState(fieldName).error}
|
|
200
|
+
helperText={getFieldState(fieldName).error?.message}
|
|
201
|
+
InputProps={{
|
|
202
|
+
endAdornment: (
|
|
203
|
+
<InputAdornment position="end">
|
|
204
|
+
{findCurrency(settings.paymentMethods, item.currency_id)?.symbol}
|
|
205
|
+
</InputAdornment>
|
|
206
|
+
),
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
/>
|
|
211
|
+
<IconButton size="small" disabled={isLocked} onClick={() => currencies.remove(index)}>
|
|
212
|
+
<DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
|
|
213
|
+
</IconButton>
|
|
214
|
+
</Stack>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
{isLocked === false && (
|
|
218
|
+
<CurrencySelect
|
|
219
|
+
mode="waiting"
|
|
220
|
+
hasSelected={(currency) =>
|
|
221
|
+
currencies.fields.some((x: any) => x.currency_id === currency.id) ||
|
|
222
|
+
currency.id === settings.baseCurrency.id
|
|
223
|
+
}
|
|
224
|
+
onSelect={(currencyId) => currencies.append({ currency_id: currencyId, unit_amount: 0 })}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</Stack>
|
|
228
|
+
)}
|
|
148
229
|
<Controller
|
|
149
230
|
name={getFieldName('type')}
|
|
150
231
|
control={control}
|
|
232
|
+
disabled={isLocked}
|
|
151
233
|
render={({ field }) => (
|
|
152
234
|
<ToggleButtonGroup {...field} onChange={(_, value: string) => setValue(field.name, value)} exclusive>
|
|
153
|
-
<ToggleButton value="one_time">One Time</ToggleButton>
|
|
154
235
|
<ToggleButton value="recurring">Recurring</ToggleButton>
|
|
236
|
+
<ToggleButton value="one_time">One Time</ToggleButton>
|
|
155
237
|
</ToggleButtonGroup>
|
|
156
238
|
)}
|
|
157
239
|
/>
|
|
@@ -160,6 +242,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
160
242
|
<Controller
|
|
161
243
|
name={getFieldName('recurring.interval_config')}
|
|
162
244
|
control={control}
|
|
245
|
+
disabled={isLocked}
|
|
163
246
|
render={({ field }) => (
|
|
164
247
|
<Box>
|
|
165
248
|
<FormLabel>{t('admin.price.recurring.interval')}</FormLabel>
|
|
@@ -168,10 +251,10 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
168
251
|
onChange={(e) => {
|
|
169
252
|
const [interval, count] = e.target.value.split('_');
|
|
170
253
|
setValue(getFieldName('recurring.interval'), interval);
|
|
171
|
-
setValue(getFieldName('recurring.interval_count'), count);
|
|
254
|
+
setValue(getFieldName('recurring.interval_count'), +count);
|
|
172
255
|
setValue(getFieldName('recurring.interval_config'), e.target.value);
|
|
173
256
|
}}
|
|
174
|
-
sx={{ width:
|
|
257
|
+
sx={{ width: INPUT_WIDTH }}
|
|
175
258
|
size="small">
|
|
176
259
|
{!livemode && <MenuItem value="hour_1">Hourly</MenuItem>}
|
|
177
260
|
<MenuItem value="day_1">Daily</MenuItem>
|
|
@@ -189,6 +272,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
189
272
|
<Controller
|
|
190
273
|
name={getFieldName('recurring.interval_count')}
|
|
191
274
|
control={control}
|
|
275
|
+
disabled={isLocked}
|
|
192
276
|
render={({ field }) => (
|
|
193
277
|
<Box ml={2}>
|
|
194
278
|
<FormLabel> </FormLabel>
|
|
@@ -196,7 +280,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
196
280
|
{...field}
|
|
197
281
|
type="number"
|
|
198
282
|
size="small"
|
|
199
|
-
sx={{ width:
|
|
283
|
+
sx={{ width: INPUT_WIDTH }}
|
|
200
284
|
InputProps={{
|
|
201
285
|
startAdornment: <InputAdornment position="start">{t('common.every')}</InputAdornment>,
|
|
202
286
|
endAdornment: (
|
|
@@ -218,18 +302,27 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
218
302
|
)}
|
|
219
303
|
{isRecurring && (
|
|
220
304
|
<Controller
|
|
221
|
-
name={getFieldName('recurring.
|
|
305
|
+
name={getFieldName('recurring.usage_type')}
|
|
222
306
|
control={control}
|
|
307
|
+
disabled={isLocked}
|
|
223
308
|
render={({ field }) => (
|
|
224
309
|
<FormControlLabel
|
|
310
|
+
sx={{ alignItems: 'flex-start' }}
|
|
225
311
|
control={
|
|
226
312
|
<Checkbox
|
|
227
313
|
checked={isMetered}
|
|
228
314
|
{...field}
|
|
229
|
-
onChange={(_, checked: boolean) => setValue(field.name, checked)}
|
|
315
|
+
onChange={(_, checked: boolean) => setValue(field.name, checked ? 'metered' : 'licensed')}
|
|
230
316
|
/>
|
|
231
317
|
}
|
|
232
|
-
label={
|
|
318
|
+
label={
|
|
319
|
+
<Stack>
|
|
320
|
+
<Typography color="text.primary">{t('admin.price.recurring.metered')}</Typography>
|
|
321
|
+
<Typography color="text.secondary" sx={{ maxWidth: '80%' }}>
|
|
322
|
+
{t('admin.price.recurring.meteredTip')}
|
|
323
|
+
</Typography>
|
|
324
|
+
</Stack>
|
|
325
|
+
}
|
|
233
326
|
/>
|
|
234
327
|
)}
|
|
235
328
|
/>
|
|
@@ -238,10 +331,11 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
238
331
|
<Controller
|
|
239
332
|
name={getFieldName('recurring.aggregate_usage')}
|
|
240
333
|
control={control}
|
|
334
|
+
disabled={isLocked}
|
|
241
335
|
render={({ field }) => (
|
|
242
336
|
<Box>
|
|
243
337
|
<FormLabel>{t('admin.price.recurring.aggregate')}</FormLabel>
|
|
244
|
-
<Select {...field} sx={{ width:
|
|
338
|
+
<Select {...field} sx={{ width: INPUT_WIDTH }} size="small">
|
|
245
339
|
<MenuItem value="sum">Sum of usage values during period</MenuItem>
|
|
246
340
|
<MenuItem value="max">Maximum usage value during period</MenuItem>
|
|
247
341
|
<MenuItem value="last_ever">Most recent usage value</MenuItem>
|
|
@@ -252,7 +346,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
252
346
|
/>
|
|
253
347
|
)}
|
|
254
348
|
{!simple && (
|
|
255
|
-
<Collapse trigger={t('admin.price.additional')}>
|
|
349
|
+
<Collapse trigger={t('admin.price.additional')} expanded={isLocked}>
|
|
256
350
|
<Stack spacing={2} alignItems="flex-start">
|
|
257
351
|
<Controller
|
|
258
352
|
name={getFieldName('nickname')}
|
|
@@ -260,7 +354,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
260
354
|
render={({ field }) => (
|
|
261
355
|
<Box>
|
|
262
356
|
<FormLabel>{t('admin.price.nickname.label')}</FormLabel>
|
|
263
|
-
<TextField {...field} size="small" />
|
|
357
|
+
<TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} />
|
|
264
358
|
</Box>
|
|
265
359
|
)}
|
|
266
360
|
/>
|
|
@@ -270,7 +364,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
270
364
|
render={({ field }) => (
|
|
271
365
|
<Box>
|
|
272
366
|
<FormLabel>{t('admin.price.lookup_key.label')}</FormLabel>
|
|
273
|
-
<TextField {...field} size="small" />
|
|
367
|
+
<TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} />
|
|
274
368
|
</Box>
|
|
275
369
|
)}
|
|
276
370
|
/>
|
|
@@ -3,7 +3,7 @@ import Dialog from '@arcblock/ux/lib/Dialog';
|
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import { Button, CircularProgress, Stack } from '@mui/material';
|
|
5
5
|
import { fromUnitToToken } from '@ocap/util';
|
|
6
|
-
import { isEmpty } from 'lodash';
|
|
6
|
+
import { cloneDeep, isEmpty } from 'lodash';
|
|
7
7
|
import type { EventHandler } from 'react';
|
|
8
8
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
9
9
|
|
|
@@ -26,6 +26,7 @@ export default function EditPrice({
|
|
|
26
26
|
defaultValues: {
|
|
27
27
|
...price,
|
|
28
28
|
unit_amount: fromUnitToToken(price.unit_amount, price.currency.decimal),
|
|
29
|
+
// @ts-ignore
|
|
29
30
|
model: getPricingModel(price as any),
|
|
30
31
|
metadata: isEmpty(price.metadata)
|
|
31
32
|
? []
|
|
@@ -34,10 +35,13 @@ export default function EditPrice({
|
|
|
34
35
|
recurring: price.recurring
|
|
35
36
|
? {
|
|
36
37
|
...price.recurring,
|
|
37
|
-
metered: price.recurring.usage_type === 'metered',
|
|
38
38
|
interval_config: [price.recurring.interval, price.recurring.interval_count].join('_'),
|
|
39
39
|
}
|
|
40
40
|
: DEFAULT_PRICE.recurring,
|
|
41
|
+
currency_options: cloneDeep(price.currency_options).map((x: any) => {
|
|
42
|
+
x.unit_amount = fromUnitToToken(x.unit_amount, x.currency.decimal);
|
|
43
|
+
return x;
|
|
44
|
+
}),
|
|
41
45
|
},
|
|
42
46
|
});
|
|
43
47
|
|
|
@@ -7,6 +7,7 @@ import { formatPrice } from '../../../libs/util';
|
|
|
7
7
|
import Copyable from '../../copyable';
|
|
8
8
|
import Table from '../../table';
|
|
9
9
|
import LineItemActions from './actions';
|
|
10
|
+
import UsageRecords from './usage-records';
|
|
10
11
|
|
|
11
12
|
type ListProps = {
|
|
12
13
|
data: TSubscriptionItemExpanded[];
|
|
@@ -44,7 +45,7 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
|
|
|
44
45
|
options: {
|
|
45
46
|
customBodyRenderLite: (_: string, index: number) => {
|
|
46
47
|
const item = data[index] as TSubscriptionItemExpanded;
|
|
47
|
-
return <Copyable text={item
|
|
48
|
+
return <Copyable text={item.id} />;
|
|
48
49
|
},
|
|
49
50
|
},
|
|
50
51
|
},
|
|
@@ -53,8 +54,8 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
|
|
|
53
54
|
name: 'quantity',
|
|
54
55
|
options: {
|
|
55
56
|
customBodyRenderLite: (_: string, index: number) => {
|
|
56
|
-
const item = data[index];
|
|
57
|
-
return item?.quantity;
|
|
57
|
+
const item = data[index] as TSubscriptionItemExpanded;
|
|
58
|
+
return item.price.recurring?.usage_type === 'metered' ? <UsageRecords id={item.id} /> : item.quantity;
|
|
58
59
|
},
|
|
59
60
|
},
|
|
60
61
|
},
|
|
@@ -66,9 +67,9 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
|
|
|
66
67
|
sort: false,
|
|
67
68
|
customBodyRenderLite: (_: string, index: number) => {
|
|
68
69
|
const item = data[index] as TSubscriptionItemExpanded;
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
return item.price.recurring?.usage_type === 'metered'
|
|
71
|
+
? t('admin.subscription.usage.vary')
|
|
72
|
+
: formatPrice(item.price, currency, item?.price.product.unit_label, item?.quantity);
|
|
72
73
|
},
|
|
73
74
|
},
|
|
74
75
|
},
|