payment-kit 1.15.4 → 1.15.5

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.
Files changed (30) hide show
  1. package/api/src/index.ts +3 -0
  2. package/api/src/integrations/blocklet/user.ts +30 -0
  3. package/api/src/libs/invoice.ts +41 -0
  4. package/api/src/libs/notification/template/customer-reward-succeeded.ts +41 -20
  5. package/api/src/libs/notification/template/subscription-renew-failed.ts +19 -1
  6. package/api/src/libs/notification/template/subscription-succeeded.ts +94 -9
  7. package/api/src/libs/notification/template/subscription-trial-start.ts +92 -18
  8. package/api/src/libs/notification/template/subscription-trial-will-end.ts +45 -15
  9. package/api/src/libs/notification/template/subscription-will-renew.ts +28 -11
  10. package/api/src/libs/util.ts +18 -1
  11. package/api/src/locales/en.ts +12 -3
  12. package/api/src/locales/zh.ts +12 -3
  13. package/api/src/queues/payment.ts +3 -1
  14. package/api/src/routes/donations.ts +1 -1
  15. package/api/src/routes/subscriptions.ts +30 -5
  16. package/api/src/routes/usage-records.ts +13 -4
  17. package/api/src/store/migrations/20240910-customer-sync.ts +21 -0
  18. package/api/src/store/models/customer.ts +5 -0
  19. package/blocklet.yml +1 -1
  20. package/package.json +9 -9
  21. package/scripts/sdk.js +25 -2
  22. package/src/components/payment-link/before-pay.tsx +41 -29
  23. package/src/components/pricing-table/product-settings.tsx +37 -25
  24. package/src/pages/admin/index.tsx +0 -1
  25. package/src/pages/admin/payments/intents/detail.tsx +14 -1
  26. package/src/pages/admin/payments/payouts/detail.tsx +6 -1
  27. package/src/pages/admin/products/pricing-tables/create.tsx +3 -0
  28. package/src/pages/checkout/pricing-table.tsx +26 -7
  29. package/src/pages/customer/index.tsx +3 -3
  30. package/src/pages/customer/invoice/past-due.tsx +14 -2
@@ -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
- {grouped.map((item) => {
119
- const [productId, prices] = item;
120
- // @ts-ignore
121
- const product = products.find((x) => x.id === productId);
122
- if (!product) {
123
- return null;
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}
@@ -48,7 +48,6 @@ function Admin() {
48
48
 
49
49
  const onLivemodeChange = (e: any) => {
50
50
  settings.setLivemode(!e.target.checked);
51
- settings.refresh();
52
51
  };
53
52
 
54
53
  const onTabChange = (newTab: string) => {
@@ -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 title={<pre>{JSON.stringify(data.last_payment_error, null, 2)}</pre>}>
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 title={<pre>{JSON.stringify(data.last_attempt_error, null, 2)}</pre>}>
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
  )}
@@ -60,6 +60,9 @@ export default function CreatePricingTable() {
60
60
  Toast.error(t('admin.paymentLink.noProducts'));
61
61
  return;
62
62
  }
63
+ if (Object.values(errors).some((v) => v)) {
64
+ return;
65
+ }
63
66
 
64
67
  api
65
68
  .post('/api/pricing-tables', data)
@@ -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
- <Box
32
+ <Stack
30
33
  sx={{
31
34
  pt: {
32
35
  xs: 0,
33
- md: '60px',
36
+ },
37
+ overflow: {
38
+ xs: isMobileSafariEnv ? 'visible' : 'hidden',
39
+ md: 'hidden',
34
40
  },
35
41
  }}>
36
- <CheckoutTable id={id} mode="standalone" />
37
- </Box>
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>