payment-kit 1.13.19 → 1.13.20

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.
@@ -1,13 +1,24 @@
1
1
  import { user } from '@blocklet/sdk/lib/middlewares';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
+ import pick from 'lodash/pick';
4
5
  import type { WhereOptions } from 'sequelize';
5
6
 
6
7
  import { authenticate } from '../libs/security';
8
+ import { formatMetadata } from '../libs/util';
7
9
  import { Customer } from '../store/models/customer';
8
10
 
9
11
  const router = Router();
10
12
  const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
13
+ const authPortal = authenticate<Customer>({
14
+ component: true,
15
+ roles: ['owner', 'admin'],
16
+ record: {
17
+ // @ts-ignore
18
+ model: Customer,
19
+ field: 'id',
20
+ },
21
+ });
11
22
 
12
23
  const schema = Joi.object<{
13
24
  page: number;
@@ -67,4 +78,25 @@ router.get('/:id', auth, async (req, res) => {
67
78
  }
68
79
  });
69
80
 
81
+ // eslint-disable-next-line consistent-return
82
+ router.put('/:id', authPortal, async (req, res) => {
83
+ try {
84
+ const doc = await Customer.findByPkOrDid(req.params.id as string);
85
+ if (!doc) {
86
+ return res.status(404).json({ error: 'Customer not found' });
87
+ }
88
+
89
+ const raw = pick(req.body, ['metadata', 'name', 'email', 'phone', 'address']);
90
+ if (raw.metadata) {
91
+ raw.metadata = formatMetadata(raw.metadata);
92
+ }
93
+
94
+ await doc.update(raw);
95
+ res.json(doc);
96
+ } catch (err) {
97
+ console.error(err);
98
+ res.json(null);
99
+ }
100
+ });
101
+
70
102
  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.13.19
17
+ version: 1.13.20
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.19",
3
+ "version": "1.13.20",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -100,7 +100,7 @@
100
100
  "devDependencies": {
101
101
  "@arcblock/eslint-config": "^0.2.4",
102
102
  "@arcblock/eslint-config-ts": "^0.2.4",
103
- "@did-pay/types": "1.13.19",
103
+ "@did-pay/types": "1.13.20",
104
104
  "@types/cookie-parser": "^1.4.4",
105
105
  "@types/cors": "^2.8.14",
106
106
  "@types/dotenv-flow": "^3.3.1",
@@ -137,5 +137,5 @@
137
137
  "parser": "typescript"
138
138
  }
139
139
  },
140
- "gitHead": "ffc58582731d2f1c1057346cee8127b26266ccd8"
140
+ "gitHead": "60955ccbcacc231990e00bacdc271a7c428adaf6"
141
141
  }
@@ -61,15 +61,13 @@ export default function AddressForm({ mode }: Props) {
61
61
  variant="outlined"
62
62
  placeholder={t('checkout.billing.line1')}
63
63
  />
64
- <Stack direction="row" spacing={0}>
65
- <FormInput
66
- name="billing_address.city"
67
- rules={{ required: t('checkout.required') }}
68
- errorPosition="right"
69
- variant="outlined"
70
- placeholder={t('checkout.billing.city')}
71
- />
72
- </Stack>
64
+ <FormInput
65
+ name="billing_address.city"
66
+ rules={{ required: t('checkout.required') }}
67
+ errorPosition="right"
68
+ variant="outlined"
69
+ placeholder={t('checkout.billing.city')}
70
+ />
73
71
  </Stack>
74
72
  </Stack>
75
73
  </Fade>
@@ -270,7 +270,7 @@ export default function PaymentForm({
270
270
  errorPosition="right"
271
271
  rules={{
272
272
  required: t('checkout.required'),
273
- validate: (x) => (isEmail(x) ? true : t('checkout.customer.emailInvalid')),
273
+ validate: (x) => (isEmail(x) ? true : t('checkout.invalid')),
274
274
  }}
275
275
  InputProps={{
276
276
  startAdornment: <InputAdornment position="start">{t('checkout.customer.email')}</InputAdornment>,
@@ -1,3 +1,4 @@
1
+ /* eslint-disable react/prop-types */
1
2
  import { InputAdornment, MenuItem, Select, Typography } from '@mui/material';
2
3
  import { useEffect } from 'react';
3
4
  import { useFormContext, useWatch } from 'react-hook-form';
@@ -5,18 +6,23 @@ import { CountryIso2, FlagEmoji, defaultCountries, parseCountry, usePhoneInput }
5
6
 
6
7
  import FormInput from '../../input';
7
8
 
9
+ const isValidCountry = (code: string) => defaultCountries.some((x) => x[1] === code);
10
+
8
11
  export default function PhoneInput({ ...props }) {
12
+ const countryFieldName = props.countryFieldName || 'billing_address.country';
13
+
9
14
  const { control, getValues, setValue } = useFormContext();
15
+ const values = getValues();
10
16
  const { phone, handlePhoneValueChange, inputRef, country, setCountry } = usePhoneInput({
11
- defaultCountry: 'us',
12
- value: getValues().customer_phone || '',
17
+ defaultCountry: isValidCountry(values[countryFieldName]) ? values[countryFieldName] : 'us',
18
+ value: values[props.name] || '',
13
19
  countries: defaultCountries,
14
20
  onChange: (data) => {
15
- setValue('customer_phone', data.phone);
21
+ setValue(props.name, data.phone);
16
22
  },
17
23
  });
18
24
 
19
- const userCountry = useWatch({ control, name: 'billing_address.country' });
25
+ const userCountry = useWatch({ control, name: countryFieldName });
20
26
 
21
27
  useEffect(() => {
22
28
  if (userCountry !== country) {
@@ -27,7 +33,7 @@ export default function PhoneInput({ ...props }) {
27
33
 
28
34
  const onCountryChange = (e: any) => {
29
35
  setCountry(e.target.value as CountryIso2);
30
- setValue('billing_address.country', e.target.value);
36
+ setValue(countryFieldName, e.target.value);
31
37
  };
32
38
 
33
39
  return (
@@ -0,0 +1,73 @@
1
+ import Dialog from '@arcblock/ux/lib/Dialog';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import type { TCustomer } from '@did-pay/types';
4
+ import { Button, CircularProgress, Stack } from '@mui/material';
5
+ import type { EventHandler } from 'react';
6
+ import { FormProvider, useForm } from 'react-hook-form';
7
+
8
+ import CustomerForm from './form';
9
+
10
+ export default function EditCustomer({
11
+ data,
12
+ loading,
13
+ onSave,
14
+ onCancel,
15
+ }: {
16
+ data: TCustomer;
17
+ loading: boolean;
18
+ onSave: EventHandler<any>;
19
+ onCancel: EventHandler<any>;
20
+ }) {
21
+ const { t } = useLocaleContext();
22
+ const methods = useForm<TCustomer>({
23
+ defaultValues: {
24
+ name: data.name || '',
25
+ email: data.email || '',
26
+ phone: data.phone || '',
27
+ address: Object.assign(
28
+ {
29
+ country: '',
30
+ state: '',
31
+ city: '',
32
+ line1: '',
33
+ line2: '',
34
+ postal_code: '',
35
+ },
36
+ data.address || {},
37
+ { country: data.address?.country || 'us' }
38
+ ),
39
+ },
40
+ });
41
+
42
+ const { handleSubmit, reset } = methods;
43
+ const onSubmit = async () => {
44
+ await handleSubmit(onSave)();
45
+ reset();
46
+ onCancel(null);
47
+ };
48
+
49
+ return (
50
+ <Dialog
51
+ open
52
+ disableEscapeKeyDown
53
+ fullWidth
54
+ maxWidth="sm"
55
+ onClose={() => onCancel(null)}
56
+ showCloseButton={false}
57
+ title={t('customer.update')}
58
+ actions={
59
+ <Stack direction="row">
60
+ <Button size="small" sx={{ mr: 2 }} onClick={onCancel}>
61
+ {t('common.cancel')}
62
+ </Button>
63
+ <Button variant="contained" color="primary" size="small" disabled={loading} onClick={onSubmit}>
64
+ {loading && <CircularProgress size="small" />} {t('common.save')}
65
+ </Button>
66
+ </Stack>
67
+ }>
68
+ <FormProvider {...methods}>
69
+ <CustomerForm />
70
+ </FormProvider>
71
+ </Dialog>
72
+ );
73
+ }
@@ -0,0 +1,104 @@
1
+ import 'react-international-phone/style.css';
2
+
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { InputAdornment, Stack, Typography } from '@mui/material';
5
+ import { PhoneNumberUtil } from 'google-libphonenumber';
6
+ import isEmail from 'validator/es/lib/isEmail';
7
+
8
+ import PhoneInput from '../checkout/form/phone';
9
+ import FormInput from '../input';
10
+
11
+ const phoneUtil = PhoneNumberUtil.getInstance();
12
+
13
+ export default function CustomerForm() {
14
+ const { t } = useLocaleContext();
15
+
16
+ return (
17
+ <Stack direction="column" spacing={3}>
18
+ <Typography component="h6" sx={{ mb: 1, color: 'text.primary', fontWeight: 600 }}>
19
+ {t('checkout.contact')}
20
+ </Typography>
21
+ <FormInput
22
+ name="name"
23
+ variant="outlined"
24
+ errorPosition="right"
25
+ rules={{
26
+ required: t('checkout.required'),
27
+ }}
28
+ InputProps={{
29
+ startAdornment: <InputAdornment position="start">{t('checkout.customer.name')}</InputAdornment>,
30
+ }}
31
+ />
32
+ <FormInput
33
+ name="email"
34
+ variant="outlined"
35
+ errorPosition="right"
36
+ rules={{
37
+ required: t('checkout.required'),
38
+ validate: (x) => (isEmail(x) ? true : t('checkout.invalid')),
39
+ }}
40
+ InputProps={{
41
+ startAdornment: <InputAdornment position="start">{t('checkout.customer.email')}</InputAdornment>,
42
+ }}
43
+ />
44
+ <PhoneInput
45
+ name="phone"
46
+ variant="outlined"
47
+ countryFieldName="address.country"
48
+ errorPosition="right"
49
+ placeholder={t('checkout.customer.phonePlaceholder')}
50
+ rules={{
51
+ required: t('checkout.required'),
52
+ validate: (x: string) => {
53
+ try {
54
+ const parsed = phoneUtil.parseAndKeepRawInput(x);
55
+ return phoneUtil.isValidNumber(parsed) ? true : t('checkout.invalid');
56
+ } catch (err) {
57
+ console.error(err, x);
58
+ return t('checkout.invalid');
59
+ }
60
+ },
61
+ }}
62
+ />
63
+
64
+ <Typography component="h6" sx={{ mb: 1, color: 'text.primary', fontWeight: 600 }}>
65
+ {t('checkout.billing.required')}
66
+ </Typography>
67
+ <FormInput
68
+ name="address.state"
69
+ rules={{}}
70
+ variant="outlined"
71
+ errorPosition="right"
72
+ placeholder={t('checkout.billing.state')}
73
+ />
74
+ <FormInput
75
+ name="address.city"
76
+ rules={{}}
77
+ variant="outlined"
78
+ errorPosition="right"
79
+ placeholder={t('checkout.billing.city')}
80
+ />
81
+ <FormInput
82
+ name="address.line1"
83
+ rules={{}}
84
+ variant="outlined"
85
+ errorPosition="right"
86
+ placeholder={t('checkout.billing.line1')}
87
+ />
88
+ <FormInput
89
+ name="address.line2"
90
+ rules={{}}
91
+ variant="outlined"
92
+ errorPosition="right"
93
+ placeholder={t('checkout.billing.line2')}
94
+ />
95
+ <FormInput
96
+ name="address.postal_code"
97
+ errorPosition="right"
98
+ rules={{ required: t('checkout.required') }}
99
+ variant="outlined"
100
+ placeholder={t('checkout.billing.postal_code')}
101
+ />
102
+ </Stack>
103
+ );
104
+ }
@@ -31,9 +31,8 @@ export default function MetadataEditor({
31
31
  reset();
32
32
  onCancel(e);
33
33
  };
34
- // eslint-disable-next-line @typescript-eslint/no-shadow
35
- const onSubmit = async (data: any) => {
36
- await handleSubmit(onSave)(data);
34
+ const onSubmit = async () => {
35
+ await handleSubmit(onSave)();
37
36
  reset();
38
37
  onCancel(null);
39
38
  };
@@ -9,7 +9,7 @@ export default function AfterPay() {
9
9
  const type = useWatch({ control, name: 'after_completion.type' });
10
10
 
11
11
  return (
12
- <Stack spacing={2}>
12
+ <Stack spacing={2} sx={{ width: '100%' }}>
13
13
  <Typography variant="h6" sx={{ fontWeight: 600 }}>
14
14
  {t('admin.paymentLink.confirmPage')}
15
15
  </Typography>
@@ -26,8 +26,8 @@ export default function RenamePaymentLink({
26
26
  });
27
27
 
28
28
  const { handleSubmit, reset } = methods;
29
- const onSubmit = async (data: any) => {
30
- await handleSubmit(onSave)(data);
29
+ const onSubmit = async () => {
30
+ await handleSubmit(onSave)();
31
31
  reset();
32
32
  onCancel(null);
33
33
  };
@@ -423,6 +423,7 @@ export default flat({
423
423
  name: 'Name',
424
424
  email: 'Email',
425
425
  phone: 'Phone',
426
+ phonePlaceholder: 'Phone number',
426
427
  phoneTip: 'In case we need to contact you about your order',
427
428
  },
428
429
  },
@@ -13,6 +13,7 @@ import { Link, useNavigate } from 'react-router-dom';
13
13
 
14
14
  import Copyable from '../../../../components/copyable';
15
15
  import CustomerActions from '../../../../components/customer/actions';
16
+ import EditCustomer from '../../../../components/customer/edit';
16
17
  import EventList from '../../../../components/event/list';
17
18
  import InfoMetric from '../../../../components/info-metric';
18
19
  import InfoRow from '../../../../components/info-row';
@@ -37,12 +38,11 @@ export default function CustomerDetail(props: { id: string }) {
37
38
  },
38
39
  editing: {
39
40
  metadata: false,
40
- product: false,
41
+ customer: false,
41
42
  },
42
43
  loading: {
43
44
  metadata: false,
44
- price: false,
45
- product: false,
45
+ customer: false,
46
46
  },
47
47
  });
48
48
 
@@ -56,9 +56,9 @@ export default function CustomerDetail(props: { id: string }) {
56
56
  return <CircularProgress />;
57
57
  }
58
58
 
59
- const createUpdater = (key: string) => async (updates: TCustomerExpanded) => {
59
+ const onUpdateMetadata = async (updates: TCustomerExpanded) => {
60
60
  try {
61
- setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
61
+ setState((prev) => ({ loading: { ...prev.loading, metadata: true } }));
62
62
  await api.put(`/api/customers/${props.id}`, updates).then((res) => res.data);
63
63
  Toast.success(t('common.saved'));
64
64
  runAsync();
@@ -66,11 +66,24 @@ export default function CustomerDetail(props: { id: string }) {
66
66
  console.error(err);
67
67
  Toast.error(formatError(err));
68
68
  } finally {
69
- setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
69
+ setState((prev) => ({ loading: { ...prev.loading, metadata: false } }));
70
+ }
71
+ };
72
+
73
+ const onUpdateInfo = async (updates: TCustomerExpanded) => {
74
+ try {
75
+ setState((prev) => ({ loading: { ...prev.loading, customer: true } }));
76
+ await api.put(`/api/customers/${props.id}`, updates).then((res) => res.data);
77
+ Toast.success(t('common.saved'));
78
+ runAsync();
79
+ } catch (err) {
80
+ console.error(err);
81
+ Toast.error(formatError(err));
82
+ } finally {
83
+ setState((prev) => ({ loading: { ...prev.loading, customer: false } }));
70
84
  }
71
85
  };
72
86
 
73
- const onUpdateMetadata = createUpdater('metadata');
74
87
  const onChange = (action: string) => {
75
88
  if (action === 'remove') {
76
89
  navigate('/admin/customers');
@@ -122,7 +135,8 @@ export default function CustomerDetail(props: { id: string }) {
122
135
  variant="outlined"
123
136
  color="inherit"
124
137
  size="small"
125
- onClick={() => setState((prev) => ({ editing: { ...prev.editing, product: true } }))}>
138
+ disabled={state.editing.customer}
139
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, customer: true } }))}>
126
140
  <Edit fontSize="small" sx={{ mr: 0.5 }} />
127
141
  {t('common.edit')}
128
142
  </Button>
@@ -157,6 +171,14 @@ export default function CustomerDetail(props: { id: string }) {
157
171
  </Stack>
158
172
  }
159
173
  />
174
+ {state.editing.customer && (
175
+ <EditCustomer
176
+ data={data}
177
+ loading={state.loading.customer}
178
+ onSave={onUpdateInfo}
179
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, customer: false } }))}
180
+ />
181
+ )}
160
182
  </Stack>
161
183
  </Box>
162
184
  <Box className="section">
@@ -1,4 +1,5 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
2
3
  import type { TCustomerExpanded } from '@did-pay/types';
3
4
  import { Edit } from '@mui/icons-material';
4
5
  import { Alert, Box, Button, CircularProgress, Grid, Stack } from '@mui/material';
@@ -6,6 +7,7 @@ import { styled } from '@mui/system';
6
7
  import { useRequest, useSetState } from 'ahooks';
7
8
  import { FlagEmoji } from 'react-international-phone';
8
9
 
10
+ import EditCustomer from '../../components/customer/edit';
9
11
  import InfoRow from '../../components/info-row';
10
12
  import Layout from '../../components/layout';
11
13
  import CustomerInvoiceList from '../../components/portal/invoice/list';
@@ -45,6 +47,20 @@ export default function CustomerHome() {
45
47
  );
46
48
  }
47
49
 
50
+ const onUpdateInfo = async (updates: TCustomerExpanded) => {
51
+ try {
52
+ setState({ loading: true });
53
+ await api.put(`/api/customers/${data.id}`, updates).then((res) => res.data);
54
+ Toast.success(t('common.saved'));
55
+ runAsync();
56
+ } catch (err) {
57
+ console.error(err);
58
+ Toast.error(formatError(err));
59
+ } finally {
60
+ setState({ loading: false });
61
+ }
62
+ };
63
+
48
64
  return (
49
65
  <Layout>
50
66
  <Grid container spacing={5}>
@@ -103,6 +119,14 @@ export default function CustomerHome() {
103
119
  value={data.address?.postal_code}
104
120
  />
105
121
  </Stack>
122
+ {state.editing && (
123
+ <EditCustomer
124
+ data={data}
125
+ loading={state.loading}
126
+ onSave={onUpdateInfo}
127
+ onCancel={() => setState({ editing: false })}
128
+ />
129
+ )}
106
130
  </Box>
107
131
  </Root>
108
132
  </Grid>