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.
@@ -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
- const count: number = await Invoice.count({
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
- const result = await isDelegationSufficientForPayment({
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: invoice.attempt_count + 1,
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(invoice.attempt_count + 1);
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: invoice.attempt_count + 1,
235
+ attempt_count: attemptCount,
238
236
  attempted: true,
239
237
  next_payment_attempt: retryAt,
240
238
  });
@@ -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[] = [
@@ -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
  },
@@ -72,6 +72,7 @@ export default flat({
72
72
  noToken: '您的账户没有任何代币,请充值代币后重新续费',
73
73
  noEnoughToken: '您的账户代币余额为 {balance},不足 {price},请充值代币后重新续费',
74
74
  noSupported: '不支持使用代币续费,请检查您的套餐',
75
+ txSendFailed: '扣款交易发送失败',
75
76
  },
76
77
  },
77
78
  },
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.70
17
+ version: 1.13.72
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.70",
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.70",
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": "f5fbe3d9d64d7ccdcdcc9dc2df791ab7665e64a3"
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 key="customer-home" path="/customer" element={<CustomerHome />} />,
52
- <Route key="customer-subscription" path="/customer/subscription/:id" element={<CustomerSubscription />} />
53
- <Route key="customer-invoice" path="/customer/invoice/:id" element={<CustomerInvoice />} />,
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 '../contexts/session';
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
- useEffect(() => {
57
- if (!session.user && !loggingIn) {
58
- setLoggingIn(true);
59
- session.login(() => {
60
- setLoggingIn(false);
61
- });
62
- }
63
- }, [session.user]);
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' }} />;
@@ -10,6 +10,7 @@ export default flat({
10
10
  actions: 'Actions',
11
11
  options: 'Options',
12
12
  advanced: 'Advanced options',
13
+ login: 'Login to access this page',
13
14
  settings: 'Settings',
14
15
  preview: 'Preview',
15
16
  required: 'Required',
@@ -15,6 +15,7 @@ export default flat({
15
15
  required: '必填',
16
16
  setup: '设置',
17
17
  name: '姓名',
18
+ login: '登录以访问此页面',
18
19
  amount: '金额',
19
20
  total: '总计',
20
21
  status: '状态',
@@ -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
- <Layout>
45
- <Alert sx={{ mt: 3 }} severity="info">
46
- {t('customer.empty')}
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
- <Layout>
68
- <Grid container spacing={5}>
69
- <Grid item xs={12} md={8}>
70
- <Root direction="column" spacing={3} sx={{ my: 2 }}>
71
- <Box className="section">
72
- <SectionHeader title={t('customer.subscriptions.current')} mb={0} />
73
- <Box className="section-body">
74
- <CurrentSubscriptions
75
- id={data.id}
76
- onChange={runAsync}
77
- style={{
78
- cursor: 'pointer',
79
- }}
80
- onClickSubscription={(subscription) => {
81
- navigate(`/customer/subscription/${subscription.id}`);
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
- <Box className="section">
87
- <SectionHeader title={t('customer.invoices')} mb={0} />
88
- <Box className="section-body">
89
- <CustomerInvoiceList customer_id={data.id} />
90
- </Box>
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
- </Root>
93
- </Grid>
94
- <Grid item xs={12} md={4}>
95
- <Root direction="column" spacing={4} sx={{ my: 2 }}>
96
- <Box className="section">
97
- <SectionHeader title={t('customer.details')}>
98
- <Button
99
- variant="outlined"
100
- color="inherit"
101
- size="small"
102
- disabled={state.editing || state.loading}
103
- onClick={() => setState({ editing: true })}>
104
- <Edit fontSize="small" sx={{ mr: 0.5 }} />
105
- {t('customer.update')}
106
- </Button>
107
- </SectionHeader>
108
- <Stack>
109
- <InfoRow sizes={[1, 2]} label={t('admin.customer.name')} value={data.name} />
110
- <InfoRow sizes={[1, 2]} label={t('admin.customer.phone')} value={data.phone} />
111
- <InfoRow sizes={[1, 2]} label={t('admin.customer.email')} value={data.email} />
112
- <InfoRow
113
- sizes={[1, 2]}
114
- label={t('admin.customer.address.country')}
115
- value={
116
- data.address?.country ? (
117
- <FlagEmoji iso2={data.address?.country} style={{ display: 'flex', width: 24 }} />
118
- ) : (
119
- ''
120
- )
121
- }
122
- />
123
- <InfoRow sizes={[1, 2]} label={t('admin.customer.address.state')} value={data.address?.state} />
124
- <InfoRow sizes={[1, 2]} label={t('admin.customer.address.city')} value={data.address?.city} />
125
- <InfoRow sizes={[1, 2]} label={t('admin.customer.address.line1')} value={data.address?.line1} />
126
- <InfoRow sizes={[1, 2]} label={t('admin.customer.address.line2')} value={data.address?.line2} />
127
- <InfoRow
128
- sizes={[1, 2]}
129
- label={t('admin.customer.address.postal_code')}
130
- value={data.address?.postal_code}
131
- />
132
- </Stack>
133
- {state.editing && (
134
- <EditCustomer
135
- data={data}
136
- loading={state.loading}
137
- onSave={onUpdateInfo}
138
- onCancel={() => setState({ editing: false })}
139
- />
140
- )}
141
- </Box>
142
- </Root>
143
- </Grid>
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
- </Layout>
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
- <Layout>
74
- <Grid container spacing={3} sx={{ mt: 1 }}>
75
- <Grid item xs={12} md={12}>
76
- <Link to="/customer">
77
- <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
78
- <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
79
- <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
80
- {t('common.previous')}
81
- </Typography>
82
- </Stack>
83
- </Link>
84
- </Grid>
85
- <Grid item xs={12} md={5}>
86
- <Box>
87
- <SectionHeader title={t('customer.invoice.summary')} mb={0} />
88
- <Stack sx={{ mt: 1 }}>
89
- <InfoRow label={t('admin.invoice.number')} value={data.number} />
90
- <InfoRow label={t('admin.invoice.from')} value={data.statement_descriptor || blocklet.appName} />
91
- <InfoRow label={t('admin.invoice.customer')} value={data.customer.name} />
92
- <InfoRow
93
- label={t('common.status')}
94
- value={<Status label={data.status} color={getInvoiceStatusColor(data.status)} />}
95
- />
96
- <InfoRow
97
- label={t('admin.subscription.currentPeriod')}
98
- value={
99
- data.period_start && data.period_end
100
- ? [formatTime(data.period_start * 1000), formatTime(data.period_end * 1000)].join(' ~ ')
101
- : ''
102
- }
103
- />
104
- {data.status_transitions?.paid_at && (
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
- </Grid>
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
- </Layout>
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
- <Layout>
42
- <Root direction="column" spacing={4} sx={{ mb: 4 }}>
43
- <Box>
44
- <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
45
- <Link to="/customer">
46
- <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal', mt: '16px' }}>
47
- <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
48
- <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
49
- {t('customer.subscriptions.title')}
50
- </Typography>
51
- </Stack>
52
- </Link>
53
- </Stack>
54
- <Box mt={2}>
55
- <Stack direction="row" justifyContent="space-between" alignItems="center">
56
- <Stack direction="row" alignItems="center">
57
- <Typography variant="h5" sx={{ fontWeight: 600 }}>
58
- {formatSubscriptionProduct(data.items)}
59
- </Typography>
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
- <Stack
64
- className="section-body"
65
- direction="row"
66
- spacing={3}
67
- justifyContent="flex-start"
68
- flexWrap="wrap"
69
- sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
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.startedAt')}
72
- value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
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
- <InfoRow
105
- label={t('admin.subscription.currentPeriod')}
106
- value={[formatTime(data.current_period_start * 1000), formatTime(data.current_period_end * 1000)].join(
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
- <Box className="section">
138
- <SectionHeader title={t('admin.product.pricing')} mb={0} />
139
- <Box className="section-body">
140
- <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="customer" />
141
- </Box>
142
- </Box>
143
- <Box className="section">
144
- <SectionHeader title={t('admin.invoices')} mb={0} />
145
- <Box className="section-body">
146
- <InvoiceList
147
- features={{ customer: false, toolbar: false }}
148
- subscription_id={data.id}
149
- invoiceProps={{
150
- onClick: (invoice) => {
151
- navigate(`/customer/invoice/${invoice.id}`);
152
- },
153
- }}
154
- mode="customer"
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
- </Box>
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
- </Root>
159
- </Layout>
155
+ </Box>
156
+ </Root>
160
157
  );
161
158
  }
162
159