payment-kit 1.13.70 → 1.13.72
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/jobs/payment.ts +15 -17
- package/api/src/libs/util.ts +1 -0
- package/api/src/locales/en.ts +1 -0
- package/api/src/locales/zh.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +3 -3
- package/src/app.tsx +30 -3
- package/src/components/{layout.tsx → layout/admin.tsx} +24 -11
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/pages/admin/index.tsx +1 -1
- package/src/pages/customer/index.tsx +77 -82
- package/src/pages/customer/invoice.tsx +70 -73
- package/src/pages/customer/subscription/index.tsx +105 -108
package/api/src/jobs/payment.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { events } from '../libs/event';
|
|
|
6
6
|
import logger from '../libs/logger';
|
|
7
7
|
import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
|
|
8
8
|
import createQueue from '../libs/queue';
|
|
9
|
-
import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
|
|
9
|
+
import { MAX_RETRY_COUNT, MIN_RETRY_MAIL, getNextRetry } from '../libs/util';
|
|
10
10
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
11
11
|
import { Invoice } from '../store/models/invoice';
|
|
12
12
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -66,13 +66,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
where: {
|
|
71
|
-
subscription_id: invoice.subscription_id,
|
|
72
|
-
status: 'paid',
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
if (count >= 2) {
|
|
69
|
+
if (invoice.billing_reason === 'subscription_cycle') {
|
|
76
70
|
events.emit('customer.subscription.renewed', subscription, invoice);
|
|
77
71
|
}
|
|
78
72
|
}
|
|
@@ -131,6 +125,7 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
131
125
|
|
|
132
126
|
// try payment capture and reschedule on error
|
|
133
127
|
logger.info(`PaymentIntent capture attempt: ${paymentIntent.id}`);
|
|
128
|
+
let result;
|
|
134
129
|
try {
|
|
135
130
|
await paymentIntent.update({ status: 'processing', last_payment_error: null });
|
|
136
131
|
|
|
@@ -138,7 +133,7 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
138
133
|
const payer = paymentSettings?.payment_method_options.arcblock?.payer;
|
|
139
134
|
|
|
140
135
|
// check balance before capture
|
|
141
|
-
|
|
136
|
+
result = await isDelegationSufficientForPayment({
|
|
142
137
|
paymentMethod,
|
|
143
138
|
paymentCurrency,
|
|
144
139
|
userDid: payer as string,
|
|
@@ -146,11 +141,6 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
146
141
|
});
|
|
147
142
|
if (result.sufficient === false) {
|
|
148
143
|
logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
|
|
149
|
-
events.emit('customer.subscription.renew_failed', {
|
|
150
|
-
invoice,
|
|
151
|
-
result,
|
|
152
|
-
});
|
|
153
|
-
// FIXME: send email to customer, pause subscription
|
|
154
144
|
throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
|
|
155
145
|
}
|
|
156
146
|
|
|
@@ -218,11 +208,19 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
218
208
|
});
|
|
219
209
|
}
|
|
220
210
|
} else if (invoice) {
|
|
211
|
+
const attemptCount = invoice.attempt_count + 1;
|
|
212
|
+
if (attemptCount >= MIN_RETRY_MAIL && invoice.billing_reason === 'subscription_cycle') {
|
|
213
|
+
events.emit('customer.subscription.renew_failed', {
|
|
214
|
+
invoice,
|
|
215
|
+
result: result || { sufficient: false, reason: 'TX_SEND_FAILED' },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
221
219
|
if (invoice.attempt_count > MAX_RETRY_COUNT) {
|
|
222
220
|
await paymentIntent.update({ status: 'requires_action', last_payment_error: error });
|
|
223
221
|
await invoice.update({
|
|
224
222
|
status: 'uncollectible',
|
|
225
|
-
attempt_count:
|
|
223
|
+
attempt_count: attemptCount,
|
|
226
224
|
attempted: true,
|
|
227
225
|
status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
|
|
228
226
|
});
|
|
@@ -230,11 +228,11 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
230
228
|
// FIXME: send email to customer, pause subscription
|
|
231
229
|
logger.error('PaymentIntent capture failed after max retry', { id: paymentIntent.id });
|
|
232
230
|
} else {
|
|
233
|
-
const retryAt = getNextRetry(
|
|
231
|
+
const retryAt = getNextRetry(attemptCount);
|
|
234
232
|
|
|
235
233
|
await paymentIntent.update({ status: 'requires_capture', last_payment_error: error });
|
|
236
234
|
await invoice.update({
|
|
237
|
-
attempt_count:
|
|
235
|
+
attempt_count: attemptCount,
|
|
238
236
|
attempted: true,
|
|
239
237
|
next_payment_attempt: retryAt,
|
|
240
238
|
});
|
package/api/src/libs/util.ts
CHANGED
|
@@ -10,6 +10,7 @@ import dayjs from './dayjs';
|
|
|
10
10
|
export const OCAP_PAYMENT_TX_TYPE = 'fg:t:transfer_v2';
|
|
11
11
|
|
|
12
12
|
export const MAX_RETRY_COUNT = 18; // 2^18 seconds ~~ 3 days, total retry time: 6 days
|
|
13
|
+
export const MIN_RETRY_MAIL = 13; // total retry time before sending first mail: 6 hours
|
|
13
14
|
export const STRIPE_API_VERSION = '2023-08-16';
|
|
14
15
|
export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook');
|
|
15
16
|
export const STRIPE_EVENTS: any[] = [
|
package/api/src/locales/en.ts
CHANGED
|
@@ -77,6 +77,7 @@ export default flat({
|
|
|
77
77
|
noEnoughToken:
|
|
78
78
|
'Your account token balance is {balance}, not enough for {price}, please replenish your tokens and renew your account',
|
|
79
79
|
noSupported: 'Token renewal is not supported, please check your subscription',
|
|
80
|
+
txSendFailed: 'Failed to send transaction when try to collect payment.',
|
|
80
81
|
},
|
|
81
82
|
},
|
|
82
83
|
},
|
package/api/src/locales/zh.ts
CHANGED
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.72",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"@abtnode/types": "^1.16.19",
|
|
107
107
|
"@arcblock/eslint-config": "^0.2.4",
|
|
108
108
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
109
|
-
"@did-pay/types": "1.13.
|
|
109
|
+
"@did-pay/types": "1.13.72",
|
|
110
110
|
"@types/cookie-parser": "^1.4.6",
|
|
111
111
|
"@types/cors": "^2.8.17",
|
|
112
112
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -143,5 +143,5 @@
|
|
|
143
143
|
"parser": "typescript"
|
|
144
144
|
}
|
|
145
145
|
},
|
|
146
|
-
"gitHead": "
|
|
146
|
+
"gitHead": "bbd67e67e0e6154506b12d7ce30b314e895382c0"
|
|
147
147
|
}
|
package/src/app.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-d
|
|
|
11
11
|
import { joinURL } from 'ufo';
|
|
12
12
|
|
|
13
13
|
import ErrorFallback from './components/error-fallback';
|
|
14
|
+
import Layout from './components/layout/admin';
|
|
14
15
|
import { SessionProvider } from './contexts/session';
|
|
15
16
|
import { translations } from './locales';
|
|
16
17
|
|
|
@@ -48,9 +49,35 @@ function App() {
|
|
|
48
49
|
<Route key="admin-tabs" path="/admin/:group" element={<AdminPage />} />,
|
|
49
50
|
<Route key="admin-sub" path="/admin/:group/:page" element={<AdminPage />} />,
|
|
50
51
|
<Route key="admin-fallback" path="/admin/*" element={<AdminPage />} />,
|
|
51
|
-
<Route
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
<Route
|
|
53
|
+
key="customer-home"
|
|
54
|
+
path="/customer"
|
|
55
|
+
element={
|
|
56
|
+
<Layout>
|
|
57
|
+
<CustomerHome />
|
|
58
|
+
</Layout>
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
,
|
|
62
|
+
<Route
|
|
63
|
+
key="customer-subscription"
|
|
64
|
+
path="/customer/subscription/:id"
|
|
65
|
+
element={
|
|
66
|
+
<Layout>
|
|
67
|
+
<CustomerSubscription />
|
|
68
|
+
</Layout>
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
<Route
|
|
72
|
+
key="customer-invoice"
|
|
73
|
+
path="/customer/invoice/:id"
|
|
74
|
+
element={
|
|
75
|
+
<Layout>
|
|
76
|
+
<CustomerInvoice />
|
|
77
|
+
</Layout>
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
,
|
|
54
81
|
<Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
|
|
55
82
|
<Route path="*" element={<Navigate to="/" />} />
|
|
56
83
|
</Routes>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/exhaustive-deps */
|
|
2
|
+
import Center from '@arcblock/ux/lib/Center';
|
|
3
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
4
|
import Dashboard from '@blocklet/ui-react/lib/Dashboard';
|
|
5
|
+
import { Avatar, Button, Stack, Typography } from '@mui/material';
|
|
3
6
|
import { styled } from '@mui/system';
|
|
4
|
-
import { useEffect, useState } from 'react';
|
|
5
7
|
|
|
6
|
-
import { useSessionContext } from '
|
|
8
|
+
import { useSessionContext } from '../../contexts/session';
|
|
7
9
|
|
|
8
10
|
const Root = styled(Dashboard)`
|
|
9
11
|
width: 100%;
|
|
@@ -50,17 +52,28 @@ const Root = styled(Dashboard)`
|
|
|
50
52
|
`;
|
|
51
53
|
|
|
52
54
|
export default function Layout(props: any) {
|
|
55
|
+
const { t } = useLocaleContext();
|
|
53
56
|
const { session } = useSessionContext();
|
|
54
|
-
const [loggingIn, setLoggingIn] = useState(false);
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
const handleLogin = () => {
|
|
59
|
+
session.login();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!session.user) {
|
|
63
|
+
return (
|
|
64
|
+
<Center>
|
|
65
|
+
<Stack maxWidth="sm" direction="column" alignItems="center" spacing={3}>
|
|
66
|
+
<Avatar src={window.blocklet.appLogo} sx={{ width: 80, height: 80 }} />
|
|
67
|
+
<Stack direction="column" alignItems="center" spacing={3}>
|
|
68
|
+
<Typography variant="h4">{window.blocklet.appName}</Typography>
|
|
69
|
+
<Button size="large" variant="contained" color="secondary" onClick={handleLogin}>
|
|
70
|
+
{t('common.login')}
|
|
71
|
+
</Button>
|
|
72
|
+
</Stack>
|
|
73
|
+
</Stack>
|
|
74
|
+
</Center>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
64
77
|
|
|
65
78
|
if (session.user) {
|
|
66
79
|
return <Root {...props} footerProps={{ className: 'dashboard-footer' }} />;
|
package/src/locales/en.tsx
CHANGED
package/src/locales/zh.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { Box, Chip, Stack } from '@mui/material';
|
|
|
3
3
|
import React, { isValidElement } from 'react';
|
|
4
4
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
5
5
|
|
|
6
|
-
import Layout from '../../components/layout';
|
|
6
|
+
import Layout from '../../components/layout/admin';
|
|
7
7
|
import Switch from '../../components/switch';
|
|
8
8
|
import { SettingsProvider, useSettingsContext } from '../../contexts/settings';
|
|
9
9
|
|
|
@@ -10,7 +10,6 @@ import { useNavigate } from 'react-router-dom';
|
|
|
10
10
|
|
|
11
11
|
import EditCustomer from '../../components/customer/edit';
|
|
12
12
|
import InfoRow from '../../components/info-row';
|
|
13
|
-
import Layout from '../../components/layout';
|
|
14
13
|
import CustomerInvoiceList from '../../components/portal/invoice/list';
|
|
15
14
|
import CurrentSubscriptions from '../../components/portal/subscription/list';
|
|
16
15
|
import SectionHeader from '../../components/section/header';
|
|
@@ -41,11 +40,9 @@ export default function CustomerHome() {
|
|
|
41
40
|
|
|
42
41
|
if (!data) {
|
|
43
42
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</Alert>
|
|
48
|
-
</Layout>
|
|
43
|
+
<Alert sx={{ mt: 3 }} severity="info">
|
|
44
|
+
{t('customer.empty')}
|
|
45
|
+
</Alert>
|
|
49
46
|
);
|
|
50
47
|
}
|
|
51
48
|
|
|
@@ -64,85 +61,83 @@ export default function CustomerHome() {
|
|
|
64
61
|
};
|
|
65
62
|
|
|
66
63
|
return (
|
|
67
|
-
<
|
|
68
|
-
<Grid
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/>
|
|
84
|
-
</Box>
|
|
64
|
+
<Grid container spacing={5}>
|
|
65
|
+
<Grid item xs={12} md={8}>
|
|
66
|
+
<Root direction="column" spacing={3} sx={{ my: 2 }}>
|
|
67
|
+
<Box className="section">
|
|
68
|
+
<SectionHeader title={t('customer.subscriptions.current')} mb={0} />
|
|
69
|
+
<Box className="section-body">
|
|
70
|
+
<CurrentSubscriptions
|
|
71
|
+
id={data.id}
|
|
72
|
+
onChange={runAsync}
|
|
73
|
+
style={{
|
|
74
|
+
cursor: 'pointer',
|
|
75
|
+
}}
|
|
76
|
+
onClickSubscription={(subscription) => {
|
|
77
|
+
navigate(`/customer/subscription/${subscription.id}`);
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
85
80
|
</Box>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
</Box>
|
|
82
|
+
<Box className="section">
|
|
83
|
+
<SectionHeader title={t('customer.invoices')} mb={0} />
|
|
84
|
+
<Box className="section-body">
|
|
85
|
+
<CustomerInvoiceList customer_id={data.id} />
|
|
91
86
|
</Box>
|
|
92
|
-
</
|
|
93
|
-
</
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
</
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
</
|
|
143
|
-
</
|
|
87
|
+
</Box>
|
|
88
|
+
</Root>
|
|
89
|
+
</Grid>
|
|
90
|
+
<Grid item xs={12} md={4}>
|
|
91
|
+
<Root direction="column" spacing={4} sx={{ my: 2 }}>
|
|
92
|
+
<Box className="section">
|
|
93
|
+
<SectionHeader title={t('customer.details')}>
|
|
94
|
+
<Button
|
|
95
|
+
variant="outlined"
|
|
96
|
+
color="inherit"
|
|
97
|
+
size="small"
|
|
98
|
+
disabled={state.editing || state.loading}
|
|
99
|
+
onClick={() => setState({ editing: true })}>
|
|
100
|
+
<Edit fontSize="small" sx={{ mr: 0.5 }} />
|
|
101
|
+
{t('customer.update')}
|
|
102
|
+
</Button>
|
|
103
|
+
</SectionHeader>
|
|
104
|
+
<Stack>
|
|
105
|
+
<InfoRow sizes={[1, 2]} label={t('admin.customer.name')} value={data.name} />
|
|
106
|
+
<InfoRow sizes={[1, 2]} label={t('admin.customer.phone')} value={data.phone} />
|
|
107
|
+
<InfoRow sizes={[1, 2]} label={t('admin.customer.email')} value={data.email} />
|
|
108
|
+
<InfoRow
|
|
109
|
+
sizes={[1, 2]}
|
|
110
|
+
label={t('admin.customer.address.country')}
|
|
111
|
+
value={
|
|
112
|
+
data.address?.country ? (
|
|
113
|
+
<FlagEmoji iso2={data.address?.country} style={{ display: 'flex', width: 24 }} />
|
|
114
|
+
) : (
|
|
115
|
+
''
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
<InfoRow sizes={[1, 2]} label={t('admin.customer.address.state')} value={data.address?.state} />
|
|
120
|
+
<InfoRow sizes={[1, 2]} label={t('admin.customer.address.city')} value={data.address?.city} />
|
|
121
|
+
<InfoRow sizes={[1, 2]} label={t('admin.customer.address.line1')} value={data.address?.line1} />
|
|
122
|
+
<InfoRow sizes={[1, 2]} label={t('admin.customer.address.line2')} value={data.address?.line2} />
|
|
123
|
+
<InfoRow
|
|
124
|
+
sizes={[1, 2]}
|
|
125
|
+
label={t('admin.customer.address.postal_code')}
|
|
126
|
+
value={data.address?.postal_code}
|
|
127
|
+
/>
|
|
128
|
+
</Stack>
|
|
129
|
+
{state.editing && (
|
|
130
|
+
<EditCustomer
|
|
131
|
+
data={data}
|
|
132
|
+
loading={state.loading}
|
|
133
|
+
onSave={onUpdateInfo}
|
|
134
|
+
onCancel={() => setState({ editing: false })}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
</Box>
|
|
138
|
+
</Root>
|
|
144
139
|
</Grid>
|
|
145
|
-
</
|
|
140
|
+
</Grid>
|
|
146
141
|
);
|
|
147
142
|
}
|
|
148
143
|
|
|
@@ -11,7 +11,6 @@ import TxLink from '../../components/blockchain/tx';
|
|
|
11
11
|
import Currency from '../../components/currency';
|
|
12
12
|
import InfoRow from '../../components/info-row';
|
|
13
13
|
import InvoiceTable from '../../components/invoice/table';
|
|
14
|
-
import Layout from '../../components/layout';
|
|
15
14
|
import SectionHeader from '../../components/section/header';
|
|
16
15
|
import Status from '../../components/status';
|
|
17
16
|
import { useSessionContext } from '../../contexts/session';
|
|
@@ -70,80 +69,78 @@ export default function CustomerHome() {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
return (
|
|
73
|
-
<
|
|
74
|
-
<Grid
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
data.period_start
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{data.status_transitions
|
|
105
|
-
<InfoRow label={t('admin.invoice.paidAt')} value={formatTime(data.status_transitions.paid_at * 1000)} />
|
|
106
|
-
)}
|
|
107
|
-
<InfoRow
|
|
108
|
-
label={t('admin.paymentCurrency.name')}
|
|
109
|
-
value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
|
|
110
|
-
/>
|
|
111
|
-
<InfoRow
|
|
112
|
-
label={t('common.txHash')}
|
|
113
|
-
value={
|
|
114
|
-
data.paymentIntent?.payment_details ? (
|
|
115
|
-
<TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />
|
|
116
|
-
) : (
|
|
117
|
-
''
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
/>
|
|
121
|
-
</Stack>
|
|
122
|
-
</Box>
|
|
123
|
-
</Grid>
|
|
124
|
-
<Grid item xs={12} md={7}>
|
|
125
|
-
<SectionHeader title={t('customer.invoice.details')} mb={0}>
|
|
126
|
-
{['open'].includes(data.status) && (
|
|
127
|
-
<Button
|
|
128
|
-
variant="contained"
|
|
129
|
-
color="primary"
|
|
130
|
-
size="small"
|
|
131
|
-
disabled={state.downloading}
|
|
132
|
-
onClick={() => setState({ downloading: true })}>
|
|
133
|
-
{t('customer.invoice.download')}
|
|
134
|
-
</Button>
|
|
135
|
-
)}
|
|
136
|
-
</SectionHeader>
|
|
137
|
-
<InvoiceTable invoice={data} simple />
|
|
138
|
-
<Stack direction="row" justifyContent="flex-end" alignItems="center" mt={2}>
|
|
139
|
-
{['open', 'uncollectible'].includes(data.status) && (
|
|
140
|
-
<Button variant="contained" color="primary" disabled={state.paying} onClick={onPay}>
|
|
141
|
-
{t('customer.invoice.pay')}
|
|
142
|
-
</Button>
|
|
72
|
+
<Grid container spacing={3} sx={{ mt: 1 }}>
|
|
73
|
+
<Grid item xs={12} md={12}>
|
|
74
|
+
<Link to="/customer">
|
|
75
|
+
<Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
|
|
76
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
77
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
78
|
+
{t('common.previous')}
|
|
79
|
+
</Typography>
|
|
80
|
+
</Stack>
|
|
81
|
+
</Link>
|
|
82
|
+
</Grid>
|
|
83
|
+
<Grid item xs={12} md={5}>
|
|
84
|
+
<Box>
|
|
85
|
+
<SectionHeader title={t('customer.invoice.summary')} mb={0} />
|
|
86
|
+
<Stack sx={{ mt: 1 }}>
|
|
87
|
+
<InfoRow label={t('admin.invoice.number')} value={data.number} />
|
|
88
|
+
<InfoRow label={t('admin.invoice.from')} value={data.statement_descriptor || blocklet.appName} />
|
|
89
|
+
<InfoRow label={t('admin.invoice.customer')} value={data.customer.name} />
|
|
90
|
+
<InfoRow
|
|
91
|
+
label={t('common.status')}
|
|
92
|
+
value={<Status label={data.status} color={getInvoiceStatusColor(data.status)} />}
|
|
93
|
+
/>
|
|
94
|
+
<InfoRow
|
|
95
|
+
label={t('admin.subscription.currentPeriod')}
|
|
96
|
+
value={
|
|
97
|
+
data.period_start && data.period_end
|
|
98
|
+
? [formatTime(data.period_start * 1000), formatTime(data.period_end * 1000)].join(' ~ ')
|
|
99
|
+
: ''
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
{data.status_transitions?.paid_at && (
|
|
103
|
+
<InfoRow label={t('admin.invoice.paidAt')} value={formatTime(data.status_transitions.paid_at * 1000)} />
|
|
143
104
|
)}
|
|
105
|
+
<InfoRow
|
|
106
|
+
label={t('admin.paymentCurrency.name')}
|
|
107
|
+
value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
|
|
108
|
+
/>
|
|
109
|
+
<InfoRow
|
|
110
|
+
label={t('common.txHash')}
|
|
111
|
+
value={
|
|
112
|
+
data.paymentIntent?.payment_details ? (
|
|
113
|
+
<TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />
|
|
114
|
+
) : (
|
|
115
|
+
''
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
144
119
|
</Stack>
|
|
145
|
-
</
|
|
120
|
+
</Box>
|
|
121
|
+
</Grid>
|
|
122
|
+
<Grid item xs={12} md={7}>
|
|
123
|
+
<SectionHeader title={t('customer.invoice.details')} mb={0}>
|
|
124
|
+
{['open'].includes(data.status) && (
|
|
125
|
+
<Button
|
|
126
|
+
variant="contained"
|
|
127
|
+
color="primary"
|
|
128
|
+
size="small"
|
|
129
|
+
disabled={state.downloading}
|
|
130
|
+
onClick={() => setState({ downloading: true })}>
|
|
131
|
+
{t('customer.invoice.download')}
|
|
132
|
+
</Button>
|
|
133
|
+
)}
|
|
134
|
+
</SectionHeader>
|
|
135
|
+
<InvoiceTable invoice={data} simple />
|
|
136
|
+
<Stack direction="row" justifyContent="flex-end" alignItems="center" mt={2}>
|
|
137
|
+
{['open', 'uncollectible'].includes(data.status) && (
|
|
138
|
+
<Button variant="contained" color="primary" disabled={state.paying} onClick={onPay}>
|
|
139
|
+
{t('customer.invoice.pay')}
|
|
140
|
+
</Button>
|
|
141
|
+
)}
|
|
142
|
+
</Stack>
|
|
146
143
|
</Grid>
|
|
147
|
-
</
|
|
144
|
+
</Grid>
|
|
148
145
|
);
|
|
149
146
|
}
|
|
@@ -12,7 +12,6 @@ import Currency from '../../../components/currency';
|
|
|
12
12
|
import InfoMetric from '../../../components/info-metric';
|
|
13
13
|
import InfoRow from '../../../components/info-row';
|
|
14
14
|
import InvoiceList from '../../../components/invoice/list';
|
|
15
|
-
import Layout from '../../../components/layout';
|
|
16
15
|
import SectionHeader from '../../../components/section/header';
|
|
17
16
|
import SubscriptionItemList from '../../../components/subscription/items';
|
|
18
17
|
import SubscriptionStatus from '../../../components/subscription/status';
|
|
@@ -38,125 +37,123 @@ export default function CustomerSubscription() {
|
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
return (
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
<Stack direction="row"
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<SubscriptionStatus subscription={data} sx={{ ml: 1 }} />
|
|
61
|
-
</Stack>
|
|
40
|
+
<Root direction="column" spacing={4} sx={{ mb: 4 }}>
|
|
41
|
+
<Box>
|
|
42
|
+
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
|
|
43
|
+
<Link to="/customer">
|
|
44
|
+
<Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal', mt: '16px' }}>
|
|
45
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
46
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
47
|
+
{t('customer.subscriptions.title')}
|
|
48
|
+
</Typography>
|
|
49
|
+
</Stack>
|
|
50
|
+
</Link>
|
|
51
|
+
</Stack>
|
|
52
|
+
<Box mt={2}>
|
|
53
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
54
|
+
<Stack direction="row" alignItems="center">
|
|
55
|
+
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
|
56
|
+
{formatSubscriptionProduct(data.items)}
|
|
57
|
+
</Typography>
|
|
58
|
+
<SubscriptionStatus subscription={data} sx={{ ml: 1 }} />
|
|
62
59
|
</Stack>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
</Stack>
|
|
61
|
+
<Stack
|
|
62
|
+
className="section-body"
|
|
63
|
+
direction="row"
|
|
64
|
+
spacing={3}
|
|
65
|
+
justifyContent="flex-start"
|
|
66
|
+
flexWrap="wrap"
|
|
67
|
+
sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
|
|
68
|
+
<InfoMetric
|
|
69
|
+
label={t('admin.subscription.startedAt')}
|
|
70
|
+
value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
|
|
71
|
+
divider
|
|
72
|
+
/>
|
|
73
|
+
{!data.cancel_at && (
|
|
70
74
|
<InfoMetric
|
|
71
|
-
label={t('admin.subscription.
|
|
72
|
-
value={formatTime(data.
|
|
75
|
+
label={t('admin.subscription.nextInvoice')}
|
|
76
|
+
value={formatTime(data.current_period_end * 1000)}
|
|
73
77
|
divider
|
|
74
78
|
/>
|
|
75
|
-
{!data.cancel_at && (
|
|
76
|
-
<InfoMetric
|
|
77
|
-
label={t('admin.subscription.nextInvoice')}
|
|
78
|
-
value={formatTime(data.current_period_end * 1000)}
|
|
79
|
-
divider
|
|
80
|
-
/>
|
|
81
|
-
)}
|
|
82
|
-
{data.cancel_at && (
|
|
83
|
-
<InfoMetric
|
|
84
|
-
label={t('admin.subscription.cancel.schedule')}
|
|
85
|
-
value={formatTime(data.cancel_at * 1000)}
|
|
86
|
-
divider
|
|
87
|
-
/>
|
|
88
|
-
)}
|
|
89
|
-
</Stack>
|
|
90
|
-
</Box>
|
|
91
|
-
</Box>
|
|
92
|
-
|
|
93
|
-
<Box className="section">
|
|
94
|
-
<SectionHeader title={t('admin.details')} />
|
|
95
|
-
<Stack>
|
|
96
|
-
<InfoRow
|
|
97
|
-
label={t('common.customer')}
|
|
98
|
-
value={<Link to={`/admin/customers/${data.customer.id}`}>{data.customer.name}</Link>}
|
|
99
|
-
/>
|
|
100
|
-
<InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
101
|
-
{data.status === 'paused' && !!data.pause_collection?.resumes_at && (
|
|
102
|
-
<InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
|
|
103
79
|
)}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
/>
|
|
110
|
-
<InfoRow
|
|
111
|
-
label={t('admin.subscription.trialingPeriod')}
|
|
112
|
-
value={
|
|
113
|
-
data.trail_end && data.trail_start
|
|
114
|
-
? [formatTime(data.trail_start * 1000), formatTime(data.trail_end * 1000)].join(' ~ ')
|
|
115
|
-
: ''
|
|
116
|
-
}
|
|
117
|
-
/>
|
|
118
|
-
<InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
|
|
119
|
-
<InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
|
|
120
|
-
<InfoRow
|
|
121
|
-
label={t('admin.paymentMethod._name')}
|
|
122
|
-
value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
|
|
123
|
-
/>
|
|
124
|
-
<InfoRow
|
|
125
|
-
label={t('admin.paymentCurrency.name')}
|
|
126
|
-
value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
|
|
127
|
-
/>
|
|
128
|
-
{data.payment_details?.arcblock?.tx_hash && (
|
|
129
|
-
<InfoRow
|
|
130
|
-
label={t('common.txHash')}
|
|
131
|
-
value={<TxLink details={data.payment_details} method={data.paymentMethod} />}
|
|
80
|
+
{data.cancel_at && (
|
|
81
|
+
<InfoMetric
|
|
82
|
+
label={t('admin.subscription.cancel.schedule')}
|
|
83
|
+
value={formatTime(data.cancel_at * 1000)}
|
|
84
|
+
divider
|
|
132
85
|
/>
|
|
133
86
|
)}
|
|
134
87
|
</Stack>
|
|
135
88
|
</Box>
|
|
89
|
+
</Box>
|
|
136
90
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
91
|
+
<Box className="section">
|
|
92
|
+
<SectionHeader title={t('admin.details')} />
|
|
93
|
+
<Stack>
|
|
94
|
+
<InfoRow
|
|
95
|
+
label={t('common.customer')}
|
|
96
|
+
value={<Link to={`/admin/customers/${data.customer.id}`}>{data.customer.name}</Link>}
|
|
97
|
+
/>
|
|
98
|
+
<InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
99
|
+
{data.status === 'paused' && !!data.pause_collection?.resumes_at && (
|
|
100
|
+
<InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
|
|
101
|
+
)}
|
|
102
|
+
<InfoRow
|
|
103
|
+
label={t('admin.subscription.currentPeriod')}
|
|
104
|
+
value={[formatTime(data.current_period_start * 1000), formatTime(data.current_period_end * 1000)].join(
|
|
105
|
+
' ~ '
|
|
106
|
+
)}
|
|
107
|
+
/>
|
|
108
|
+
<InfoRow
|
|
109
|
+
label={t('admin.subscription.trialingPeriod')}
|
|
110
|
+
value={
|
|
111
|
+
data.trail_end && data.trail_start
|
|
112
|
+
? [formatTime(data.trail_start * 1000), formatTime(data.trail_end * 1000)].join(' ~ ')
|
|
113
|
+
: ''
|
|
114
|
+
}
|
|
115
|
+
/>
|
|
116
|
+
<InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
|
|
117
|
+
<InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
|
|
118
|
+
<InfoRow
|
|
119
|
+
label={t('admin.paymentMethod._name')}
|
|
120
|
+
value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
|
|
121
|
+
/>
|
|
122
|
+
<InfoRow
|
|
123
|
+
label={t('admin.paymentCurrency.name')}
|
|
124
|
+
value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
|
|
125
|
+
/>
|
|
126
|
+
{data.payment_details?.arcblock?.tx_hash && (
|
|
127
|
+
<InfoRow
|
|
128
|
+
label={t('common.txHash')}
|
|
129
|
+
value={<TxLink details={data.payment_details} method={data.paymentMethod} />}
|
|
155
130
|
/>
|
|
156
|
-
|
|
131
|
+
)}
|
|
132
|
+
</Stack>
|
|
133
|
+
</Box>
|
|
134
|
+
|
|
135
|
+
<Box className="section">
|
|
136
|
+
<SectionHeader title={t('admin.product.pricing')} mb={0} />
|
|
137
|
+
<Box className="section-body">
|
|
138
|
+
<SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="customer" />
|
|
139
|
+
</Box>
|
|
140
|
+
</Box>
|
|
141
|
+
<Box className="section">
|
|
142
|
+
<SectionHeader title={t('admin.invoices')} mb={0} />
|
|
143
|
+
<Box className="section-body">
|
|
144
|
+
<InvoiceList
|
|
145
|
+
features={{ customer: false, toolbar: false }}
|
|
146
|
+
subscription_id={data.id}
|
|
147
|
+
invoiceProps={{
|
|
148
|
+
onClick: (invoice) => {
|
|
149
|
+
navigate(`/customer/invoice/${invoice.id}`);
|
|
150
|
+
},
|
|
151
|
+
}}
|
|
152
|
+
mode="customer"
|
|
153
|
+
/>
|
|
157
154
|
</Box>
|
|
158
|
-
</
|
|
159
|
-
</
|
|
155
|
+
</Box>
|
|
156
|
+
</Root>
|
|
160
157
|
);
|
|
161
158
|
}
|
|
162
159
|
|