payment-kit 1.21.15 → 1.21.17
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 +2 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +30 -25
- package/api/src/integrations/stripe/handlers/setup-intent.ts +231 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +31 -9
- package/api/src/integrations/stripe/resource.ts +29 -0
- package/api/src/libs/payment.ts +9 -3
- package/api/src/libs/util.ts +17 -0
- package/api/src/queues/vendors/return-processor.ts +52 -75
- package/api/src/queues/vendors/return-scanner.ts +38 -3
- package/api/src/routes/connect/change-payer.ts +148 -0
- package/api/src/routes/connect/shared.ts +30 -0
- package/api/src/routes/invoices.ts +141 -2
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/subscriptions.ts +130 -3
- package/api/src/routes/vendor.ts +100 -72
- package/api/src/store/models/checkout-session.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/components/invoice-pdf/template.tsx +30 -0
- package/src/components/subscription/payment-method-info.tsx +222 -0
- package/src/global.css +4 -0
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/admin/billing/invoices/detail.tsx +5 -3
- package/src/pages/admin/billing/subscriptions/detail.tsx +16 -0
- package/src/pages/admin/overview.tsx +14 -14
- package/src/pages/admin/products/vendors/create.tsx +6 -40
- package/src/pages/admin/products/vendors/index.tsx +5 -1
- package/src/pages/customer/invoice/detail.tsx +59 -17
- package/src/pages/customer/subscription/detail.tsx +20 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Button } from '@arcblock/ux';
|
|
2
|
+
import DID from '@arcblock/ux/lib/DID';
|
|
3
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
+
import { StripeForm, api, formatError, usePaymentContext } from '@blocklet/payment-react';
|
|
6
|
+
import type { TCustomer } from '@blocklet/payment-types';
|
|
7
|
+
import { CreditCard, Email, AccountBalance } from '@mui/icons-material';
|
|
8
|
+
import { Box, Stack, Typography } from '@mui/material';
|
|
9
|
+
import { useSetState } from 'ahooks';
|
|
10
|
+
import { useEffect } from 'react';
|
|
11
|
+
|
|
12
|
+
interface PaymentMethodData {
|
|
13
|
+
id?: string;
|
|
14
|
+
type: string;
|
|
15
|
+
// Stripe payment methods
|
|
16
|
+
card?: {
|
|
17
|
+
brand: string;
|
|
18
|
+
last4: string;
|
|
19
|
+
exp_month: number;
|
|
20
|
+
exp_year: number;
|
|
21
|
+
};
|
|
22
|
+
link?: {
|
|
23
|
+
email: string;
|
|
24
|
+
};
|
|
25
|
+
us_bank_account?: {
|
|
26
|
+
account_type: string;
|
|
27
|
+
bank_name: string;
|
|
28
|
+
last4: string;
|
|
29
|
+
};
|
|
30
|
+
billing_details?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
phone?: string;
|
|
34
|
+
};
|
|
35
|
+
// On-chain payment methods (arcblock/ethereum/base)
|
|
36
|
+
payer?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
subscriptionId: string;
|
|
41
|
+
customer: TCustomer;
|
|
42
|
+
paymentMethodDetails: PaymentMethodData | null;
|
|
43
|
+
editable?: boolean;
|
|
44
|
+
paymentMethodType: string;
|
|
45
|
+
onUpdate?: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function PaymentMethodInfo({
|
|
49
|
+
subscriptionId,
|
|
50
|
+
customer,
|
|
51
|
+
paymentMethodDetails,
|
|
52
|
+
editable = false,
|
|
53
|
+
onUpdate = () => {},
|
|
54
|
+
paymentMethodType,
|
|
55
|
+
}: Props) {
|
|
56
|
+
const { t, locale } = useLocaleContext();
|
|
57
|
+
const { connect } = usePaymentContext();
|
|
58
|
+
|
|
59
|
+
const [state, setState] = useSetState<{
|
|
60
|
+
editing: boolean;
|
|
61
|
+
submitting: boolean;
|
|
62
|
+
setupIntentId: string | null;
|
|
63
|
+
clientSecret: string | null;
|
|
64
|
+
publishableKey: string | null;
|
|
65
|
+
}>({
|
|
66
|
+
editing: false,
|
|
67
|
+
submitting: false,
|
|
68
|
+
setupIntentId: null,
|
|
69
|
+
clientSecret: null,
|
|
70
|
+
publishableKey: null,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!state.editing) {
|
|
75
|
+
setState({ clientSecret: null, publishableKey: null, setupIntentId: null });
|
|
76
|
+
}
|
|
77
|
+
}, [state.editing, setState]);
|
|
78
|
+
|
|
79
|
+
const handleEdit = async () => {
|
|
80
|
+
if (paymentMethodType === 'stripe') {
|
|
81
|
+
try {
|
|
82
|
+
setState({ submitting: true });
|
|
83
|
+
const { data } = await api.post(`/api/subscriptions/${subscriptionId}/update-stripe-payment-method`);
|
|
84
|
+
setState({
|
|
85
|
+
editing: true,
|
|
86
|
+
clientSecret: data.client_secret,
|
|
87
|
+
publishableKey: data.publishable_key,
|
|
88
|
+
setupIntentId: data.setup_intent_id,
|
|
89
|
+
submitting: false,
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
Toast.error(formatError(err));
|
|
93
|
+
setState({ submitting: false });
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
connect.open({
|
|
97
|
+
action: 'change-payer',
|
|
98
|
+
saveConnect: false,
|
|
99
|
+
locale: locale as 'en' | 'zh',
|
|
100
|
+
useSocket: true,
|
|
101
|
+
messages: {
|
|
102
|
+
scan: '',
|
|
103
|
+
title: t('admin.subscription.changePayer.connect.title'),
|
|
104
|
+
success: t('admin.subscription.changePayer.connect.success'),
|
|
105
|
+
error: t('admin.subscription.changePayer.connect.error'),
|
|
106
|
+
confirm: '',
|
|
107
|
+
} as any,
|
|
108
|
+
extraParams: { subscriptionId },
|
|
109
|
+
onSuccess: () => {
|
|
110
|
+
connect.close();
|
|
111
|
+
Toast.success(t('admin.subscription.changePayer.connect.success'));
|
|
112
|
+
onUpdate?.();
|
|
113
|
+
},
|
|
114
|
+
onClose: () => {
|
|
115
|
+
connect.close();
|
|
116
|
+
},
|
|
117
|
+
onError: (err: any) => {
|
|
118
|
+
Toast.error(formatError(err));
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleConfirm = () => {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
Toast.success(t('admin.subscription.changePayer.connect.success'));
|
|
127
|
+
setState({ editing: false, submitting: false });
|
|
128
|
+
onUpdate?.();
|
|
129
|
+
}, 2000);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleCancel = () => {
|
|
133
|
+
setState({ editing: false, clientSecret: null, publishableKey: null, setupIntentId: null });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (!paymentMethodDetails) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { card, link, us_bank_account: usBankAccount, payer, type } = paymentMethodDetails;
|
|
141
|
+
|
|
142
|
+
if (state.editing && state.clientSecret && state.publishableKey) {
|
|
143
|
+
return (
|
|
144
|
+
<Box>
|
|
145
|
+
<StripeForm
|
|
146
|
+
clientSecret={state.clientSecret}
|
|
147
|
+
intentType="setup_intent"
|
|
148
|
+
publicKey={state.publishableKey}
|
|
149
|
+
customer={customer}
|
|
150
|
+
mode="setup"
|
|
151
|
+
onConfirm={handleConfirm}
|
|
152
|
+
onCancel={handleCancel}
|
|
153
|
+
returnUrl={window.location.href}
|
|
154
|
+
/>
|
|
155
|
+
</Box>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const renderPaymentMethodInfo = () => {
|
|
160
|
+
if (type === 'card' && card) {
|
|
161
|
+
return (
|
|
162
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
163
|
+
<CreditCard sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
164
|
+
<Typography variant="body2">
|
|
165
|
+
{card.brand.toUpperCase()} •••• {card.last4}
|
|
166
|
+
</Typography>
|
|
167
|
+
</Stack>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (type === 'link' && link) {
|
|
172
|
+
return (
|
|
173
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
174
|
+
<Email sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
175
|
+
<Typography variant="body2">
|
|
176
|
+
{t('admin.subscription.changePayer.stripe.linkType')} ({link.email})
|
|
177
|
+
</Typography>
|
|
178
|
+
</Stack>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (type === 'us_bank_account' && usBankAccount) {
|
|
183
|
+
return (
|
|
184
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
185
|
+
<AccountBalance sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
186
|
+
<Typography variant="body2">
|
|
187
|
+
{usBankAccount.bank_name || t('admin.subscription.changePayer.stripe.bankAccount')} ••••{' '}
|
|
188
|
+
{usBankAccount.last4}
|
|
189
|
+
</Typography>
|
|
190
|
+
</Stack>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (['arcblock', 'ethereum', 'base'].includes(type) && payer) {
|
|
195
|
+
return <DID did={payer} responsive={false} compact copyable={false} />;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
200
|
+
-
|
|
201
|
+
</Typography>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flex: 1 }}>
|
|
207
|
+
{renderPaymentMethodInfo()}
|
|
208
|
+
{editable && (
|
|
209
|
+
<Button
|
|
210
|
+
variant="text"
|
|
211
|
+
size="small"
|
|
212
|
+
sx={{
|
|
213
|
+
color: 'text.link',
|
|
214
|
+
}}
|
|
215
|
+
loading={state.submitting}
|
|
216
|
+
onClick={handleEdit}>
|
|
217
|
+
{t('admin.subscription.changePayer.btn')}
|
|
218
|
+
</Button>
|
|
219
|
+
)}
|
|
220
|
+
</Stack>
|
|
221
|
+
);
|
|
222
|
+
}
|
package/src/global.css
CHANGED
package/src/locales/en.tsx
CHANGED
|
@@ -1421,6 +1421,19 @@ export default flat({
|
|
|
1421
1421
|
batchPay: {
|
|
1422
1422
|
button: 'Pay due invoices',
|
|
1423
1423
|
},
|
|
1424
|
+
payerAddress: 'Payment Address',
|
|
1425
|
+
changePayer: {
|
|
1426
|
+
btn: 'Change',
|
|
1427
|
+
stripe: {
|
|
1428
|
+
linkType: 'Stripe Link',
|
|
1429
|
+
bankAccount: 'Bank Account',
|
|
1430
|
+
},
|
|
1431
|
+
connect: {
|
|
1432
|
+
title: 'Change Payment Address',
|
|
1433
|
+
success: 'Payment Address changed successfully',
|
|
1434
|
+
error: 'Failed to change payment Address',
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1424
1437
|
},
|
|
1425
1438
|
customer: {
|
|
1426
1439
|
view: 'View customer',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -1387,6 +1387,19 @@ export default flat({
|
|
|
1387
1387
|
batchPay: {
|
|
1388
1388
|
button: '批量付款',
|
|
1389
1389
|
},
|
|
1390
|
+
payerAddress: '扣费地址',
|
|
1391
|
+
changePayer: {
|
|
1392
|
+
btn: '变更',
|
|
1393
|
+
stripe: {
|
|
1394
|
+
linkType: 'Stripe Link',
|
|
1395
|
+
bankAccount: '银行账户',
|
|
1396
|
+
},
|
|
1397
|
+
connect: {
|
|
1398
|
+
title: '变更扣费地址',
|
|
1399
|
+
success: '扣费地址变更成功',
|
|
1400
|
+
error: '扣费地址变更失败',
|
|
1401
|
+
},
|
|
1402
|
+
},
|
|
1390
1403
|
},
|
|
1391
1404
|
customer: {
|
|
1392
1405
|
view: '查看客户',
|
|
@@ -55,7 +55,7 @@ const fetchData = (
|
|
|
55
55
|
relatedCreditGrants?: TCreditGrantExpanded[];
|
|
56
56
|
}
|
|
57
57
|
> => {
|
|
58
|
-
return api.get(`/api/invoices/${id}`).then((res) => res.data);
|
|
58
|
+
return api.get(`/api/invoices/${id}?sync=true`).then((res: any) => res.data);
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const InfoDirection = 'column';
|
|
@@ -92,7 +92,7 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
92
92
|
const createUpdater = (key: string) => async (updates: TInvoice) => {
|
|
93
93
|
try {
|
|
94
94
|
setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
|
|
95
|
-
await api.put(`/api/invoices/${props.id}`, updates).then((res) => res.data);
|
|
95
|
+
await api.put(`/api/invoices/${props.id}`, updates).then((res: any) => res.data);
|
|
96
96
|
Toast.success(t('common.saved'));
|
|
97
97
|
runAsync();
|
|
98
98
|
} catch (err) {
|
|
@@ -120,6 +120,8 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
120
120
|
};
|
|
121
121
|
// @ts-ignore
|
|
122
122
|
const isDonation = data?.checkoutSession?.submit_type === 'donate';
|
|
123
|
+
const showDownloadButton =
|
|
124
|
+
['open', 'paid', 'uncollectible'].includes(data.status) && !isDonation && !isEmpty(data.lines);
|
|
123
125
|
return (
|
|
124
126
|
<Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
|
|
125
127
|
<Box>
|
|
@@ -144,7 +146,7 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
144
146
|
</Typography>
|
|
145
147
|
</Stack>
|
|
146
148
|
<Box style={{ display: 'flex', gap: '10px' }}>
|
|
147
|
-
<Download data={data} />
|
|
149
|
+
{showDownloadButton && <Download data={data} />}
|
|
148
150
|
<InvoiceActions data={data} onChange={runAsync} variant="normal" />
|
|
149
151
|
</Box>
|
|
150
152
|
</Stack>
|
|
@@ -35,6 +35,7 @@ import { goBackOrFallback } from '../../../../libs/util';
|
|
|
35
35
|
import InfoRowGroup from '../../../../components/info-row-group';
|
|
36
36
|
import VendorServiceList from '../../../../components/subscription/vendor-service-list';
|
|
37
37
|
import { useSessionContext } from '../../../../contexts/session';
|
|
38
|
+
import PaymentMethodInfo from '../../../../components/subscription/payment-method-info';
|
|
38
39
|
|
|
39
40
|
const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
|
|
40
41
|
return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
|
|
@@ -285,6 +286,21 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
285
286
|
/>
|
|
286
287
|
}
|
|
287
288
|
/>
|
|
289
|
+
|
|
290
|
+
{(data as any).paymentMethodDetails && (
|
|
291
|
+
<InfoRow
|
|
292
|
+
label={t('admin.subscription.payerAddress')}
|
|
293
|
+
value={
|
|
294
|
+
<PaymentMethodInfo
|
|
295
|
+
subscriptionId={data.id}
|
|
296
|
+
customer={data.customer}
|
|
297
|
+
paymentMethodDetails={(data as any).paymentMethodDetails}
|
|
298
|
+
editable={false}
|
|
299
|
+
paymentMethodType={data.paymentMethod?.type}
|
|
300
|
+
/>
|
|
301
|
+
}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
288
304
|
{data.payment_details && hasDelegateTxHash(data.payment_details, data.paymentMethod) && (
|
|
289
305
|
<InfoRow
|
|
290
306
|
label={t('common.delegateTxHash')}
|
|
@@ -3,7 +3,7 @@ import DID from '@arcblock/ux/lib/DID';
|
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import { api, formatBNStr, formatToDate, usePaymentContext } from '@blocklet/payment-react';
|
|
5
5
|
import { BN } from '@ocap/util';
|
|
6
|
-
import type { GroupedBN, TPaymentMethod, TPaymentStat } from '@blocklet/payment-types';
|
|
6
|
+
import type { GroupedBN, TPaymentCurrency, TPaymentMethod, TPaymentStat } from '@blocklet/payment-types';
|
|
7
7
|
import {
|
|
8
8
|
Avatar,
|
|
9
9
|
Box,
|
|
@@ -175,8 +175,8 @@ export default function Overview() {
|
|
|
175
175
|
|
|
176
176
|
const currencies: TCurrencyMap = useMemo(() => {
|
|
177
177
|
const map: TCurrencyMap = {};
|
|
178
|
-
(settings.paymentMethods || []).forEach((method) => {
|
|
179
|
-
(method.payment_currencies || []).forEach((currency) => {
|
|
178
|
+
(settings.paymentMethods || []).forEach((method: { payment_currencies: TPaymentCurrency[] }) => {
|
|
179
|
+
(method.payment_currencies || []).forEach((currency: TPaymentCurrency) => {
|
|
180
180
|
if (!map[currency.id]) {
|
|
181
181
|
map[currency.id] = {
|
|
182
182
|
...currency,
|
|
@@ -1105,10 +1105,12 @@ export default function Overview() {
|
|
|
1105
1105
|
sx={{
|
|
1106
1106
|
p: 1.5,
|
|
1107
1107
|
flex: '1 1 calc(50% - 8px)',
|
|
1108
|
+
minWidth: 0,
|
|
1108
1109
|
borderRadius: 1,
|
|
1109
1110
|
backgroundColor: theme.mode === 'dark' ? 'grey.100' : 'grey.50',
|
|
1110
1111
|
border: 'none',
|
|
1111
1112
|
boxShadow: 'none',
|
|
1113
|
+
containerType: 'inline-size',
|
|
1112
1114
|
}}>
|
|
1113
1115
|
<Box>
|
|
1114
1116
|
<Typography
|
|
@@ -1124,13 +1126,11 @@ export default function Overview() {
|
|
|
1124
1126
|
sx={{
|
|
1125
1127
|
gap: 1.5,
|
|
1126
1128
|
flexWrap: 'wrap',
|
|
1127
|
-
flexDirection:
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
xs: 'flex-start',
|
|
1133
|
-
md: 'baseline',
|
|
1129
|
+
flexDirection: 'row',
|
|
1130
|
+
alignItems: 'baseline',
|
|
1131
|
+
'@container (max-width: 220px)': {
|
|
1132
|
+
flexDirection: 'column',
|
|
1133
|
+
alignItems: 'flex-start',
|
|
1134
1134
|
},
|
|
1135
1135
|
}}>
|
|
1136
1136
|
<Typography
|
|
@@ -1239,14 +1239,14 @@ export default function Overview() {
|
|
|
1239
1239
|
|
|
1240
1240
|
<Grid
|
|
1241
1241
|
container
|
|
1242
|
+
spacing={{ xs: 3, lg: 6 }}
|
|
1242
1243
|
sx={{
|
|
1243
|
-
gap: { xs: 3, md: 6 },
|
|
1244
1244
|
mb: 4,
|
|
1245
1245
|
}}>
|
|
1246
1246
|
<Grid
|
|
1247
1247
|
size={{
|
|
1248
1248
|
xs: 12,
|
|
1249
|
-
|
|
1249
|
+
lg: 8,
|
|
1250
1250
|
}}>
|
|
1251
1251
|
{renderFinancialSection()}
|
|
1252
1252
|
</Grid>
|
|
@@ -1254,7 +1254,7 @@ export default function Overview() {
|
|
|
1254
1254
|
<Grid
|
|
1255
1255
|
size={{
|
|
1256
1256
|
xs: 12,
|
|
1257
|
-
|
|
1257
|
+
lg: 4,
|
|
1258
1258
|
}}>
|
|
1259
1259
|
{renderBusinessMonitoringSection()}
|
|
1260
1260
|
</Grid>
|
|
@@ -1265,7 +1265,7 @@ export default function Overview() {
|
|
|
1265
1265
|
open={open}
|
|
1266
1266
|
anchorEl={state.anchorEl}
|
|
1267
1267
|
onClose={() => setState({ anchorEl: null })}
|
|
1268
|
-
anchorOrigin={{ vertical: 'bottom', horizontal: '
|
|
1268
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}>
|
|
1269
1269
|
<DateRangePicker
|
|
1270
1270
|
open
|
|
1271
1271
|
toggle={onTogglePicker as any}
|
|
@@ -5,23 +5,21 @@ import { AddOutlined } from '@mui/icons-material';
|
|
|
5
5
|
import {
|
|
6
6
|
Button,
|
|
7
7
|
CircularProgress,
|
|
8
|
-
Stack,
|
|
9
|
-
TextField,
|
|
10
|
-
FormControlLabel,
|
|
11
|
-
Switch,
|
|
12
8
|
FormControl,
|
|
9
|
+
FormControlLabel,
|
|
13
10
|
InputLabel,
|
|
14
|
-
Select,
|
|
15
11
|
MenuItem,
|
|
12
|
+
Select,
|
|
13
|
+
Stack,
|
|
14
|
+
Switch,
|
|
15
|
+
TextField,
|
|
16
16
|
} from '@mui/material';
|
|
17
17
|
import { useState } from 'react';
|
|
18
|
-
import {
|
|
18
|
+
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
|
19
19
|
import { dispatch } from 'use-bus';
|
|
20
20
|
|
|
21
|
-
import { joinURL, withQuery } from 'ufo';
|
|
22
21
|
import DrawerForm from '../../../../components/drawer-form';
|
|
23
22
|
import MetadataForm from '../../../../components/metadata/form';
|
|
24
|
-
import { formatProxyUrl } from '../../../../libs/util';
|
|
25
23
|
|
|
26
24
|
interface Vendor {
|
|
27
25
|
id: string;
|
|
@@ -84,8 +82,6 @@ export default function VendorCreate({
|
|
|
84
82
|
app_url: '',
|
|
85
83
|
status: 'inactive' as const,
|
|
86
84
|
metadata: [{ key: 'blockletMetaUrl', value: '' }],
|
|
87
|
-
app_pid: '',
|
|
88
|
-
app_logo: '',
|
|
89
85
|
};
|
|
90
86
|
|
|
91
87
|
const methods = useForm<VendorFormData>({
|
|
@@ -147,36 +143,6 @@ export default function VendorCreate({
|
|
|
147
143
|
metadata: metadataObj,
|
|
148
144
|
};
|
|
149
145
|
|
|
150
|
-
// 如果状态为启用,则检测应用地址可用性
|
|
151
|
-
if (submitData.status === 'active') {
|
|
152
|
-
try {
|
|
153
|
-
const response = await fetch(
|
|
154
|
-
formatProxyUrl(withQuery(joinURL(submitData.app_url, '__blocklet__.js'), { type: 'json' })),
|
|
155
|
-
{
|
|
156
|
-
method: 'GET',
|
|
157
|
-
headers: { 'Content-Type': 'application/json' },
|
|
158
|
-
}
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
if (!response.ok) {
|
|
162
|
-
Toast.error(t('admin.vendor.addressCheckFailed'));
|
|
163
|
-
setLoading(false);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 从响应中获取appPid和appLogo
|
|
168
|
-
const blockletInfo = await response.json();
|
|
169
|
-
if (blockletInfo) {
|
|
170
|
-
submitData.app_pid = blockletInfo.pid || blockletInfo.appPid;
|
|
171
|
-
submitData.app_logo = blockletInfo.logo || blockletInfo.appLogo;
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
Toast.error(t('admin.vendor.addressCheckFailed'));
|
|
175
|
-
setLoading(false);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
146
|
if (isEditMode && vendorData) {
|
|
181
147
|
// 编辑模式:更新供应商
|
|
182
148
|
await api.put(`/api/vendors/${vendorData.id}`, submitData);
|
|
@@ -6,6 +6,7 @@ import { ContentCopy } from '@mui/icons-material';
|
|
|
6
6
|
import { Box, Chip, CircularProgress, IconButton, Tooltip, Typography } from '@mui/material';
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
import useBus from 'use-bus';
|
|
9
|
+
import omit from 'lodash/omit';
|
|
9
10
|
|
|
10
11
|
import { useLocalStorageState } from 'ahooks';
|
|
11
12
|
import FilterToolbar from '../../../../components/filter-toolbar';
|
|
@@ -320,7 +321,10 @@ export default function VendorsList() {
|
|
|
320
321
|
setSelectedVendor(null);
|
|
321
322
|
refresh();
|
|
322
323
|
}}
|
|
323
|
-
vendorData={
|
|
324
|
+
vendorData={{
|
|
325
|
+
...selectedVendor,
|
|
326
|
+
metadata: omit(selectedVendor?.metadata || {}, 'mountPoint'),
|
|
327
|
+
}}
|
|
324
328
|
/>
|
|
325
329
|
)}
|
|
326
330
|
</>
|
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
usePaymentContext,
|
|
16
16
|
PaymentBeneficiaries,
|
|
17
17
|
useMobile,
|
|
18
|
+
StripePaymentAction,
|
|
18
19
|
} from '@blocklet/payment-react';
|
|
19
20
|
import type { TCheckoutSession, TCreditGrantExpanded, TInvoiceExpanded, TPaymentLink } from '@blocklet/payment-types';
|
|
20
21
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
21
22
|
import { Alert, Box, Button, CircularProgress, Divider, Stack, Tooltip, Typography, useTheme } from '@mui/material';
|
|
22
23
|
import { styled } from '@mui/system';
|
|
23
24
|
import { useRequest, useSetState } from 'ahooks';
|
|
24
|
-
import { useEffect } from 'react';
|
|
25
|
+
import { useEffect, useRef } from 'react';
|
|
25
26
|
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
26
27
|
|
|
27
28
|
import { joinURL } from 'ufo';
|
|
@@ -48,7 +49,7 @@ const fetchData = (
|
|
|
48
49
|
relatedCreditGrants?: TCreditGrantExpanded[];
|
|
49
50
|
}
|
|
50
51
|
> => {
|
|
51
|
-
return api.get(`/api/invoices/${id}`).then((res) => res.data);
|
|
52
|
+
return api.get(`/api/invoices/${id}`).then((res: any) => res.data);
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
export default function CustomerInvoiceDetail() {
|
|
@@ -64,16 +65,19 @@ export default function CustomerInvoiceDetail() {
|
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
const { loading, error, data, runAsync } = useRequest(() => fetchData(params.id as string));
|
|
68
|
+
|
|
67
69
|
const action = searchParams.get('action');
|
|
68
70
|
const { session } = useSessionContext();
|
|
69
71
|
const isDonation = data?.checkoutSession?.submit_type === 'donate';
|
|
72
|
+
const payHandlerRef = useRef<(() => void) | null>(null);
|
|
70
73
|
|
|
71
|
-
const
|
|
74
|
+
const handleExternalPayment = () => {
|
|
72
75
|
setState({ paying: true });
|
|
73
76
|
connect.open({
|
|
74
77
|
action: 'collect',
|
|
75
78
|
saveConnect: false,
|
|
76
79
|
locale: locale as 'en' | 'zh',
|
|
80
|
+
extraParams: { invoiceId: params.id, action },
|
|
77
81
|
messages: {
|
|
78
82
|
scan: '',
|
|
79
83
|
title: t(`payment.customer.invoice.${action || 'pay'}`),
|
|
@@ -81,9 +85,9 @@ export default function CustomerInvoiceDetail() {
|
|
|
81
85
|
error: t(`payment.customer.invoice.${action || 'pay'}Error`),
|
|
82
86
|
confirm: '',
|
|
83
87
|
} as any,
|
|
84
|
-
extraParams: { invoiceId: params.id, action },
|
|
85
88
|
onSuccess: async () => {
|
|
86
89
|
connect.close();
|
|
90
|
+
setState({ paying: false });
|
|
87
91
|
await runAsync();
|
|
88
92
|
},
|
|
89
93
|
onClose: () => {
|
|
@@ -110,7 +114,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
110
114
|
['pay', 'renew'].includes(action as string) &&
|
|
111
115
|
['open', 'uncollectible'].includes(data?.status as string)
|
|
112
116
|
) {
|
|
113
|
-
|
|
117
|
+
handleExternalPayment();
|
|
114
118
|
}
|
|
115
119
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
120
|
}, [error]);
|
|
@@ -133,6 +137,9 @@ export default function CustomerInvoiceDetail() {
|
|
|
133
137
|
|
|
134
138
|
const hidePayButton = data.billing_reason === 'overdraft_protection';
|
|
135
139
|
const paymentDetails = data.paymentIntent?.payment_details || data.metadata?.payment_details;
|
|
140
|
+
|
|
141
|
+
const showDownloadButton =
|
|
142
|
+
['open', 'paid', 'uncollectible'].includes(data.status) && !isDonation && !isEmpty(data.lines);
|
|
136
143
|
return (
|
|
137
144
|
<InvoiceDetailRoot direction="column" spacing={3}>
|
|
138
145
|
<Stack
|
|
@@ -161,7 +168,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
161
168
|
justifyContent: 'flex-end',
|
|
162
169
|
alignItems: 'center',
|
|
163
170
|
}}>
|
|
164
|
-
{
|
|
171
|
+
{showDownloadButton && <Download data={data} />}
|
|
165
172
|
{data?.paymentLink?.donation_settings?.reference && (
|
|
166
173
|
<Button
|
|
167
174
|
variant="outlined"
|
|
@@ -171,17 +178,52 @@ export default function CustomerInvoiceDetail() {
|
|
|
171
178
|
{t('customer.payout.viewReference')}
|
|
172
179
|
</Button>
|
|
173
180
|
)}
|
|
174
|
-
{['open', 'uncollectible'].includes(data.status) &&
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
{['open', 'uncollectible'].includes(data.status) &&
|
|
182
|
+
!hidePayButton &&
|
|
183
|
+
(data.paymentMethod?.type === 'stripe' ? (
|
|
184
|
+
<StripePaymentAction
|
|
185
|
+
invoice={data}
|
|
186
|
+
paymentMethod={data.paymentMethod}
|
|
187
|
+
onSuccess={async () => {
|
|
188
|
+
Toast.close();
|
|
189
|
+
setState({ paying: false });
|
|
190
|
+
await runAsync();
|
|
191
|
+
}}
|
|
192
|
+
onError={() => {
|
|
193
|
+
setState({ paying: false });
|
|
194
|
+
}}
|
|
195
|
+
onClose={() => {
|
|
196
|
+
setState({ paying: false });
|
|
197
|
+
}}>
|
|
198
|
+
{(onPay: () => void, paying: boolean) => {
|
|
199
|
+
payHandlerRef.current = onPay;
|
|
200
|
+
return (
|
|
201
|
+
<Button
|
|
202
|
+
variant="outlined"
|
|
203
|
+
color="primary"
|
|
204
|
+
size="small"
|
|
205
|
+
sx={{ borderColor: 'divider' }}
|
|
206
|
+
disabled={state.paying || paying}
|
|
207
|
+
onClick={() => {
|
|
208
|
+
setState({ paying: true });
|
|
209
|
+
onPay();
|
|
210
|
+
}}>
|
|
211
|
+
{t('payment.customer.invoice.pay')}
|
|
212
|
+
</Button>
|
|
213
|
+
);
|
|
214
|
+
}}
|
|
215
|
+
</StripePaymentAction>
|
|
216
|
+
) : (
|
|
217
|
+
<Button
|
|
218
|
+
variant="outlined"
|
|
219
|
+
color="primary"
|
|
220
|
+
size="small"
|
|
221
|
+
sx={{ borderColor: 'divider' }}
|
|
222
|
+
disabled={state.paying}
|
|
223
|
+
onClick={handleExternalPayment}>
|
|
224
|
+
{t('payment.customer.invoice.pay')}
|
|
225
|
+
</Button>
|
|
226
|
+
))}
|
|
185
227
|
</Stack>
|
|
186
228
|
</Stack>
|
|
187
229
|
<Box
|