payment-kit 1.17.5 → 1.17.7

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.
@@ -78,7 +78,7 @@ export async function checkCurrencySupportRecurring(currencyIds: string[] | stri
78
78
  include: [{ model: PaymentMethod, as: 'payment_method' }],
79
79
  })) as (PaymentCurrency & { payment_method: PaymentMethod })[];
80
80
  const notSupportCurrencies = currencies.filter(
81
- (c) => EVM_CHAIN_TYPES.includes(c.payment_method?.type) && c.symbol === 'ETH'
81
+ (c) => EVM_CHAIN_TYPES.includes(c.payment_method?.type) && c.payment_method?.default_currency_id === c.id
82
82
  );
83
83
  return {
84
84
  notSupportCurrencies,
@@ -2,6 +2,7 @@ import { fromTokenToUnit } from '@ocap/util';
2
2
  import { Router } from 'express';
3
3
  import { InferAttributes, Op, WhereOptions } from 'sequelize';
4
4
 
5
+ import Joi from 'joi';
5
6
  import { fetchErc20Meta } from '../integrations/ethereum/token';
6
7
  import logger from '../libs/logger';
7
8
  import { authenticate } from '../libs/security';
@@ -151,4 +152,63 @@ router.get('/:id', auth, async (req, res) => {
151
152
  }
152
153
  });
153
154
 
155
+ const updateCurrencySchema = Joi.object({
156
+ name: Joi.string().empty('').optional(),
157
+ description: Joi.string().empty('').optional(),
158
+ logo: Joi.string().empty('').optional(),
159
+ }).unknown(true);
160
+ router.put('/:id', auth, async (req, res) => {
161
+ const { id } = req.params;
162
+ const raw: Partial<TPaymentCurrency> = req.body;
163
+
164
+ const { error } = updateCurrencySchema.validate(raw);
165
+ if (error) {
166
+ return res.status(400).json({ error: error.message });
167
+ }
168
+
169
+ const currency = await PaymentCurrency.findByPk(id);
170
+ if (!currency) {
171
+ return res.status(404).json({ error: 'Payment currency not found' });
172
+ }
173
+ if (raw.contract && raw.contract !== currency.contract) {
174
+ return res.status(400).json({ error: 'contract cannot be updated' });
175
+ }
176
+
177
+ const method = await PaymentMethod.findByPk(currency.payment_method_id);
178
+ if (!method) {
179
+ return res.status(400).json({ error: 'Payment method not found' });
180
+ }
181
+
182
+ const updatedCurrency = await currency.update({
183
+ name: raw.name || currency.name,
184
+ description: raw.description || currency.description,
185
+ logo: raw.logo || method.logo,
186
+ });
187
+ return res.json(updatedCurrency);
188
+ });
189
+
190
+ router.delete('/:id', auth, async (req, res) => {
191
+ const { id } = req.params;
192
+
193
+ const currency = await PaymentCurrency.findByPk(id);
194
+ if (!currency) {
195
+ return res.status(404).json({ error: 'Payment currency not found' });
196
+ }
197
+ const isLocked = await currency.isLocked();
198
+ if (isLocked) {
199
+ return res.status(400).json({ error: 'Can not delete locked payment currency' });
200
+ }
201
+ const isUsed = await currency.isUsed();
202
+ if (isUsed) {
203
+ return res.status(400).json({ error: 'Can not delete payment currency used by other resources' });
204
+ }
205
+ try {
206
+ await currency.destroy();
207
+ return res.status(200).end();
208
+ } catch (err) {
209
+ logger.error('delete payment currency error', err);
210
+ return res.status(400).json({ error: 'Delete payment currency failed' });
211
+ }
212
+ });
213
+
154
214
  export default router;
@@ -68,7 +68,7 @@ router.post('/', auth, async (req, res) => {
68
68
  const currency = await PaymentCurrency.create({
69
69
  livemode: method.livemode,
70
70
  active: method.active,
71
- locked: false,
71
+ locked: true,
72
72
  is_base_currency: false,
73
73
  payment_method_id: method.id,
74
74
 
@@ -144,7 +144,7 @@ router.post('/', auth, async (req, res) => {
144
144
  const currency = await PaymentCurrency.create({
145
145
  livemode: method.livemode,
146
146
  active: method.active,
147
- locked: false,
147
+ locked: true,
148
148
  is_base_currency: false,
149
149
  payment_method_id: method.id,
150
150
 
@@ -7,6 +7,7 @@ import {
7
7
  InferCreationAttributes,
8
8
  Model,
9
9
  Op,
10
+ QueryTypes,
10
11
  } from 'sequelize';
11
12
 
12
13
  import { createIdGenerator } from '../../libs/util';
@@ -145,6 +146,36 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
145
146
  ...options,
146
147
  });
147
148
  }
149
+
150
+ public async isLocked(): Promise<boolean> {
151
+ const { PaymentMethod } = this.sequelize.models;
152
+ const method = (await PaymentMethod!.findByPk(this.payment_method_id)) as any;
153
+ return this.locked || method?.default_currency_id === this.id;
154
+ }
155
+
156
+ public async isUsed(): Promise<boolean> {
157
+ const { Price } = this.sequelize.models;
158
+ const price = await Price!.findOne({
159
+ where: {
160
+ currency_id: this.id,
161
+ },
162
+ });
163
+ if (price) {
164
+ return true;
165
+ }
166
+ // @ts-ignore
167
+ const [{ count }] = await this.sequelize.query(
168
+ `SELECT count(p.id) AS count
169
+ FROM prices AS p
170
+ JOIN json_each(p.currency_options) AS option
171
+ ON json_extract(option.value, '$.currency_id') = ?`,
172
+ {
173
+ replacements: [this.id],
174
+ type: QueryTypes.SELECT,
175
+ }
176
+ );
177
+ return count > 0;
178
+ }
148
179
  }
149
180
 
150
181
  export type TPaymentCurrency = InferAttributes<PaymentCurrency>;
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.5
17
+ version: 1.17.7
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.5",
3
+ "version": "1.17.7",
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.5",
56
+ "@blocklet/payment-react": "1.17.7",
57
57
  "@blocklet/sdk": "^1.16.37",
58
58
  "@blocklet/ui-react": "^2.11.27",
59
59
  "@blocklet/uploader": "^0.1.64",
@@ -120,7 +120,7 @@
120
120
  "devDependencies": {
121
121
  "@abtnode/types": "^1.16.37",
122
122
  "@arcblock/eslint-config-ts": "^0.3.3",
123
- "@blocklet/payment-types": "1.17.5",
123
+ "@blocklet/payment-types": "1.17.7",
124
124
  "@types/cookie-parser": "^1.4.7",
125
125
  "@types/cors": "^2.8.17",
126
126
  "@types/debug": "^4.1.12",
@@ -166,5 +166,5 @@
166
166
  "parser": "typescript"
167
167
  }
168
168
  },
169
- "gitHead": "7713be8272f1056796820a9d52f742ed63899900"
169
+ "gitHead": "f2a439af19e405746a6663b4dbef8a96231a1076"
170
170
  }
@@ -1,20 +1,57 @@
1
- /* eslint-disable no-nested-ternary */
2
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
2
  import Toast from '@arcblock/ux/lib/Toast';
4
3
  import { api, formatError } from '@blocklet/payment-react';
5
- import type { TPaymentCurrency, TPaymentMethod } from '@blocklet/payment-types';
4
+ import type { TPaymentCurrency, TPaymentMethodExpanded } from '@blocklet/payment-types';
6
5
  import { AddOutlined } from '@mui/icons-material';
7
- import { Button, CircularProgress } from '@mui/material';
6
+ import {
7
+ Button,
8
+ CircularProgress,
9
+ TextField,
10
+ Autocomplete,
11
+ Box,
12
+ Typography,
13
+ Avatar,
14
+ Stack,
15
+ Divider,
16
+ } from '@mui/material';
8
17
  import { useSetState } from 'ahooks';
9
18
  import { FormProvider, useForm } from 'react-hook-form';
10
19
  import { dispatch } from 'use-bus';
11
-
20
+ import { useEffect } from 'react';
12
21
  import DrawerForm from '../drawer-form';
13
22
  import PaymentCurrencyForm from './form';
14
23
 
15
- export default function PaymentCurrencyAdd({ method, onClose }: { method: TPaymentMethod; onClose: Function }) {
24
+ const loadTokenList = () =>
25
+ import(
26
+ /* webpackChunkName: "payment-token-list" */
27
+ './tokenList.json'
28
+ );
29
+ interface TokenInfo {
30
+ address: string;
31
+ chainId: number;
32
+ decimals: number;
33
+ name: string;
34
+ symbol: string;
35
+ logoURI?: string;
36
+ }
37
+
38
+ export default function PaymentCurrencyAdd({
39
+ method,
40
+ onClose,
41
+ }: {
42
+ method: TPaymentMethodExpanded;
43
+ onClose: () => void;
44
+ }) {
16
45
  const { t } = useLocaleContext();
17
- const [state, setState] = useSetState({ loading: false });
46
+ const [state, setState] = useSetState<{
47
+ loading: boolean;
48
+ tokenListLoading: boolean;
49
+ availableTokens: TokenInfo[];
50
+ }>({
51
+ loading: false,
52
+ tokenListLoading: false,
53
+ availableTokens: [],
54
+ });
18
55
 
19
56
  const methods = useForm<TPaymentCurrency>({
20
57
  defaultValues: {
@@ -25,7 +62,39 @@ export default function PaymentCurrencyAdd({ method, onClose }: { method: TPayme
25
62
  contract: '',
26
63
  },
27
64
  });
28
- const { handleSubmit } = methods;
65
+ const { handleSubmit, setValue } = methods;
66
+
67
+ const showTokenSelect = ['ethereum', 'base'].includes(method.type);
68
+
69
+ // @ts-ignore
70
+ const chainId = method?.settings?.[method.type]?.chain_id || '';
71
+
72
+ useEffect(() => {
73
+ if (showTokenSelect && !state.availableTokens.length) {
74
+ setState({ tokenListLoading: true });
75
+ loadTokenList().then((module) => {
76
+ const availableTokens = showTokenSelect
77
+ ? // @ts-ignore
78
+ ((module.default?.[chainId] as TokenInfo[]) || []).filter(
79
+ (token: TokenInfo) => !(method.payment_currencies || []).find((c: any) => c.contract === token.address)
80
+ )
81
+ : [];
82
+ setState({
83
+ availableTokens,
84
+ tokenListLoading: false,
85
+ });
86
+ });
87
+ }
88
+ }, [showTokenSelect, chainId]);
89
+
90
+ const handleTokenSelect = (token: TokenInfo | null) => {
91
+ if (token) {
92
+ setValue('name', token.name);
93
+ setValue('description', `${token.name} (${token.symbol})`);
94
+ setValue('logo', token.logoURI || '');
95
+ setValue('contract', token.address);
96
+ }
97
+ };
29
98
 
30
99
  const onSubmit = (data: TPaymentCurrency) => {
31
100
  setState({ loading: true });
@@ -54,10 +123,64 @@ export default function PaymentCurrencyAdd({ method, onClose }: { method: TPayme
54
123
  width={640}
55
124
  addons={
56
125
  <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={state.loading}>
57
- {state.loading ? <CircularProgress size="small" /> : t('admin.paymentCurrency.save')}
126
+ {state.loading ? <CircularProgress size={20} /> : t('admin.paymentCurrency.save')}
58
127
  </Button>
59
128
  }>
60
129
  <FormProvider {...methods}>
130
+ {showTokenSelect && (
131
+ <Box sx={{ mb: 3 }}>
132
+ <Autocomplete
133
+ disablePortal
134
+ options={state.availableTokens}
135
+ loading={state.tokenListLoading}
136
+ getOptionLabel={(option: TokenInfo) => option.symbol}
137
+ onChange={(_, value) => handleTokenSelect(value)}
138
+ renderInput={(params) => (
139
+ <TextField
140
+ {...params}
141
+ label={t('admin.paymentCurrency.quickAdd')}
142
+ placeholder={t('admin.paymentCurrency.searchToken')}
143
+ fullWidth
144
+ />
145
+ )}
146
+ renderOption={(props, option) => (
147
+ <Box
148
+ component="li"
149
+ {...props}
150
+ sx={{
151
+ display: 'flex',
152
+ alignItems: 'center',
153
+ gap: 1,
154
+ cursor: 'pointer',
155
+ '&:hover': {
156
+ backgroundColor: 'action.hover',
157
+ },
158
+ }}>
159
+ <Avatar
160
+ src={option.logoURI || option.symbol}
161
+ alt={option.symbol}
162
+ sx={{
163
+ width: 24,
164
+ height: 24,
165
+ }}
166
+ />
167
+ <Stack>
168
+ <Typography fontWeight={500}>{option.name}</Typography>
169
+ <Typography variant="caption" color="text.secondary">
170
+ {option.symbol}
171
+ </Typography>
172
+ </Stack>
173
+ </Box>
174
+ )}
175
+ isOptionEqualToValue={(option, value) => option.address === value.address}
176
+ />
177
+ <Divider sx={{ my: 3 }}>
178
+ <Typography variant="caption" color="text.secondary">
179
+ {t('admin.paymentCurrency.orManualInput')}
180
+ </Typography>
181
+ </Divider>
182
+ </Box>
183
+ )}
61
184
  <PaymentCurrencyForm />
62
185
  </FormProvider>
63
186
  </DrawerForm>
@@ -0,0 +1,73 @@
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 { TPaymentCurrency, 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 '../drawer-form';
13
+ import PaymentCurrencyForm from './form';
14
+
15
+ export default function PaymentCurrencyEdit({
16
+ method,
17
+ onClose,
18
+ value,
19
+ }: {
20
+ method: TPaymentMethod;
21
+ onClose: () => void;
22
+ value: TPaymentCurrency;
23
+ }) {
24
+ const { t } = useLocaleContext();
25
+ const [state, setState] = useSetState({ loading: false });
26
+
27
+ const methods = useForm<TPaymentCurrency>({
28
+ defaultValues: {
29
+ payment_method_id: method.id,
30
+ name: value?.name,
31
+ description: value?.description,
32
+ logo: value?.logo,
33
+ contract: value?.contract,
34
+ },
35
+ });
36
+ const { handleSubmit } = methods;
37
+
38
+ const onSubmit = (data: TPaymentCurrency) => {
39
+ setState({ loading: true });
40
+ api
41
+ .put(`/api/payment-currencies/${value.id}`, data)
42
+ .then(() => {
43
+ setState({ loading: false });
44
+ Toast.success(t('admin.paymentCurrency.saved'));
45
+ methods.reset();
46
+ dispatch('drawer.submitted');
47
+ dispatch('paymentCurrency.updated');
48
+ })
49
+ .catch((err) => {
50
+ setState({ loading: false });
51
+ console.error(err);
52
+ Toast.error(formatError(err));
53
+ });
54
+ };
55
+
56
+ return (
57
+ <DrawerForm
58
+ open
59
+ icon={<AddOutlined />}
60
+ onClose={onClose}
61
+ text={t('admin.paymentCurrency.edit')}
62
+ width={640}
63
+ addons={
64
+ <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={state.loading}>
65
+ {state.loading ? <CircularProgress size="small" /> : t('admin.paymentCurrency.save')}
66
+ </Button>
67
+ }>
68
+ <FormProvider {...methods}>
69
+ <PaymentCurrencyForm disableKeys={['contract']} />
70
+ </FormProvider>
71
+ </DrawerForm>
72
+ );
73
+ }
@@ -6,7 +6,15 @@ import { useFormContext, useWatch } from 'react-hook-form';
6
6
 
7
7
  import Uploader from '../uploader';
8
8
 
9
- export default function PaymentCurrencyForm() {
9
+ type TPaymentCurrencyFormProps = {
10
+ disableKeys?: string[];
11
+ };
12
+
13
+ PaymentCurrencyForm.defaultProps = {
14
+ disableKeys: [],
15
+ };
16
+
17
+ export default function PaymentCurrencyForm({ disableKeys = [] }: TPaymentCurrencyFormProps) {
10
18
  const { t } = useLocaleContext();
11
19
  const { control, setValue } = useFormContext();
12
20
  const logo = useWatch({ control, name: 'logo' });
@@ -29,6 +37,7 @@ export default function PaymentCurrencyForm() {
29
37
  rules={{ required: true }}
30
38
  label={t('admin.paymentMethod.name.label')}
31
39
  placeholder={t('admin.paymentMethod.name.tip')}
40
+ disabled={disableKeys.includes('name')}
32
41
  />
33
42
  <FormInput
34
43
  key="description"
@@ -37,6 +46,7 @@ export default function PaymentCurrencyForm() {
37
46
  rules={{ required: true }}
38
47
  label={t('admin.paymentMethod.description.label')}
39
48
  placeholder={t('admin.paymentMethod.description.tip')}
49
+ disabled={disableKeys.includes('description')}
40
50
  />
41
51
  <FormInput
42
52
  key="contract"
@@ -45,6 +55,7 @@ export default function PaymentCurrencyForm() {
45
55
  rules={{ required: true }}
46
56
  label={t('admin.paymentCurrency.contract.label')}
47
57
  placeholder={t('admin.paymentCurrency.contract.tip')}
58
+ disabled={disableKeys.includes('contract')}
48
59
  />
49
60
  <Stack direction="column">
50
61
  <Typography mb={1}>{t('admin.paymentCurrency.logo.label')}</Typography>