payment-kit 1.15.4 → 1.15.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/index.ts +3 -0
- package/api/src/integrations/blocklet/user.ts +31 -0
- package/api/src/libs/invoice.ts +41 -0
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +41 -20
- package/api/src/libs/notification/template/subscription-renew-failed.ts +19 -1
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +96 -11
- package/api/src/libs/notification/template/subscription-trial-start.ts +92 -18
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +46 -17
- package/api/src/libs/notification/template/subscription-will-renew.ts +38 -14
- package/api/src/libs/payment.ts +12 -0
- package/api/src/libs/util.ts +18 -1
- package/api/src/locales/en.ts +12 -3
- package/api/src/locales/zh.ts +12 -3
- package/api/src/queues/payment.ts +3 -1
- package/api/src/routes/checkout-sessions.ts +1 -1
- package/api/src/routes/donations.ts +1 -1
- package/api/src/routes/subscriptions.ts +30 -5
- package/api/src/routes/usage-records.ts +13 -4
- package/api/src/store/migrations/20240910-customer-sync.ts +21 -0
- package/api/src/store/models/customer.ts +5 -0
- package/blocklet.yml +1 -1
- package/package.json +9 -9
- package/scripts/sdk.js +25 -2
- package/src/components/filter-toolbar.tsx +1 -1
- package/src/components/payment-link/before-pay.tsx +41 -29
- package/src/components/pricing-table/product-settings.tsx +37 -25
- package/src/pages/admin/index.tsx +0 -1
- package/src/pages/admin/payments/intents/detail.tsx +14 -1
- package/src/pages/admin/payments/payouts/detail.tsx +6 -1
- package/src/pages/admin/products/pricing-tables/create.tsx +3 -0
- package/src/pages/checkout/pricing-table.tsx +26 -7
- package/src/pages/customer/index.tsx +3 -3
- package/src/pages/customer/invoice/past-due.tsx +14 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
-
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
|
6
6
|
import { useSearchParams } from 'react-router-dom';
|
|
7
7
|
|
|
@@ -29,6 +29,7 @@ export default function BeforePay({
|
|
|
29
29
|
const items = useFieldArray({ control, name: 'line_items' });
|
|
30
30
|
const includeFreeTrial = useWatch({ control, name: 'include_free_trial' });
|
|
31
31
|
const [state, setState] = useState({ creating: false });
|
|
32
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
33
|
|
|
33
34
|
useEffect(() => {
|
|
34
35
|
if (items.fields.length) {
|
|
@@ -56,6 +57,14 @@ export default function BeforePay({
|
|
|
56
57
|
setValue('invoice_creation.enabled', true);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
if (containerRef.current) {
|
|
62
|
+
containerRef.current.scrollTo({
|
|
63
|
+
top: containerRef.current.scrollHeight,
|
|
64
|
+
behavior: 'smooth',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}, 0);
|
|
59
68
|
}
|
|
60
69
|
};
|
|
61
70
|
|
|
@@ -87,35 +96,38 @@ export default function BeforePay({
|
|
|
87
96
|
{t('admin.paymentLink.products')} ({getValues().line_items.length})
|
|
88
97
|
</Typography>
|
|
89
98
|
<Stack spacing={2} sx={{ width: '100%' }}>
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
<Stack direction="column" sx={{ maxHeight: 500, width: '100%', overflowY: 'auto', gap: 2 }} ref={containerRef}>
|
|
100
|
+
{items.fields.map((item, index) => {
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
const product = getProductByPriceId(products, item.price_id);
|
|
103
|
+
if (!product) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<LineItem
|
|
109
|
+
key={item.id}
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
valid={isPriceAligned(items.fields, products, index).aligned}
|
|
112
|
+
prefix={`line_items.${index}`}
|
|
113
|
+
product={product}
|
|
114
|
+
onRemove={() => items.remove(index)}
|
|
115
|
+
onUpdate={refresh}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).recurring) && (
|
|
120
|
+
<Typography color="error" fontSize="small">
|
|
121
|
+
{t('admin.paymentLink.recurringNotAligned')}
|
|
122
|
+
</Typography>
|
|
123
|
+
)}
|
|
124
|
+
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).currency) && (
|
|
125
|
+
<Typography color="error" fontSize="small">
|
|
126
|
+
{t('admin.paymentLink.currencyNotAligned')}
|
|
127
|
+
</Typography>
|
|
128
|
+
)}
|
|
129
|
+
</Stack>
|
|
96
130
|
|
|
97
|
-
return (
|
|
98
|
-
<LineItem
|
|
99
|
-
key={item.id}
|
|
100
|
-
// @ts-ignore
|
|
101
|
-
valid={isPriceAligned(items.fields, products, index).aligned}
|
|
102
|
-
prefix={`line_items.${index}`}
|
|
103
|
-
product={product}
|
|
104
|
-
onRemove={() => items.remove(index)}
|
|
105
|
-
onUpdate={refresh}
|
|
106
|
-
/>
|
|
107
|
-
);
|
|
108
|
-
})}
|
|
109
|
-
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).recurring) && (
|
|
110
|
-
<Typography color="error" fontSize="small">
|
|
111
|
-
{t('admin.paymentLink.recurringNotAligned')}
|
|
112
|
-
</Typography>
|
|
113
|
-
)}
|
|
114
|
-
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).currency) && (
|
|
115
|
-
<Typography color="error" fontSize="small">
|
|
116
|
-
{t('admin.paymentLink.currencyNotAligned')}
|
|
117
|
-
</Typography>
|
|
118
|
-
)}
|
|
119
131
|
<ProductSelect
|
|
120
132
|
mode={items.fields.length ? 'waiting' : 'selecting'}
|
|
121
133
|
onSelect={onProductSelected}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { Checkbox, FormControlLabel, MenuItem, Select, Stack, Typography } from '@mui/material';
|
|
3
3
|
import { useSetState } from 'ahooks';
|
|
4
|
-
import { useEffect } from 'react';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
5
|
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
|
6
6
|
import { useSearchParams } from 'react-router-dom';
|
|
7
7
|
|
|
@@ -23,6 +23,7 @@ export default function PricingTableProductSettings({
|
|
|
23
23
|
const items = useFieldArray({ control, name: 'items' });
|
|
24
24
|
const [state, setState] = useSetState({ creating: false });
|
|
25
25
|
const highlight = useWatch({ control, name: 'highlight' });
|
|
26
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
26
27
|
|
|
27
28
|
useEffect(() => {
|
|
28
29
|
if (items.fields.length) {
|
|
@@ -83,6 +84,14 @@ export default function PricingTableProductSettings({
|
|
|
83
84
|
cross_sell_behavior: 'auto',
|
|
84
85
|
});
|
|
85
86
|
}
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (containerRef.current) {
|
|
89
|
+
containerRef.current.scrollTo({
|
|
90
|
+
top: containerRef.current.scrollHeight,
|
|
91
|
+
behavior: 'smooth',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}, 0);
|
|
86
95
|
}
|
|
87
96
|
};
|
|
88
97
|
|
|
@@ -115,31 +124,34 @@ export default function PricingTableProductSettings({
|
|
|
115
124
|
{t('admin.paymentLink.products')}
|
|
116
125
|
</Typography>
|
|
117
126
|
<Stack spacing={2} sx={{ width: '100%' }}>
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
<Stack direction="column" sx={{ maxHeight: 800, width: '100%', overflowY: 'auto', gap: 2 }} ref={containerRef}>
|
|
128
|
+
{grouped.map((item) => {
|
|
129
|
+
const [productId, prices] = item;
|
|
130
|
+
// @ts-ignore
|
|
131
|
+
const product = products.find((x) => x.id === productId);
|
|
132
|
+
if (!product) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ProductItem
|
|
138
|
+
key={productId}
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
valid={prices.every((x) => isPriceCurrencyAligned(items.fields, products, x.index))}
|
|
141
|
+
product={product}
|
|
142
|
+
prices={prices}
|
|
143
|
+
onUpdate={refresh}
|
|
144
|
+
onRemove={(i: number) => items.remove(i)}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
{items.fields.some((_, index) => !isPriceCurrencyAligned(items.fields as any[], products, index)) && (
|
|
149
|
+
<Typography color="error" fontSize="small">
|
|
150
|
+
{t('admin.paymentLink.currencyNotAligned')}
|
|
151
|
+
</Typography>
|
|
152
|
+
)}
|
|
153
|
+
</Stack>
|
|
125
154
|
|
|
126
|
-
return (
|
|
127
|
-
<ProductItem
|
|
128
|
-
key={productId}
|
|
129
|
-
// @ts-ignore
|
|
130
|
-
valid={prices.every((x) => isPriceCurrencyAligned(items.fields, products, x.index))}
|
|
131
|
-
product={product}
|
|
132
|
-
prices={prices}
|
|
133
|
-
onUpdate={refresh}
|
|
134
|
-
onRemove={(i: number) => items.remove(i)}
|
|
135
|
-
/>
|
|
136
|
-
);
|
|
137
|
-
})}
|
|
138
|
-
{items.fields.some((_, index) => !isPriceCurrencyAligned(items.fields as any[], products, index)) && (
|
|
139
|
-
<Typography color="error" fontSize="small">
|
|
140
|
-
{t('admin.paymentLink.currencyNotAligned')}
|
|
141
|
-
</Typography>
|
|
142
|
-
)}
|
|
143
155
|
<ProductSelect
|
|
144
156
|
mode={items.fields.length ? 'waiting' : 'selecting'}
|
|
145
157
|
onSelect={onProductSelected}
|
|
@@ -20,6 +20,7 @@ import { styled } from '@mui/system';
|
|
|
20
20
|
import { useRequest, useSetState } from 'ahooks';
|
|
21
21
|
import { Link } from 'react-router-dom';
|
|
22
22
|
|
|
23
|
+
import { startCase } from 'lodash';
|
|
23
24
|
import Copyable from '../../../../components/copyable';
|
|
24
25
|
import CustomerLink from '../../../../components/customer/link';
|
|
25
26
|
import EventList from '../../../../components/event/list';
|
|
@@ -237,7 +238,19 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
237
238
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
238
239
|
<Status label={data.status} color={getPaymentIntentStatusColor(data.status)} />
|
|
239
240
|
{data.last_payment_error && (
|
|
240
|
-
<Tooltip
|
|
241
|
+
<Tooltip
|
|
242
|
+
title={
|
|
243
|
+
<pre style={{ whiteSpace: 'break-spaces' }}>
|
|
244
|
+
{JSON.stringify(
|
|
245
|
+
{
|
|
246
|
+
...data.last_payment_error,
|
|
247
|
+
type: startCase(data.last_payment_error?.type),
|
|
248
|
+
},
|
|
249
|
+
null,
|
|
250
|
+
2
|
|
251
|
+
)}
|
|
252
|
+
</pre>
|
|
253
|
+
}>
|
|
241
254
|
<InfoOutlined fontSize="small" color="error" />
|
|
242
255
|
</Tooltip>
|
|
243
256
|
)}
|
|
@@ -228,7 +228,12 @@ export default function PayoutDetail(props: { id: string }) {
|
|
|
228
228
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
229
229
|
<Status label={data.status} color={getPayoutStatusColor(data.status)} />
|
|
230
230
|
{data.last_attempt_error && (
|
|
231
|
-
<Tooltip
|
|
231
|
+
<Tooltip
|
|
232
|
+
title={
|
|
233
|
+
<pre style={{ whiteSpace: 'break-spaces' }}>
|
|
234
|
+
{JSON.stringify(data.last_attempt_error, null, 2)}
|
|
235
|
+
</pre>
|
|
236
|
+
}>
|
|
232
237
|
<InfoOutlined fontSize="small" color="error" />
|
|
233
238
|
</Tooltip>
|
|
234
239
|
)}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { CheckoutTable } from '@blocklet/payment-react';
|
|
1
|
+
import { CheckoutTable, isMobileSafari } from '@blocklet/payment-react';
|
|
2
2
|
import Header from '@blocklet/ui-react/lib/Header';
|
|
3
|
-
import { Box } from '@mui/material';
|
|
3
|
+
import { Box, Stack } from '@mui/material';
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
6
|
id: string;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export default function PricingTablePage({ id }: Props) {
|
|
10
|
+
const isMobileSafariEnv = isMobileSafari();
|
|
10
11
|
return (
|
|
11
12
|
<Box
|
|
12
13
|
sx={{
|
|
13
14
|
width: '100vw',
|
|
15
|
+
maxHeight: '100vh',
|
|
14
16
|
minHeight: '90vh',
|
|
15
|
-
pb: 4,
|
|
16
17
|
display: 'flex',
|
|
17
18
|
flexDirection: 'column',
|
|
19
|
+
overflow: isMobileSafariEnv ? 'visible' : 'hidden',
|
|
18
20
|
}}>
|
|
19
21
|
<Header
|
|
20
22
|
meta={undefined}
|
|
@@ -24,17 +26,34 @@ export default function PricingTablePage({ id }: Props) {
|
|
|
24
26
|
theme={undefined}
|
|
25
27
|
hideNavMenu={undefined}
|
|
26
28
|
maxWidth={false}
|
|
29
|
+
sx={{ borderBottom: '1px solid var(--stroke-border-base, #EFF1F5)' }}
|
|
27
30
|
/>
|
|
28
31
|
|
|
29
|
-
<
|
|
32
|
+
<Stack
|
|
30
33
|
sx={{
|
|
31
34
|
pt: {
|
|
32
35
|
xs: 0,
|
|
33
|
-
|
|
36
|
+
},
|
|
37
|
+
overflow: {
|
|
38
|
+
xs: isMobileSafariEnv ? 'visible' : 'hidden',
|
|
39
|
+
md: 'hidden',
|
|
34
40
|
},
|
|
35
41
|
}}>
|
|
36
|
-
<CheckoutTable
|
|
37
|
-
|
|
42
|
+
<CheckoutTable
|
|
43
|
+
id={id}
|
|
44
|
+
mode="standalone"
|
|
45
|
+
theme={{
|
|
46
|
+
sx: {
|
|
47
|
+
overflow: {
|
|
48
|
+
xs: 'auto',
|
|
49
|
+
md: 'hidden',
|
|
50
|
+
},
|
|
51
|
+
display: 'flex',
|
|
52
|
+
flexDirection: 'column',
|
|
53
|
+
},
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
</Stack>
|
|
38
57
|
</Box>
|
|
39
58
|
);
|
|
40
59
|
}
|
|
@@ -391,7 +391,7 @@ export default function CustomerHome() {
|
|
|
391
391
|
/>
|
|
392
392
|
<InfoRow
|
|
393
393
|
label={t('admin.customer.address.city')}
|
|
394
|
-
value={<TruncatedText text={data.address?.city} maxLength={280} useWidth />}
|
|
394
|
+
value={data.address?.city && <TruncatedText text={data.address?.city} maxLength={280} useWidth />}
|
|
395
395
|
// value={data.address?.city}
|
|
396
396
|
sizes={[1, 1]}
|
|
397
397
|
alignItems="normal"
|
|
@@ -399,14 +399,14 @@ export default function CustomerHome() {
|
|
|
399
399
|
/>
|
|
400
400
|
<InfoRow
|
|
401
401
|
label={t('admin.customer.address.line1')}
|
|
402
|
-
value={<TruncatedText text={data.address?.line1} maxLength={280} useWidth />}
|
|
402
|
+
value={data.address?.line1 && <TruncatedText text={data.address?.line1} maxLength={280} useWidth />}
|
|
403
403
|
sizes={[1, 1]}
|
|
404
404
|
alignItems="normal"
|
|
405
405
|
direction="column"
|
|
406
406
|
/>
|
|
407
407
|
<InfoRow
|
|
408
408
|
label={t('admin.customer.address.line2')}
|
|
409
|
-
value={<TruncatedText text={data.address?.line2} maxLength={280} useWidth />}
|
|
409
|
+
value={data.address?.line2 && <TruncatedText text={data.address?.line2} maxLength={280} useWidth />}
|
|
410
410
|
sizes={[1, 1]}
|
|
411
411
|
alignItems="normal"
|
|
412
412
|
direction="column"
|
|
@@ -6,10 +6,11 @@ import { ArrowBackOutlined } from '@mui/icons-material';
|
|
|
6
6
|
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
7
7
|
import { styled } from '@mui/system';
|
|
8
8
|
import { useRequest } from 'ahooks';
|
|
9
|
-
import { useEffect } from 'react';
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
10
|
import { useSearchParams } from 'react-router-dom';
|
|
11
11
|
import { joinURL } from 'ufo';
|
|
12
12
|
|
|
13
|
+
import { isEmpty } from 'lodash';
|
|
13
14
|
import SectionHeader from '../../../components/section/header';
|
|
14
15
|
import { useSessionContext } from '../../../contexts/session';
|
|
15
16
|
import api from '../../../libs/api';
|
|
@@ -24,6 +25,7 @@ export default function CustomerInvoicePastDue() {
|
|
|
24
25
|
const { events } = useSessionContext();
|
|
25
26
|
const { connect } = usePaymentContext();
|
|
26
27
|
const [params] = useSearchParams();
|
|
28
|
+
const [alertVisible, setAlertVisible] = useState(true);
|
|
27
29
|
|
|
28
30
|
const { loading, error, data, runAsync } = useRequest(fetchData);
|
|
29
31
|
|
|
@@ -71,6 +73,14 @@ export default function CustomerInvoicePastDue() {
|
|
|
71
73
|
});
|
|
72
74
|
};
|
|
73
75
|
|
|
76
|
+
const onTableDataChange = (tableData: any) => {
|
|
77
|
+
if (isEmpty(tableData) || tableData?.count === 0) {
|
|
78
|
+
setAlertVisible(false);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
setAlertVisible(true);
|
|
82
|
+
};
|
|
83
|
+
|
|
74
84
|
return (
|
|
75
85
|
<Stack direction="column" spacing={3} sx={{ my: 2 }}>
|
|
76
86
|
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
|
@@ -86,7 +96,8 @@ export default function CustomerInvoicePastDue() {
|
|
|
86
96
|
</Stack>
|
|
87
97
|
</Stack>
|
|
88
98
|
<Root direction="column" spacing={3}>
|
|
89
|
-
<Alert severity="error">{t('payment.customer.pastDue.warning')}</Alert>
|
|
99
|
+
{alertVisible && <Alert severity="error">{t('payment.customer.pastDue.warning')}</Alert>}
|
|
100
|
+
|
|
90
101
|
<Box className="section">
|
|
91
102
|
<SectionHeader title={t('payment.customer.pastDue.invoices')} mb={0}>
|
|
92
103
|
{subscriptionId && currencyId && (
|
|
@@ -105,6 +116,7 @@ export default function CustomerInvoicePastDue() {
|
|
|
105
116
|
target="_blank"
|
|
106
117
|
action="pay"
|
|
107
118
|
type="table"
|
|
119
|
+
onTableDataChange={onTableDataChange}
|
|
108
120
|
/>
|
|
109
121
|
</Box>
|
|
110
122
|
</Box>
|