payment-kit 1.13.23 → 1.13.24

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,11 +1,13 @@
1
1
  import { Op } from 'sequelize';
2
2
 
3
+ import { events } from '../libs/event';
3
4
  import logger from '../libs/logger';
4
5
  import createQueue from '../libs/queue';
6
+ import { getWebhookJobId } from '../libs/util';
5
7
  import { Event } from '../store/models/event';
6
8
  import { WebhookAttempt } from '../store/models/webhook-attempt';
7
9
  import { WebhookEndpoint } from '../store/models/webhook-endpoint';
8
- import { getJobId, webhookQueue } from './webhook';
10
+ import { webhookQueue } from './webhook';
9
11
 
10
12
  type EventJob = {
11
13
  eventId: string;
@@ -43,7 +45,7 @@ export const handleEvent = async (job: EventJob) => {
43
45
  if (!attempted) {
44
46
  logger.info('schedule initial attempt for event', job);
45
47
  webhookQueue.push({
46
- id: getJobId(event.id, webhook.id),
48
+ id: getWebhookJobId(event.id, webhook.id),
47
49
  job: { eventId: event.id, webhookId: webhook.id },
48
50
  });
49
51
  }
@@ -60,14 +62,14 @@ export const eventQueue = createQueue<EventJob>({
60
62
  });
61
63
 
62
64
  export const startEventQueue = async () => {
63
- const events = await Event.findAll({
65
+ const docs = await Event.findAll({
64
66
  where: {
65
67
  pending_webhooks: { [Op.gt]: 0 },
66
68
  },
67
69
  attributes: ['id'],
68
70
  });
69
71
 
70
- events.forEach(async (x) => {
72
+ docs.forEach(async (x) => {
71
73
  const exist = await eventQueue.get(x.id);
72
74
  if (!exist) {
73
75
  eventQueue.push({ id: x.id, job: { eventId: x.id } });
@@ -78,3 +80,7 @@ export const startEventQueue = async () => {
78
80
  eventQueue.on('failed', ({ id, job, error }) => {
79
81
  logger.error('event job failed', { id, job, error });
80
82
  });
83
+
84
+ events.on('event.created', (event) => {
85
+ eventQueue.push({ id: event.id, job: { eventId: event.id } });
86
+ });
@@ -4,8 +4,10 @@ import axios, { AxiosError } from 'axios';
4
4
  import { wallet } from '../libs/auth';
5
5
  import logger from '../libs/logger';
6
6
  import createQueue from '../libs/queue';
7
- import { MAX_RETRY_COUNT, getNextRetry, md5 } from '../libs/util';
7
+ import { MAX_RETRY_COUNT, getNextRetry, getWebhookJobId } from '../libs/util';
8
+ import { Customer } from '../store/models/customer';
8
9
  import { Event } from '../store/models/event';
10
+ import { PaymentCurrency } from '../store/models/payment-currency';
9
11
  import { WebhookAttempt } from '../store/models/webhook-attempt';
10
12
  import { WebhookEndpoint } from '../store/models/webhook-endpoint';
11
13
 
@@ -14,10 +16,6 @@ type WebhookJob = {
14
16
  webhookId: string;
15
17
  };
16
18
 
17
- export const getJobId = (eventId: string, webhookId: string) => {
18
- return md5([eventId, webhookId].join('-'));
19
- };
20
-
21
19
  // https://stripe.com/docs/webhooks
22
20
  export const handleWebhook = async (job: WebhookJob) => {
23
21
  logger.info('handle webhook', job);
@@ -45,16 +43,27 @@ export const handleWebhook = async (job: WebhookJob) => {
45
43
  const retryCount = lastRetryCount ? +lastRetryCount + 1 : 1;
46
44
 
47
45
  try {
46
+ const json = event.toJSON();
47
+
48
+ // expand basic fields
49
+ const { object } = json.data;
50
+ if (object.customer_id && !object.customer) {
51
+ object.customer = await Customer.findByPk(object.customer_id);
52
+ }
53
+ if (object.currency_id && !object.currency) {
54
+ object.currency = await PaymentCurrency.findByPk(object.currency_id);
55
+ }
56
+
48
57
  // verify similar to component call, but supports external urls
49
58
  const result = await axios({
50
59
  url: webhook.url,
51
60
  method: 'POST',
52
61
  timeout: 60 * 1000,
53
- data: event.toJSON(),
62
+ data: json,
54
63
  headers: {
55
64
  'x-app-id': wallet.address,
56
65
  'x-app-pk': wallet.publicKey,
57
- 'x-component-sig': sign(event.toJSON()),
66
+ 'x-component-sig': sign(json),
58
67
  'x-component-did': process.env.BLOCKLET_COMPONENT_DID as string,
59
68
  },
60
69
  });
@@ -87,7 +96,7 @@ export const handleWebhook = async (job: WebhookJob) => {
87
96
  if (retryCount < MAX_RETRY_COUNT) {
88
97
  process.nextTick(() => {
89
98
  webhookQueue.push({
90
- id: getJobId(event.id, webhook.id),
99
+ id: getWebhookJobId(event.id, webhook.id),
91
100
  job: { eventId: event.id, webhookId: webhook.id },
92
101
  runAt: getNextRetry(retryCount),
93
102
  });
@@ -1,7 +1,7 @@
1
1
  import pick from 'lodash/pick';
2
2
 
3
- import { eventQueue } from '../jobs/event';
4
3
  import { Event } from '../store/models/event';
4
+ import { events } from './event';
5
5
 
6
6
  export async function createEvent(scope: string, type: string, model: any, options: any) {
7
7
  // console.log('createEvent', scope, type, model, options);
@@ -28,7 +28,7 @@ export async function createEvent(scope: string, type: string, model: any, optio
28
28
  pending_webhooks: 99, // force all events goto the event queue
29
29
  });
30
30
 
31
- eventQueue.push({ id: event.id, job: { eventId: event.id } });
31
+ events.emit('event.created', { id: event.id });
32
32
  }
33
33
 
34
34
  export async function createStatusEvent(
@@ -69,5 +69,5 @@ export async function createStatusEvent(
69
69
  pending_webhooks: 99, // force all events goto the event queue
70
70
  });
71
71
 
72
- eventQueue.push({ id: event.id, job: { eventId: event.id } });
72
+ events.emit('event.created', { id: event.id });
73
73
  }
@@ -0,0 +1,3 @@
1
+ import EventEmitter from 'events';
2
+
3
+ export const events = new EventEmitter();
@@ -66,6 +66,7 @@ export function createCodeGenerator(prefix: string, size: number = 24) {
66
66
  return prefix ? () => `${prefix}_${generator()}` : generator;
67
67
  }
68
68
 
69
+ // FIXME: merge with old metadata
69
70
  export function formatMetadata(metadata?: Record<string, any>): Record<string, any> {
70
71
  if (!metadata) {
71
72
  return {};
@@ -137,3 +138,7 @@ export const getNextRetry = (retryCount: number) => {
137
138
  const now = dayjs().unix();
138
139
  return now + delay;
139
140
  };
141
+
142
+ export const getWebhookJobId = (eventId: string, webhookId: string) => {
143
+ return md5([eventId, webhookId].join('-'));
144
+ };
@@ -8,7 +8,7 @@ import dayjs from '../../libs/dayjs';
8
8
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
9
9
 
10
10
  export default {
11
- action: 'pay',
11
+ action: 'payment',
12
12
  authPrincipal: false,
13
13
  claims: {
14
14
  authPrincipal: async ({ extraParams }: CallbackArgs) => {
@@ -8,6 +8,7 @@ import dayjs from '../libs/dayjs';
8
8
  import logger from '../libs/logger';
9
9
  import { authenticate } from '../libs/security';
10
10
  import { expandLineItems } from '../libs/session';
11
+ import { formatMetadata } from '../libs/util';
11
12
  import { Customer } from '../store/models/customer';
12
13
  import { PaymentCurrency } from '../store/models/payment-currency';
13
14
  import { PaymentMethod } from '../store/models/payment-method';
@@ -302,4 +303,18 @@ router.put('/:id/resume', auth, async (req, res) => {
302
303
  return res.json(doc);
303
304
  });
304
305
 
306
+ router.put('/:id', auth, async (req, res) => {
307
+ const doc = await Subscription.findByPk(req.params.id);
308
+
309
+ if (!doc) {
310
+ return res.status(404).json({ error: 'Subscription not found' });
311
+ }
312
+
313
+ if (req.body.metadata) {
314
+ await doc.update({ metadata: formatMetadata(req.body.metadata) });
315
+ }
316
+
317
+ return res.json(doc);
318
+ });
319
+
305
320
  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.23
17
+ version: 1.13.24
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
@@ -40,7 +40,7 @@ payment:
40
40
  timeout:
41
41
  start: 60
42
42
  requirements:
43
- server: '>=1.16.15'
43
+ server: '>=1.16.10'
44
44
  os: '*'
45
45
  cpu: '*'
46
46
  scripts:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.23",
3
+ "version": "1.13.24",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -64,6 +64,7 @@
64
64
  "axios": "^0.27.2",
65
65
  "body-parser": "^1.20.2",
66
66
  "cookie-parser": "^1.4.6",
67
+ "copy-to-clipboard": "^3.3.3",
67
68
  "cors": "^2.8.5",
68
69
  "dayjs": "^1.11.10",
69
70
  "dotenv-flow": "^3.3.0",
@@ -100,7 +101,7 @@
100
101
  "devDependencies": {
101
102
  "@arcblock/eslint-config": "^0.2.4",
102
103
  "@arcblock/eslint-config-ts": "^0.2.4",
103
- "@did-pay/types": "1.13.23",
104
+ "@did-pay/types": "1.13.24",
104
105
  "@types/cookie-parser": "^1.4.4",
105
106
  "@types/cors": "^2.8.14",
106
107
  "@types/dotenv-flow": "^3.3.1",
@@ -137,5 +138,5 @@
137
138
  "parser": "typescript"
138
139
  }
139
140
  },
140
- "gitHead": "a4ba80b4e2d020c912361af6fd555f8bfc52f54f"
141
+ "gitHead": "abe08b8cb109399d3b53b985f628d9688b561c0c"
141
142
  }
@@ -38,6 +38,14 @@ const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
38
38
  };
39
39
 
40
40
  export default function TxLink(props: { details: PaymentDetails; method: TPaymentMethod }) {
41
+ if (!props.details) {
42
+ return (
43
+ <Typography component="small" color="text.secondary">
44
+ None
45
+ </Typography>
46
+ );
47
+ }
48
+
41
49
  const { text, link } = getTxLink(props.method, props.details);
42
50
  return (
43
51
  <Link href={link} target="_blank" rel="noopener noreferrer">
@@ -2,7 +2,9 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
3
  import type { TPaymentLinkExpanded } from '@did-pay/types';
4
4
  import { useSetState } from 'ahooks';
5
+ import Copy from 'copy-to-clipboard';
5
6
  import type { LiteralUnion } from 'type-fest';
7
+ import { joinURL } from 'ufo';
6
8
 
7
9
  import api from '../../libs/api';
8
10
  import { formatError } from '../../libs/util';
@@ -54,7 +56,7 @@ export default function PaymentLinkActions({ data, variant, onChange }: Props) {
54
56
  setState({ loading: false, action: '' });
55
57
  }
56
58
  };
57
- const noRemove = async () => {
59
+ const onRemove = async () => {
58
60
  try {
59
61
  setState({ loading: true });
60
62
  await api.delete(`/api/payment-links/${data.id}`).then((res) => res.data);
@@ -67,18 +69,27 @@ export default function PaymentLinkActions({ data, variant, onChange }: Props) {
67
69
  setState({ loading: false, action: '' });
68
70
  }
69
71
  };
72
+ const onCopyLink = () => {
73
+ Copy(joinURL(window.blocklet.appUrl, window.blocklet.prefix, `/checkout/pay/${data.id}`));
74
+ Toast.success(t('common.copied'));
75
+ };
70
76
 
71
77
  return (
72
78
  <ClickBoundary>
73
79
  <Actions
74
80
  variant={variant}
75
81
  actions={[
76
- // {
77
- // label: t('admin.paymentLink.edit'),
78
- // handler: () => setState({ action: 'edit' }),
79
- // color: 'primary',
80
- // divider: true,
81
- // },
82
+ {
83
+ label: t('admin.paymentLink.edit'),
84
+ handler: () => setState({ action: 'edit' }),
85
+ color: 'primary',
86
+ disabled: true,
87
+ },
88
+ {
89
+ label: t('admin.paymentLink.copyLink'),
90
+ handler: onCopyLink,
91
+ color: 'primary',
92
+ },
82
93
  { label: t('admin.paymentLink.rename'), handler: () => setState({ action: 'rename' }), color: 'primary' },
83
94
  { label: t('admin.paymentLink.archive'), handler: () => setState({ action: 'archive' }), color: 'primary' },
84
95
  { label: t('admin.paymentLink.remove'), handler: () => setState({ action: 'remove' }), color: 'error' },
@@ -103,7 +114,7 @@ export default function PaymentLinkActions({ data, variant, onChange }: Props) {
103
114
  )}
104
115
  {state.action === 'remove' && (
105
116
  <ConfirmDialog
106
- onConfirm={noRemove}
117
+ onConfirm={onRemove}
107
118
  onCancel={() => setState({ action: '' })}
108
119
  title={t('admin.paymentLink.remove')}
109
120
  message={t('admin.paymentLink.removeTip')}
@@ -125,7 +125,10 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
125
125
  <Controller
126
126
  name={getFieldName('unit_amount')}
127
127
  control={control}
128
- rules={{ required: t('admin.price.unit_amount.required'), min: t('admin.price.unit_amount.positive') }}
128
+ rules={{
129
+ required: t('admin.price.unit_amount.required'),
130
+ validate: (v) => (Number(v) > 0 ? true : t('admin.price.unit_amount.positive')),
131
+ }}
129
132
  disabled={isLocked}
130
133
  render={({ field }) => (
131
134
  <Box>
package/src/libs/util.ts CHANGED
@@ -132,11 +132,17 @@ export const formatProductPrice = (
132
132
  return 'No price';
133
133
  };
134
134
 
135
- export const formatPrice = (price: TPrice, currency: TPaymentCurrency, unit_label?: string, quantity: number = 1) => {
136
- const amount = fromUnitToToken(
137
- new BN(getPriceUintAmountByCurrency(price, currency)).mul(new BN(quantity)),
138
- currency.decimal
139
- ).toString();
135
+ export const formatPrice = (
136
+ price: TPrice,
137
+ currency: TPaymentCurrency,
138
+ unit_label?: string,
139
+ quantity: number = 1,
140
+ bn: boolean = true
141
+ ) => {
142
+ const unit = getPriceUintAmountByCurrency(price, currency);
143
+ const amount = bn
144
+ ? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString()
145
+ : +unit * quantity;
140
146
  if (price?.type === 'recurring' && price.recurring) {
141
147
  const recurring = formatRecurring(price.recurring, false, '/');
142
148
 
@@ -44,6 +44,7 @@ export default flat({
44
44
  loadMore: 'View more {resource}',
45
45
  loadingMore: 'Loading more {resource}...',
46
46
  noMore: 'No more {resource}',
47
+ copied: 'Copied',
47
48
  metadata: {
48
49
  label: 'Metadata',
49
50
  add: 'Add more metadata',
@@ -173,6 +174,7 @@ export default flat({
173
174
  info: 'Payment link information',
174
175
  add: 'Create payment link',
175
176
  save: 'Create link',
177
+ copyLink: 'Copy URL',
176
178
  saved: 'Payment link successfully saved',
177
179
  additional: 'Additional options',
178
180
  beforePay: 'Payment page',
@@ -34,7 +34,7 @@ export default function ProductsCreate() {
34
34
  metadata: [],
35
35
  },
36
36
  });
37
- const { control, handleSubmit } = methods;
37
+ const { control, handleSubmit, getValues } = methods;
38
38
 
39
39
  const prices = useFieldArray({ control, name: 'prices' });
40
40
  const getPrice = (index: number) => methods.getValues().prices[index];
@@ -79,10 +79,14 @@ export default function ProductsCreate() {
79
79
  expanded
80
80
  style={{ fontWeight: 'bold', width: '50%' }}
81
81
  addons={<PriceActions onDuplicate={() => prices.append(price)} onRemove={() => prices.remove(index)} />}
82
- trigger={(expanded: boolean) =>
82
+ trigger={(expanded: boolean) => {
83
+ if (expanded) {
84
+ return t('admin.price.detail');
85
+ }
86
+
83
87
  // @ts-ignore
84
- expanded ? t('admin.price.detail') : formatPrice(getPrice(index), settings.baseCurrency)
85
- }>
88
+ return formatPrice(getPrice(index), settings.baseCurrency, getValues().unit_label, 1, false);
89
+ }}>
86
90
  <PriceForm prefix={`prices.${index}`} />
87
91
  </Collapse>
88
92
  <Divider sx={{ mt: 2, mb: 4 }} />