payment-kit 1.17.11 → 1.17.12

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,6 +6,7 @@ import pick from 'lodash/pick';
6
6
  import { InferAttributes, Op, WhereOptions } from 'sequelize';
7
7
  import cloneDeep from 'lodash/cloneDeep';
8
8
  import merge from 'lodash/merge';
9
+ import { Joi } from '@arcblock/validator';
9
10
  import { ensureWebhookRegistered } from '../integrations/stripe/setup';
10
11
  import logger from '../libs/logger';
11
12
  import { authenticate } from '../libs/security';
@@ -14,6 +15,7 @@ import { PaymentMethod, TPaymentMethod } from '../store/models/payment-method';
14
15
  import type { EVMChainType, PaymentMethodSettings } from '../store/models/types';
15
16
  import { ethWallet, wallet } from '../libs/auth';
16
17
  import { EVM_CHAIN_TYPES } from '../libs/constants';
18
+ import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
17
19
 
18
20
  const router = Router();
19
21
 
@@ -179,15 +181,28 @@ router.get('/', auth, async (req, res) => {
179
181
  if (typeof query.livemode === 'string') {
180
182
  where.livemode = JSON.parse(query.livemode);
181
183
  }
182
- const list = await PaymentMethod.findAll({
183
- where,
184
- order: [['created_at', 'ASC']],
185
- include: [{ model: PaymentCurrency, as: 'payment_currencies', order: [['created_at', 'ASC']] }],
186
- });
187
- if (query.addresses === 'true') {
188
- res.json({ list, addresses: { arcblock: wallet.address, ethereum: ethWallet.address } });
189
- } else {
190
- res.json(list);
184
+ try {
185
+ const list = await PaymentMethod.findAll({
186
+ where,
187
+ order: [['created_at', 'ASC']],
188
+ include: [{ model: PaymentCurrency, as: 'payment_currencies', order: [['created_at', 'ASC']] }],
189
+ });
190
+ if (query.addresses === 'true') {
191
+ const [arcblock, ethereum] = await Promise.all([
192
+ getTokenSummaryByDid(wallet.address, !!req.livemode, 'arcblock'),
193
+ getTokenSummaryByDid(ethWallet.address, !!req.livemode, EVM_CHAIN_TYPES),
194
+ ]);
195
+ res.json({
196
+ list,
197
+ addresses: { arcblock: wallet.address, ethereum: ethWallet.address },
198
+ balances: { ...arcblock, ...ethereum },
199
+ });
200
+ } else {
201
+ res.json(list);
202
+ }
203
+ } catch (err) {
204
+ logger.error('get payment methods failed', err);
205
+ res.status(400).json({ error: err.message });
191
206
  }
192
207
  });
193
208
 
@@ -255,4 +270,36 @@ router.put('/:id/settings', auth, async (req, res) => {
255
270
  }
256
271
  });
257
272
 
273
+ const updateMethodSchema = Joi.object({
274
+ name: Joi.string().empty('').optional(),
275
+ description: Joi.string().empty('').optional(),
276
+ logo: Joi.string().empty('').optional(),
277
+ }).unknown(true);
278
+ router.put('/:id', auth, async (req, res) => {
279
+ const { id } = req.params;
280
+ const { error, value: raw } = updateMethodSchema.validate(pick(req.body, ['name', 'description', 'logo']));
281
+ if (error) {
282
+ return res.status(400).json({ error: error.message });
283
+ }
284
+
285
+ try {
286
+ const method = await PaymentMethod.findByPk(id);
287
+ if (!method) {
288
+ return res.status(404).json({ error: 'Payment method not found' });
289
+ }
290
+ const updateData: Partial<TPaymentMethod> = {
291
+ name: raw.name ?? method.name,
292
+ description: raw.description ?? method.description,
293
+ };
294
+
295
+ if ('logo' in method.dataValues || raw.logo !== undefined) {
296
+ updateData.logo = raw.logo ?? method.logo;
297
+ }
298
+ const updatedMethod = await method.update(updateData);
299
+ return res.json(updatedMethod);
300
+ } catch (err) {
301
+ return res.status(400).json({ error: err.message });
302
+ }
303
+ });
304
+
258
305
  export default router;
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.17.11
17
+ version: 1.17.12
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.17.11",
3
+ "version": "1.17.12",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -53,7 +53,7 @@
53
53
  "@arcblock/validator": "^1.19.3",
54
54
  "@blocklet/js-sdk": "^1.16.37",
55
55
  "@blocklet/logger": "^1.16.37",
56
- "@blocklet/payment-react": "1.17.11",
56
+ "@blocklet/payment-react": "1.17.12",
57
57
  "@blocklet/sdk": "^1.16.37",
58
58
  "@blocklet/ui-react": "^2.11.27",
59
59
  "@blocklet/uploader": "^0.1.64",
@@ -121,7 +121,7 @@
121
121
  "devDependencies": {
122
122
  "@abtnode/types": "^1.16.37",
123
123
  "@arcblock/eslint-config-ts": "^0.3.3",
124
- "@blocklet/payment-types": "1.17.11",
124
+ "@blocklet/payment-types": "1.17.12",
125
125
  "@types/cookie-parser": "^1.4.7",
126
126
  "@types/cors": "^2.8.17",
127
127
  "@types/debug": "^4.1.12",
@@ -167,5 +167,5 @@
167
167
  "parser": "typescript"
168
168
  }
169
169
  },
170
- "gitHead": "286c7709d37515078269b963b1e7381f599111cc"
170
+ "gitHead": "da4588c387abaed2109c591b8e08661ae90c8f3c"
171
171
  }
@@ -40,7 +40,8 @@ export default function IconCollapse(props: Props) {
40
40
  direction="row"
41
41
  alignItems="center"
42
42
  justifyContent="space-between"
43
- onClick={() => {
43
+ onClick={(e) => {
44
+ e.stopPropagation();
44
45
  props.onChange?.(props.value || '', !expanded);
45
46
  toggleExpanded();
46
47
  }}
@@ -6,7 +6,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
6
6
 
7
7
  import Uploader from '../uploader';
8
8
 
9
- export default function ArcBlockMethodForm() {
9
+ export default function ArcBlockMethodForm({ checkDisabled }: { checkDisabled: (key: string) => boolean }) {
10
10
  const { t } = useLocaleContext();
11
11
  const { control, setValue } = useFormContext();
12
12
  const logo = useWatch({ control, name: 'logo' });
@@ -29,6 +29,7 @@ export default function ArcBlockMethodForm() {
29
29
  rules={{ required: true }}
30
30
  label={t('admin.paymentMethod.name.label')}
31
31
  placeholder={t('admin.paymentMethod.name.tip')}
32
+ disabled={checkDisabled('name')}
32
33
  />
33
34
  <FormInput
34
35
  key="description"
@@ -37,6 +38,7 @@ export default function ArcBlockMethodForm() {
37
38
  rules={{ required: true }}
38
39
  label={t('admin.paymentMethod.description.label')}
39
40
  placeholder={t('admin.paymentMethod.description.tip')}
41
+ disabled={checkDisabled('description')}
40
42
  />
41
43
  <FormInput
42
44
  key="secret_key"
@@ -45,6 +47,7 @@ export default function ArcBlockMethodForm() {
45
47
  rules={{ required: true }}
46
48
  label={t('admin.paymentMethod.arcblock.chain_id.label')}
47
49
  placeholder={t('admin.paymentMethod.arcblock.chain_id.tip')}
50
+ disabled={checkDisabled('settings.arcblock.chain_id')}
48
51
  />
49
52
  <FormInput
50
53
  key="api_host"
@@ -53,6 +56,7 @@ export default function ArcBlockMethodForm() {
53
56
  rules={{ required: true }}
54
57
  label={t('admin.paymentMethod.arcblock.api_host.label')}
55
58
  placeholder={t('admin.paymentMethod.arcblock.api_host.tip')}
59
+ disabled={checkDisabled('settings.arcblock.api_host')}
56
60
  />
57
61
  <FormInput
58
62
  key="explorer_host"
@@ -61,10 +65,11 @@ export default function ArcBlockMethodForm() {
61
65
  rules={{ required: true }}
62
66
  label={t('admin.paymentMethod.arcblock.explorer_host.label')}
63
67
  placeholder={t('admin.paymentMethod.arcblock.explorer_host.tip')}
68
+ disabled={checkDisabled('settings.arcblock.explorer_host')}
64
69
  />
65
70
  <Stack direction="column">
66
71
  <Typography mb={1}>{t('admin.paymentCurrency.logo.label')}</Typography>
67
- <Uploader onUploaded={onUploaded} preview={logo} />
72
+ <Uploader onUploaded={onUploaded} preview={logo} disabled={checkDisabled('logo')} />
68
73
  </Stack>
69
74
  </>
70
75
  );
@@ -7,7 +7,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
7
7
  import Uploader from '../uploader';
8
8
  import EvmRpcInput from './evm-rpc-input';
9
9
 
10
- export default function BaseMethodForm() {
10
+ export default function BaseMethodForm({ checkDisabled }: { checkDisabled: (key: string) => boolean }) {
11
11
  const { t } = useLocaleContext();
12
12
  const { control, setValue } = useFormContext();
13
13
  const logo = useWatch({ control, name: 'logo' });
@@ -30,6 +30,7 @@ export default function BaseMethodForm() {
30
30
  rules={{ required: true }}
31
31
  label={t('admin.paymentMethod.name.label')}
32
32
  placeholder={t('admin.paymentMethod.name.tip')}
33
+ disabled={checkDisabled('name')}
33
34
  />
34
35
  <FormInput
35
36
  key="description"
@@ -38,11 +39,13 @@ export default function BaseMethodForm() {
38
39
  rules={{ required: true }}
39
40
  label={t('admin.paymentMethod.description.label')}
40
41
  placeholder={t('admin.paymentMethod.description.tip')}
42
+ disabled={checkDisabled('description')}
41
43
  />
42
44
  <EvmRpcInput
43
45
  name="settings.base.api_host"
44
46
  label={t('admin.paymentMethod.base.api_host.label')}
45
47
  placeholder={t('admin.paymentMethod.base.api_host.tip')}
48
+ disabled={checkDisabled('settings.base.api_host')}
46
49
  />
47
50
  <FormInput
48
51
  key="explorer_host"
@@ -51,6 +54,7 @@ export default function BaseMethodForm() {
51
54
  rules={{ required: true }}
52
55
  label={t('admin.paymentMethod.base.explorer_host.label')}
53
56
  placeholder={t('admin.paymentMethod.base.explorer_host.tip')}
57
+ disabled={checkDisabled('settings.base.explorer_host')}
54
58
  />
55
59
  <FormInput
56
60
  key="native_symbol"
@@ -59,6 +63,7 @@ export default function BaseMethodForm() {
59
63
  rules={{ required: true }}
60
64
  label={t('admin.paymentMethod.base.native_symbol.label')}
61
65
  placeholder={t('admin.paymentMethod.base.native_symbol.tip')}
66
+ disabled={checkDisabled('settings.base.native_symbol')}
62
67
  />
63
68
  <FormInput
64
69
  key="confirmation"
@@ -67,10 +72,11 @@ export default function BaseMethodForm() {
67
72
  rules={{ required: true }}
68
73
  label={t('admin.paymentMethod.base.confirmation.label')}
69
74
  placeholder={t('admin.paymentMethod.base.confirmation.tip')}
75
+ disabled={checkDisabled('settings.base.confirmation')}
70
76
  />
71
77
  <Stack direction="column">
72
78
  <Typography mb={1}>{t('admin.paymentCurrency.logo.label')}</Typography>
73
- <Uploader onUploaded={onUploaded} preview={logo} />
79
+ <Uploader onUploaded={onUploaded} preview={logo} disabled={checkDisabled('logo')} />
74
80
  </Stack>
75
81
  </>
76
82
  );
@@ -6,7 +6,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
6
6
 
7
7
  import Uploader from '../uploader';
8
8
 
9
- export default function BitcoinMethodForm() {
9
+ export default function BitcoinMethodForm({ checkDisabled }: { checkDisabled: (key: string) => boolean }) {
10
10
  const { t } = useLocaleContext();
11
11
  const { control, setValue } = useFormContext();
12
12
  const logo = useWatch({ control, name: 'logo' });
@@ -29,6 +29,7 @@ export default function BitcoinMethodForm() {
29
29
  rules={{ required: true }}
30
30
  label={t('admin.paymentMethod.name.label')}
31
31
  placeholder={t('admin.paymentMethod.name.tip')}
32
+ disabled={checkDisabled('name')}
32
33
  />
33
34
  <FormInput
34
35
  key="description"
@@ -37,6 +38,7 @@ export default function BitcoinMethodForm() {
37
38
  rules={{ required: true }}
38
39
  label={t('admin.paymentMethod.description.label')}
39
40
  placeholder={t('admin.paymentMethod.description.tip')}
41
+ disabled={checkDisabled('description')}
40
42
  />
41
43
  <FormInput
42
44
  key="chain_id"
@@ -45,6 +47,7 @@ export default function BitcoinMethodForm() {
45
47
  rules={{ required: true }}
46
48
  label={t('admin.paymentMethod.bitcoin.chain_id.label')}
47
49
  placeholder={t('admin.paymentMethod.bitcoin.chain_id.tip')}
50
+ disabled={checkDisabled('settings.bitcoin.chain_id')}
48
51
  />
49
52
  <FormInput
50
53
  key="api_host"
@@ -53,6 +56,7 @@ export default function BitcoinMethodForm() {
53
56
  rules={{ required: true }}
54
57
  label={t('admin.paymentMethod.bitcoin.api_host.label')}
55
58
  placeholder={t('admin.paymentMethod.bitcoin.api_host.tip')}
59
+ disabled={checkDisabled('settings.bitcoin.api_host')}
56
60
  />
57
61
  <FormInput
58
62
  key="explorer_host"
@@ -61,10 +65,11 @@ export default function BitcoinMethodForm() {
61
65
  rules={{ required: true }}
62
66
  label={t('admin.paymentMethod.bitcoin.explorer_host.label')}
63
67
  placeholder={t('admin.paymentMethod.bitcoin.explorer_host.tip')}
68
+ disabled={checkDisabled('settings.bitcoin.explorer_host')}
64
69
  />
65
70
  <Stack direction="column">
66
71
  <Typography mb={1}>{t('admin.paymentCurrency.logo.label')}</Typography>
67
- <Uploader onUploaded={onUploaded} preview={logo} />
72
+ <Uploader onUploaded={onUploaded} preview={logo} disabled={checkDisabled('logo')} />
68
73
  </Stack>
69
74
  </>
70
75
  );
@@ -7,7 +7,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
7
7
  import Uploader from '../uploader';
8
8
  import EvmRpcInput from './evm-rpc-input';
9
9
 
10
- export default function EthereumMethodForm() {
10
+ export default function EthereumMethodForm({ checkDisabled }: { checkDisabled: (key: string) => boolean }) {
11
11
  const { t } = useLocaleContext();
12
12
  const { control, setValue } = useFormContext();
13
13
  const logo = useWatch({ control, name: 'logo' });
@@ -30,6 +30,7 @@ export default function EthereumMethodForm() {
30
30
  rules={{ required: true }}
31
31
  label={t('admin.paymentMethod.name.label')}
32
32
  placeholder={t('admin.paymentMethod.name.tip')}
33
+ disabled={checkDisabled('name')}
33
34
  />
34
35
  <FormInput
35
36
  key="description"
@@ -38,11 +39,13 @@ export default function EthereumMethodForm() {
38
39
  rules={{ required: true }}
39
40
  label={t('admin.paymentMethod.description.label')}
40
41
  placeholder={t('admin.paymentMethod.description.tip')}
42
+ disabled={checkDisabled('description')}
41
43
  />
42
44
  <EvmRpcInput
43
45
  name="settings.ethereum.api_host"
44
46
  label={t('admin.paymentMethod.ethereum.api_host.label')}
45
47
  placeholder={t('admin.paymentMethod.ethereum.api_host.tip')}
48
+ disabled={checkDisabled('settings.ethereum.api_host')}
46
49
  />
47
50
  <FormInput
48
51
  key="explorer_host"
@@ -51,6 +54,7 @@ export default function EthereumMethodForm() {
51
54
  rules={{ required: true }}
52
55
  label={t('admin.paymentMethod.ethereum.explorer_host.label')}
53
56
  placeholder={t('admin.paymentMethod.ethereum.explorer_host.tip')}
57
+ disabled={checkDisabled('settings.ethereum.explorer_host')}
54
58
  />
55
59
  <FormInput
56
60
  key="native_symbol"
@@ -59,6 +63,7 @@ export default function EthereumMethodForm() {
59
63
  rules={{ required: true }}
60
64
  label={t('admin.paymentMethod.ethereum.native_symbol.label')}
61
65
  placeholder={t('admin.paymentMethod.ethereum.native_symbol.tip')}
66
+ disabled={checkDisabled('settings.ethereum.native_symbol')}
62
67
  />
63
68
  <FormInput
64
69
  key="confirmation"
@@ -67,10 +72,11 @@ export default function EthereumMethodForm() {
67
72
  rules={{ required: true }}
68
73
  label={t('admin.paymentMethod.ethereum.confirmation.label')}
69
74
  placeholder={t('admin.paymentMethod.ethereum.confirmation.tip')}
75
+ disabled={checkDisabled('settings.ethereum.confirmation')}
70
76
  />
71
77
  <Stack direction="column">
72
78
  <Typography mb={1}>{t('admin.paymentCurrency.logo.label')}</Typography>
73
- <Uploader onUploaded={onUploaded} preview={logo} />
79
+ <Uploader onUploaded={onUploaded} preview={logo} disabled={checkDisabled('logo')} />
74
80
  </Stack>
75
81
  </>
76
82
  );
@@ -10,9 +10,14 @@ interface Props {
10
10
  name: string; // 表单字段名
11
11
  label: string;
12
12
  placeholder: string;
13
+ disabled?: boolean;
13
14
  }
14
15
 
15
- export default function EvmRpcInput({ name, label, placeholder }: Props) {
16
+ EvmRpcInput.defaultProps = {
17
+ disabled: false,
18
+ };
19
+
20
+ export default function EvmRpcInput({ name, label, placeholder, disabled }: Props) {
16
21
  const { t } = useLocaleContext();
17
22
  const { control } = useFormContext();
18
23
  const apiHost = useWatch({
@@ -28,6 +33,7 @@ export default function EvmRpcInput({ name, label, placeholder }: Props) {
28
33
  name={name}
29
34
  type="text"
30
35
  rules={{ required: true }}
36
+ disabled={disabled}
31
37
  label={
32
38
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
33
39
  {label}
@@ -9,12 +9,30 @@ import EthereumMethodForm from './ethereum';
9
9
  import StripeMethodForm from './stripe';
10
10
  import BaseMethodForm from './base';
11
11
 
12
- export default function PaymentMethodForm() {
12
+ PaymentMethodForm.defaultProps = {
13
+ action: 'create',
14
+ activeKeys: [],
15
+ };
16
+
17
+ export default function PaymentMethodForm({
18
+ action,
19
+ activeKeys,
20
+ }: {
21
+ action?: 'create' | 'edit';
22
+ activeKeys?: string[];
23
+ }) {
13
24
  const { t } = useLocaleContext();
14
25
  const { control, setValue } = useFormContext();
15
26
 
16
27
  const type = useWatch({ control, name: 'type' });
17
28
 
29
+ const checkDisabled = (key: string) => {
30
+ if (action === 'edit') {
31
+ return !activeKeys?.includes(key);
32
+ }
33
+ return false;
34
+ };
35
+
18
36
  return (
19
37
  <Root direction="column" alignItems="flex-start" spacing={2}>
20
38
  <Controller
@@ -23,6 +41,7 @@ export default function PaymentMethodForm() {
23
41
  render={({ field }) => (
24
42
  <ToggleButtonGroup
25
43
  {...field}
44
+ disabled={checkDisabled(field.name)}
26
45
  onChange={(_, value: string) => {
27
46
  if (value !== null) {
28
47
  setValue(field.name, value);
@@ -42,11 +61,11 @@ export default function PaymentMethodForm() {
42
61
  <Typography variant="h6" sx={{ mb: 3, fontWeight: 600 }}>
43
62
  {t('admin.paymentMethod.settings')}
44
63
  </Typography>
45
- {type === 'stripe' && <StripeMethodForm />}
46
- {type === 'arcblock' && <ArcBlockMethodForm />}
47
- {type === 'ethereum' && <EthereumMethodForm />}
48
- {type === 'base' && <BaseMethodForm />}
49
- {type === 'bitcoin' && <BitcoinMethodForm />}
64
+ {type === 'stripe' && <StripeMethodForm checkDisabled={checkDisabled} />}
65
+ {type === 'arcblock' && <ArcBlockMethodForm checkDisabled={checkDisabled} />}
66
+ {type === 'ethereum' && <EthereumMethodForm checkDisabled={checkDisabled} />}
67
+ {type === 'base' && <BaseMethodForm checkDisabled={checkDisabled} />}
68
+ {type === 'bitcoin' && <BitcoinMethodForm checkDisabled={checkDisabled} />}
50
69
  </Root>
51
70
  );
52
71
  }
@@ -2,7 +2,7 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { FormInput } from '@blocklet/payment-react';
4
4
 
5
- export default function StripeMethodForm() {
5
+ export default function StripeMethodForm({ checkDisabled }: { checkDisabled: (key: string) => boolean }) {
6
6
  const { t } = useLocaleContext();
7
7
 
8
8
  return (
@@ -13,6 +13,7 @@ export default function StripeMethodForm() {
13
13
  rules={{ required: true }}
14
14
  label={t('admin.paymentMethod.name.label')}
15
15
  placeholder={t('admin.paymentMethod.name.tip')}
16
+ disabled={checkDisabled('name')}
16
17
  />
17
18
  <FormInput
18
19
  name="description"
@@ -27,6 +28,7 @@ export default function StripeMethodForm() {
27
28
  rules={{ required: true }}
28
29
  label={t('admin.paymentMethod.stripe.dashboard.label')}
29
30
  placeholder={t('admin.paymentMethod.stripe.dashboard.tip')}
31
+ disabled={checkDisabled('settings.stripe.dashboard')}
30
32
  />
31
33
  <FormInput
32
34
  name="settings.stripe.publishable_key"
@@ -34,6 +36,7 @@ export default function StripeMethodForm() {
34
36
  rules={{ required: true }}
35
37
  label={t('admin.paymentMethod.stripe.publishable_key.label')}
36
38
  placeholder={t('admin.paymentMethod.stripe.publishable_key.tip')}
39
+ disabled={checkDisabled('settings.stripe.publishable_key')}
37
40
  />
38
41
  <FormInput
39
42
  name="settings.stripe.secret_key"
@@ -41,6 +44,7 @@ export default function StripeMethodForm() {
41
44
  rules={{ required: true }}
42
45
  label={t('admin.paymentMethod.stripe.secret_key.label')}
43
46
  placeholder={t('admin.paymentMethod.stripe.secret_key.tip')}
47
+ disabled={checkDisabled('settings.stripe.secret_key')}
44
48
  />
45
49
  </>
46
50
  );
@@ -16,9 +16,17 @@ type Props = {
16
16
  maxFileSize?: number;
17
17
  maxNumberOfFiles?: number;
18
18
  allowedFileExts?: string[];
19
+ disabled?: boolean;
19
20
  };
20
21
 
21
- export default function Uploader({ onUploaded, preview, maxFileSize, maxNumberOfFiles, allowedFileExts }: Props) {
22
+ export default function Uploader({
23
+ onUploaded,
24
+ preview,
25
+ maxFileSize,
26
+ maxNumberOfFiles,
27
+ allowedFileExts,
28
+ disabled,
29
+ }: Props) {
22
30
  const uploaderRef = useRef<any>(null);
23
31
  const handleOpen = useCallback(() => {
24
32
  if (!uploaderRef.current) return;
@@ -74,7 +82,7 @@ export default function Uploader({ onUploaded, preview, maxFileSize, maxNumberOf
74
82
  display="flex"
75
83
  alignItems="center"
76
84
  justifyContent="center"
77
- onClick={handleOpen}
85
+ onClick={disabled ? undefined : handleOpen}
78
86
  sx={{
79
87
  position: 'relative',
80
88
  cursor: 'pointer',
@@ -117,10 +125,15 @@ export default function Uploader({ onUploaded, preview, maxFileSize, maxNumberOf
117
125
  },
118
126
  }}>
119
127
  <Stack direction="row">
120
- <Button variant="text" onClick={handleOpen} startIcon={<Edit />} sx={{ minWidth: 20, color: '#fff' }} />
121
128
  <Button
122
129
  variant="text"
123
- onClick={handleRemove}
130
+ onClick={disabled ? undefined : handleOpen}
131
+ startIcon={<Edit />}
132
+ sx={{ minWidth: 20, color: '#fff' }}
133
+ />
134
+ <Button
135
+ variant="text"
136
+ onClick={disabled ? undefined : handleRemove}
124
137
  startIcon={<Delete />}
125
138
  sx={{ minWidth: 20, color: '#fff' }}
126
139
  />
@@ -139,6 +152,7 @@ Uploader.defaultProps = {
139
152
  maxFileSize: undefined,
140
153
  maxNumberOfFiles: 1,
141
154
  allowedFileExts: ['.png', '.jpeg', '.webp'],
155
+ disabled: false,
142
156
  };
143
157
 
144
158
  const Div = styled(Box)`
@@ -322,12 +322,13 @@ export default flat({
322
322
  _name: 'Payment Method',
323
323
  type: 'Type',
324
324
  add: 'Add payment method',
325
+ edit: 'Edit payment method',
325
326
  save: 'Save payment method',
326
327
  saved: 'Payment method successfully saved',
327
328
  settings: 'Settings',
328
329
  gasTip:
329
330
  'Ensure your account on the {chain} network has sufficient balance to cover transaction fees when using {method}.',
330
- showQR: 'Show QR Code',
331
+ recharge: 'Scan to add balance',
331
332
  props: {
332
333
  type: 'Type',
333
334
  confirmation: 'Confirmation',
@@ -335,6 +336,8 @@ export default flat({
335
336
  refund: 'Refund support',
336
337
  dispute: 'Dispute support',
337
338
  currencies: 'Currency support',
339
+ explorer_host: 'Explorer Host',
340
+ balance: 'Balance',
338
341
  },
339
342
  name: {
340
343
  label: 'Name',
@@ -312,11 +312,12 @@ export default flat({
312
312
  _name: '支付方式',
313
313
  type: '类型',
314
314
  add: '添加支付方式',
315
+ edit: '编辑支付方式',
315
316
  save: '保存支付方式',
316
317
  saved: '支付方式已成功保存',
317
318
  settings: '设置',
318
319
  gasTip: '使用 {method} 支付需保证账户在 {chain} 链上有余额支付手续费',
319
- showQR: '显示二维码',
320
+ recharge: '扫码充值',
320
321
  props: {
321
322
  type: '类型',
322
323
  confirmation: '确认',
@@ -324,6 +325,8 @@ export default flat({
324
325
  refund: '退款支持',
325
326
  dispute: '纠纷支持',
326
327
  currencies: '货币支持',
328
+ explorer_host: '区块浏览器',
329
+ balance: '余额',
327
330
  },
328
331
  name: {
329
332
  label: '名称',
@@ -209,6 +209,7 @@ export default function CustomerDetail(props: { id: string }) {
209
209
  data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
210
210
  52
211
211
  )}
212
+ alt={data.customer.name}
212
213
  variant="square"
213
214
  sx={{ width: 52, height: 52, borderRadius: 'var(--radius-s, 4px)' }}
214
215
  />
@@ -66,6 +66,7 @@ export default function CustomersList() {
66
66
  48
67
67
  )}
68
68
  variant="square"
69
+ alt={item?.name}
69
70
  sx={{ borderRadius: 'var(--radius-m, 8px)' }}
70
71
  />
71
72
  <Typography sx={{ wordBreak: 'break-all' }}>{item.name}</Typography>
@@ -10,6 +10,7 @@ import omit from 'lodash/omit';
10
10
  import { useEffect } from 'react';
11
11
  import { Link } from 'react-router-dom';
12
12
 
13
+ import { ArrowForward } from '@mui/icons-material';
13
14
  import Chart, { TCurrencyMap } from '../../components/chart';
14
15
  import dayjs from '../../libs/dayjs';
15
16
  import { stringToColor } from '../../libs/util';
@@ -277,14 +278,26 @@ export default function Overview() {
277
278
  href={summary.data?.links[currencyId] as string}
278
279
  target="_blank"
279
280
  variant="outlined"
280
- sx={{ padding: 1 }}>
281
- <Stack direction="row" alignItems="center">
281
+ sx={{
282
+ padding: 1,
283
+ transition: 'all 0.2s ease-in-out',
284
+ position: 'relative',
285
+ '&:hover': {
286
+ backgroundColor: 'action.hover',
287
+ boxShadow: 1,
288
+ '& .MuiSvgIcon-root': {
289
+ opacity: 1,
290
+ transform: 'translateX(0)',
291
+ },
292
+ },
293
+ }}>
294
+ <Stack direction="row" alignItems="center" spacing={1}>
282
295
  <Avatar
283
296
  src={currencies[currencyId]?.logo}
284
- alt={currencies[currencyId]?.name}
285
- sx={{ width: 36, height: 36, marginRight: 1 }}
297
+ alt={currencies[currencyId]?.symbol}
298
+ sx={{ width: 36, height: 36 }}
286
299
  />
287
- <Box>
300
+ <Box flex={1}>
288
301
  <Typography variant="h5" component="div" sx={{ fontSize: '2rem' }}>
289
302
  {formatBNStr(
290
303
  summary.data?.balances?.[currencyId] as string,
@@ -295,6 +308,14 @@ export default function Overview() {
295
308
  {currencies[currencyId]?.symbol} on {currencies[currencyId]?.method.name}
296
309
  </Typography>
297
310
  </Box>
311
+ <ArrowForward
312
+ sx={{
313
+ opacity: 0,
314
+ transform: 'translateX(-10px)',
315
+ transition: 'all 0.2s ease-in-out',
316
+ color: 'text.secondary',
317
+ }}
318
+ />
298
319
  </Stack>
299
320
  </Card>
300
321
  ))}
@@ -0,0 +1,70 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import { api, formatError } from '@blocklet/payment-react';
5
+ import type { TPaymentMethod } from '@blocklet/payment-types';
6
+ import { AddOutlined } from '@mui/icons-material';
7
+ import { Button, CircularProgress } from '@mui/material';
8
+ import { useSetState } from 'ahooks';
9
+ import { FormProvider, useForm } from 'react-hook-form';
10
+ import { dispatch } from 'use-bus';
11
+
12
+ import DrawerForm from '../../../../components/drawer-form';
13
+ import PaymentMethodForm from '../../../../components/payment-method/form';
14
+
15
+ export default function PaymentMethodEdit({ onClose, value }: { onClose: () => void; value: TPaymentMethod | null }) {
16
+ const { t } = useLocaleContext();
17
+ const [state, setState] = useSetState({ loading: false });
18
+
19
+ const methods = useForm<TPaymentMethod>({
20
+ defaultValues: {
21
+ type: value?.type,
22
+ name: value?.name,
23
+ description: value?.description,
24
+ logo: value?.logo,
25
+ settings: value?.settings,
26
+ },
27
+ });
28
+ const { handleSubmit } = methods;
29
+
30
+ const onSubmit = (data: TPaymentMethod) => {
31
+ setState({ loading: true });
32
+ api
33
+ .put(`/api/payment-methods/${value?.id}`, data)
34
+ .then(() => {
35
+ setState({ loading: false });
36
+ Toast.success(t('admin.paymentMethod.saved'));
37
+ methods.reset();
38
+ dispatch('drawer.submitted');
39
+ dispatch('paymentMethod.updated');
40
+ })
41
+ .catch((err) => {
42
+ setState({ loading: false });
43
+ console.error(err);
44
+ Toast.error(formatError(err));
45
+ });
46
+ };
47
+
48
+ return (
49
+ <DrawerForm
50
+ open
51
+ icon={<AddOutlined />}
52
+ onClose={onClose}
53
+ text={t('admin.paymentMethod.edit')}
54
+ width={640}
55
+ footer={
56
+ <Button
57
+ variant="contained"
58
+ size="large"
59
+ onClick={handleSubmit(onSubmit)}
60
+ disabled={state.loading}
61
+ sx={{ width: '100%' }}>
62
+ {state.loading && <CircularProgress size={20} />} {t('common.save')}
63
+ </Button>
64
+ }>
65
+ <FormProvider {...methods}>
66
+ <PaymentMethodForm action="edit" activeKeys={['name', 'description', 'logo']} />
67
+ </FormProvider>
68
+ </DrawerForm>
69
+ );
70
+ }
@@ -1,6 +1,6 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { ConfirmDialog, Switch, api, formatError, usePaymentContext } from '@blocklet/payment-react';
3
- import type { TPaymentCurrency, TPaymentMethodExpanded } from '@blocklet/payment-types';
3
+ import type { ChainType, TPaymentCurrency, TPaymentMethodExpanded } from '@blocklet/payment-types';
4
4
  import {
5
5
  AddOutlined,
6
6
  Check,
@@ -11,12 +11,14 @@ import {
11
11
  QrCodeOutlined,
12
12
  CheckCircleOutline,
13
13
  ErrorOutline,
14
+ OpenInNewOutlined,
14
15
  } from '@mui/icons-material';
15
16
  import {
16
17
  Alert,
17
18
  Avatar,
18
19
  Box,
19
20
  CircularProgress,
21
+ Divider,
20
22
  Grid,
21
23
  IconButton,
22
24
  List,
@@ -34,16 +36,23 @@ import useBus from 'use-bus';
34
36
  import { useState } from 'react';
35
37
  import Toast from '@arcblock/ux/lib/Toast';
36
38
  import { DIDDialog } from '@arcblock/ux/lib/DID';
39
+ import { fromUnitToToken } from '@ocap/util';
40
+ import { joinURL } from 'ufo';
37
41
  import IconCollapse from '../../../../components/collapse';
38
42
  import InfoCard from '../../../../components/info-card';
39
43
  import InfoRow from '../../../../components/info-row';
40
44
  import PaymentCurrencyAdd from '../../../../components/payment-currency/add';
41
45
  import PaymentCurrencyEdit from '../../../../components/payment-currency/edit';
42
46
  import { useRpcStatus } from '../../../../hooks/rpc-status';
47
+ import PaymentMethodEdit from './edit';
43
48
 
44
49
  const getMethods = (
45
50
  params: Record<string, any> = {}
46
- ): Promise<{ list: TPaymentMethodExpanded[]; addresses: { arcblock: string; ethereum: string } }> => {
51
+ ): Promise<{
52
+ list: TPaymentMethodExpanded[];
53
+ addresses: { arcblock: string; ethereum: string };
54
+ balances: { [currencyId: string]: string };
55
+ }> => {
47
56
  const search = new URLSearchParams();
48
57
  Object.keys(params).forEach((key) => {
49
58
  search.set(key, String(params[key]));
@@ -155,9 +164,10 @@ function RpcStatus({ method }: { method: TPaymentMethodExpanded }) {
155
164
  const renderStatus = () => {
156
165
  if (status.loading) {
157
166
  return (
158
- <Typography variant="caption" color="text.secondary">
159
- {t('admin.paymentMethod.evm.checking')}
160
- </Typography>
167
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
168
+ <CircularProgress size={14} />
169
+ <Typography color="text.secondary">{t('admin.paymentMethod.evm.checking')}</Typography>
170
+ </Box>
161
171
  );
162
172
  }
163
173
  if (status.connected) {
@@ -184,6 +194,108 @@ function RpcStatus({ method }: { method: TPaymentMethodExpanded }) {
184
194
  );
185
195
  }
186
196
 
197
+ function Balance({
198
+ method,
199
+ balances,
200
+ addresses,
201
+ setDidDialog,
202
+ }: {
203
+ method: TPaymentMethodExpanded;
204
+ balances: { [currencyId: string]: string };
205
+ addresses: { arcblock: string; ethereum: string };
206
+ setDidDialog: (value: any) => void;
207
+ }) {
208
+ const { t } = useLocaleContext();
209
+ const defaultCurrency = (method.payment_currencies || [])?.find(
210
+ (x) => x.id === method.default_currency_id
211
+ ) as TPaymentCurrency;
212
+ const balance = fromUnitToToken(balances?.[defaultCurrency?.id] || '0', defaultCurrency?.decimal);
213
+ const explorerHost = (method?.settings?.[method?.type as ChainType] as any)?.explorer_host || '';
214
+ const getLink = () => {
215
+ if (method.type === 'arcblock' && addresses?.arcblock) {
216
+ return joinURL(explorerHost, 'accounts', addresses?.arcblock, 'tokens');
217
+ }
218
+ if (['ethereum', 'base'].includes(method.type) && addresses?.ethereum) {
219
+ return joinURL(explorerHost, 'address', addresses?.ethereum);
220
+ }
221
+ return '';
222
+ };
223
+ const insufficientBalance =
224
+ ['ethereum', 'base'].includes(method.type) &&
225
+ balances?.[defaultCurrency?.id] &&
226
+ balances?.[defaultCurrency?.id] === '0';
227
+ const getAddress = (type: string) => {
228
+ if (['ethereum', 'base'].includes(type)) {
229
+ return addresses?.ethereum || '';
230
+ }
231
+ return addresses?.arcblock || '';
232
+ };
233
+ return (
234
+ <>
235
+ <InfoRow label={t('admin.paymentMethod.props.explorer_host')} value={explorerHost} />
236
+ <InfoRow
237
+ label={
238
+ <Box display="flex" alignItems="center" gap={0.5}>
239
+ {t('admin.paymentMethod.props.balance')}
240
+ {['ethereum', 'base'].includes(method.type) && (
241
+ <Tooltip
242
+ title={t('admin.paymentMethod.gasTip', {
243
+ method: method.type,
244
+ chain: method.name,
245
+ })}>
246
+ <InfoOutlined
247
+ sx={{ fontSize: 16, cursor: 'pointer', color: insufficientBalance ? 'error.main' : 'text.secondary' }}
248
+ />
249
+ </Tooltip>
250
+ )}
251
+ </Box>
252
+ }
253
+ value={
254
+ <Stack
255
+ direction="row"
256
+ alignItems="center"
257
+ sx={{
258
+ display: 'inline-flex',
259
+ borderRadius: 1,
260
+ }}>
261
+ <Typography
262
+ component="a"
263
+ href={getLink()}
264
+ target="_blank"
265
+ rel="noreferrer"
266
+ sx={{
267
+ display: 'flex',
268
+ alignItems: 'center',
269
+ gap: 0.5,
270
+ '&.MuiTypography-root': {
271
+ color: insufficientBalance ? 'error.main' : 'text.link',
272
+ },
273
+ }}>
274
+ {balance} {defaultCurrency?.symbol}
275
+ <OpenInNewOutlined sx={{ fontSize: 14 }} />
276
+ </Typography>
277
+ <Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
278
+ <Tooltip title={t('admin.paymentMethod.recharge')}>
279
+ <IconButton
280
+ size="small"
281
+ onClick={() =>
282
+ setDidDialog({
283
+ open: true,
284
+ chainId: (method.settings?.[method.type as ChainType] as any)?.chain_id,
285
+ did: getAddress(method.type),
286
+ })
287
+ }
288
+ sx={{ p: 0.5 }}>
289
+ <QrCodeOutlined sx={{ fontSize: 16 }} />
290
+ </IconButton>
291
+ </Tooltip>
292
+ </Stack>
293
+ }
294
+ />
295
+ </>
296
+ );
297
+ }
298
+
187
299
  export default function PaymentMethods() {
188
300
  const { t } = useLocaleContext();
189
301
  const [expandedId, setExpandedId] = useSessionStorageState('payment-method-expanded-id', {
@@ -192,7 +304,7 @@ export default function PaymentMethods() {
192
304
  const {
193
305
  loading,
194
306
  error,
195
- data = { list: [], addresses: {} } as any,
307
+ data = { list: [], addresses: {}, balances: {} } as any,
196
308
  runAsync,
197
309
  } = useRequest(() =>
198
310
  getMethods({
@@ -203,10 +315,11 @@ export default function PaymentMethods() {
203
315
  setExpandedId(expanded ? methodId : '');
204
316
  };
205
317
  const [didDialog, setDidDialog] = useSetState({ open: false, chainId: '', did: '' });
318
+ const [methodDialog, setMethodDialog] = useSetState({ open: false, value: null });
206
319
  const { refresh } = usePaymentContext();
207
320
  const [currencyDialog, setCurrencyDialog] = useSetState({ action: '', value: null, method: '' });
208
321
 
209
- const { list: methods, addresses } = data;
322
+ const { list: methods, addresses, balances } = data;
210
323
  useBus(
211
324
  'paymentMethod.created',
212
325
  () => {
@@ -215,6 +328,14 @@ export default function PaymentMethods() {
215
328
  },
216
329
  []
217
330
  );
331
+ useBus(
332
+ 'paymentMethod.updated',
333
+ () => {
334
+ runAsync();
335
+ refresh(true);
336
+ },
337
+ []
338
+ );
218
339
  useBus(
219
340
  'paymentCurrency.added',
220
341
  () => {
@@ -246,13 +367,6 @@ export default function PaymentMethods() {
246
367
 
247
368
  const groups = groupByType(methods);
248
369
 
249
- const getAddress = (type: string) => {
250
- if (['ethereum', 'base'].includes(type)) {
251
- return addresses?.ethereum || '';
252
- }
253
- return addresses?.arcblock || '';
254
- };
255
-
256
370
  const handleDeleteCurrency = async (currency: TPaymentCurrency) => {
257
371
  try {
258
372
  await api.delete(`/api/payment-currencies/${currency.id}`);
@@ -275,45 +389,25 @@ export default function PaymentMethods() {
275
389
  <Typography variant="h6" sx={{ textTransform: 'uppercase' }}>
276
390
  {x}
277
391
  </Typography>
278
- {['ethereum', 'base'].includes(x) && (
279
- <Stack
280
- direction="row"
281
- alignItems="center"
282
- spacing={0.5}
283
- sx={{
284
- px: 0.5,
285
- color: 'text.secondary',
286
- }}>
287
- <InfoOutlined sx={{ fontSize: 16 }} color="warning" />
288
- <Typography variant="body2">
289
- {t('admin.paymentMethod.gasTip', {
290
- method: x,
291
- address: '222',
292
- chain: groups[x]?.[0]?.name || x,
293
- })}
294
- </Typography>
295
- <Tooltip title={t('admin.paymentMethod.showQR')}>
296
- <IconButton
297
- size="small"
298
- onClick={() =>
299
- setDidDialog({
300
- open: true,
301
- chainId: groups[x]?.[0]?.id || '',
302
- did: getAddress(x),
303
- })
304
- }
305
- sx={{ p: 0.5 }}>
306
- <QrCodeOutlined sx={{ fontSize: 16 }} />
307
- </IconButton>
308
- </Tooltip>
309
- </Stack>
310
- )}
311
392
  </Stack>
312
393
  {(groups[x] as TPaymentMethodExpanded[]).map((method) => (
313
394
  <IconCollapse
314
395
  key={method.id}
315
396
  trigger={<InfoCard {...method} />}
316
- addons={<Switch checked={method.active} />}
397
+ addons={
398
+ <>
399
+ <Switch checked={method.active} disabled sx={{ cursor: 'default' }} />
400
+ {method.type !== 'arcblock' && (
401
+ <IconButton
402
+ onClick={(e) => {
403
+ e.stopPropagation();
404
+ setMethodDialog({ open: true, value: method as any });
405
+ }}>
406
+ <EditOutlined />
407
+ </IconButton>
408
+ )}
409
+ </>
410
+ }
317
411
  style={{
318
412
  py: 1,
319
413
  borderTop: '1px solid #eee',
@@ -329,6 +423,10 @@ export default function PaymentMethods() {
329
423
  <InfoRow label={t('admin.paymentMethod.props.type')} value={method.type} />
330
424
  {method.type === 'arcblock' && <EditApiHost method={method} />}
331
425
  {['ethereum', 'base'].includes(method.type) && <RpcStatus method={method} />}
426
+ {['arcblock', 'ethereum', 'base'].includes(method.type) && (
427
+ <Balance method={method} balances={balances} addresses={addresses} setDidDialog={setDidDialog} />
428
+ )}
429
+
332
430
  <InfoRow label={t('admin.paymentMethod.props.confirmation')} value={method.confirmation.type} />
333
431
  <InfoRow
334
432
  label={t('admin.paymentMethod.props.recurring')}
@@ -382,7 +480,7 @@ export default function PaymentMethods() {
382
480
  )
383
481
  }>
384
482
  <ListItemAvatar>
385
- <Avatar src={currency.logo} alt={currency.name} />
483
+ <Avatar src={currency.logo} alt={currency.symbol} />
386
484
  </ListItemAvatar>
387
485
  <ListItemText primary={currency.name} secondary={currency.description} />
388
486
  </ListItem>
@@ -442,6 +540,9 @@ export default function PaymentMethods() {
442
540
  chainId={didDialog.chainId}
443
541
  />
444
542
  )}
543
+ {methodDialog.open && methodDialog.value && (
544
+ <PaymentMethodEdit value={methodDialog.value} onClose={() => setMethodDialog({ open: false, value: null })} />
545
+ )}
445
546
  </>
446
547
  );
447
548
  }
@@ -94,7 +94,14 @@ export default function CustomerHome() {
94
94
  const { settings } = usePaymentContext();
95
95
  const [currency, setCurrency] = useState(settings?.baseCurrency);
96
96
  const [subscriptionLoading, setSubscriptionLoading] = useState(false);
97
- const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
97
+ const currencies = flatten(
98
+ settings.paymentMethods.map((method) =>
99
+ (method.payment_currencies || []).map((c) => ({
100
+ ...c,
101
+ methodName: method.name,
102
+ }))
103
+ )
104
+ );
98
105
 
99
106
  const { livemode, setLivemode } = usePaymentContext();
100
107
  const [state, setState] = useSetState({
@@ -249,18 +256,17 @@ export default function CustomerHome() {
249
256
  {currencies.map((c) => (
250
257
  <MenuItem key={c.id} value={c.id}>
251
258
  <Box alignItems="center" display="flex" gap={1}>
252
- <Avatar src={c?.logo} alt={c?.name} sx={{ width: 18, height: 18 }} />
259
+ <Avatar src={c?.logo} alt={c?.symbol} sx={{ width: 18, height: 18 }} />
253
260
  <Typography
254
261
  variant="h5"
255
262
  component="div"
256
263
  sx={{ fontSize: '16px', color: 'text.primary', fontWeight: '500' }}>
257
- {c?.name}
264
+ {c?.symbol}
258
265
  </Typography>
259
266
  <Typography sx={{ fontSize: 12 }} color="text.lighter">
260
- {c?.symbol}
267
+ {c?.methodName}
261
268
  </Typography>
262
269
  </Box>
263
- {/* {c.symbol} */}
264
270
  </MenuItem>
265
271
  ))}
266
272
  </Select>
@@ -209,7 +209,7 @@ export default function CustomerSubscriptionDetail() {
209
209
  gap: 0.5,
210
210
  fontWeight: 500,
211
211
  }}>
212
- <Avatar src={data.paymentCurrency?.logo} sx={{ width: 16, height: 16 }} alt={data.paymentCurrency?.name} />
212
+ <Avatar src={data.paymentCurrency?.logo} sx={{ width: 16, height: 16 }} alt={data.paymentCurrency?.symbol} />
213
213
  <Box display="flex" alignItems="baseline">
214
214
  {formatBNStr(overdraftProtection?.unused, data.paymentCurrency.decimal)}
215
215
  <Typography
@@ -17,7 +17,12 @@ function Home() {
17
17
  />
18
18
  <Stack alignItems="center" justifyContent="center" sx={{ height: '60vh', width: '100vw' }}>
19
19
  <Stack maxWidth="sm" direction="column" alignItems="center" spacing={3}>
20
- <Avatar src={window.blocklet.appLogo} sx={{ width: 80, height: 80 }} variant="square" />
20
+ <Avatar
21
+ src={window.blocklet.appLogo}
22
+ sx={{ width: 80, height: 80 }}
23
+ variant="square"
24
+ alt={window.blocklet.appName || 'Payment Kit'}
25
+ />
21
26
  <Stack direction="column" alignItems="center" spacing={1}>
22
27
  <Typography variant="h4">Payment Kit</Typography>
23
28
  <Typography variant="h5" color="text.secondary" fontWeight="normal">