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.
- package/api/src/jobs/event.ts +10 -4
- package/api/src/jobs/webhook.ts +17 -8
- package/api/src/libs/audit.ts +3 -3
- package/api/src/libs/event.ts +3 -0
- package/api/src/libs/util.ts +5 -0
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/subscriptions.ts +15 -0
- package/blocklet.yml +2 -2
- package/package.json +4 -3
- package/src/components/blockchain/tx.tsx +8 -0
- package/src/components/payment-link/actions.tsx +19 -8
- package/src/components/price/form.tsx +4 -1
- package/src/libs/util.ts +11 -5
- package/src/locales/en.tsx +2 -0
- package/src/pages/admin/products/products/create.tsx +8 -4
package/api/src/jobs/event.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|
package/api/src/jobs/webhook.ts
CHANGED
|
@@ -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,
|
|
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:
|
|
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(
|
|
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:
|
|
99
|
+
id: getWebhookJobId(event.id, webhook.id),
|
|
91
100
|
job: { eventId: event.id, webhookId: webhook.id },
|
|
92
101
|
runAt: getNextRetry(retryCount),
|
|
93
102
|
});
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
72
|
+
events.emit('event.created', { id: event.id });
|
|
73
73
|
}
|
package/api/src/libs/util.ts
CHANGED
|
@@ -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: '
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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={
|
|
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={{
|
|
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 = (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
package/src/locales/en.tsx
CHANGED
|
@@ -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
|
-
|
|
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 }} />
|