payment-kit 1.13.44 → 1.13.46
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/integrations/stripe/handlers/invoice.ts +1 -1
- package/api/src/integrations/stripe/handlers/payment-intent.ts +9 -25
- package/api/src/jobs/payment.ts +76 -48
- package/api/src/libs/session.ts +1 -1
- package/api/src/routes/connect/collect.ts +2 -12
- package/api/src/routes/connect/pay.ts +3 -24
- package/api/src/routes/connect/subscribe.ts +0 -1
- package/api/src/routes/prices.ts +21 -18
- package/api/src/routes/pricing-table.ts +3 -1
- package/blocklet.yml +1 -1
- package/package.json +3 -3
- package/src/components/checkout/form/index.tsx +8 -0
- package/src/components/checkout/product-item.tsx +7 -7
- package/src/components/checkout/summary.tsx +2 -2
- package/src/components/product/cross-sell-select.tsx +2 -2
- package/src/components/product/cross-sell.tsx +2 -1
- package/src/libs/util.ts +52 -31
- package/src/locales/en.tsx +36 -11
- package/src/locales/index.tsx +25 -0
- package/src/locales/zh.tsx +33 -9
- package/src/pages/admin/payments/links/detail.tsx +2 -1
- package/src/pages/admin/products/prices/detail.tsx +11 -3
- package/src/pages/admin/products/prices/list.tsx +4 -2
- package/src/pages/admin/products/pricing-tables/detail.tsx +2 -1
- package/src/pages/admin/products/products/create.tsx +2 -2
- package/src/pages/admin/products/products/detail.tsx +2 -2
- package/src/pages/admin/products/products/index.tsx +2 -2
- package/src/pages/checkout/pricing-table.tsx +4 -4
|
@@ -176,7 +176,7 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
export async function handleStripeInvoiceCreated(event: TEventExpanded, client: Stripe) {
|
|
179
|
-
if (['invoice.created', 'payment_intent.created'].includes(event.type) === false) {
|
|
179
|
+
if (['invoice.created', 'payment_intent.created', 'payment_intent.succeeded'].includes(event.type) === false) {
|
|
180
180
|
logger.warn('abort because event type not expected', { id: event.id, type: event.type });
|
|
181
181
|
return null;
|
|
182
182
|
}
|
|
@@ -4,9 +4,10 @@ import pick from 'lodash/pick';
|
|
|
4
4
|
import pWaitFor from 'p-wait-for';
|
|
5
5
|
import type Stripe from 'stripe';
|
|
6
6
|
|
|
7
|
+
import { handlePaymentSucceed } from '../../../jobs/payment';
|
|
7
8
|
import dayjs from '../../../libs/dayjs';
|
|
8
9
|
import logger from '../../../libs/logger';
|
|
9
|
-
import {
|
|
10
|
+
import { Invoice, PaymentIntent, PaymentMethod, TEventExpanded } from '../../../store/models';
|
|
10
11
|
import { handleStripeInvoiceCreated } from './invoice';
|
|
11
12
|
|
|
12
13
|
export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, event?: TEventExpanded) {
|
|
@@ -20,30 +21,7 @@ export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, e
|
|
|
20
21
|
});
|
|
21
22
|
logger.info('payment intent succeeded on stripe event', { locale: paymentIntent.id });
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
if (checkoutSession) {
|
|
25
|
-
await checkoutSession.update({
|
|
26
|
-
status: 'complete',
|
|
27
|
-
payment_status: 'paid',
|
|
28
|
-
payment_details: paymentIntent.payment_details,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (paymentIntent.invoice_id) {
|
|
33
|
-
const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
|
|
34
|
-
if (invoice && invoice.status !== 'paid') {
|
|
35
|
-
await invoice.update({
|
|
36
|
-
paid: true,
|
|
37
|
-
status: 'paid',
|
|
38
|
-
amount_due: '0',
|
|
39
|
-
amount_paid: paymentIntent.amount,
|
|
40
|
-
amount_remaining: '0',
|
|
41
|
-
attempt_count: invoice.attempt_count + 1,
|
|
42
|
-
attempted: true,
|
|
43
|
-
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
24
|
+
await handlePaymentSucceed(paymentIntent);
|
|
47
25
|
}
|
|
48
26
|
|
|
49
27
|
export async function syncStripPayment(paymentIntent: PaymentIntent) {
|
|
@@ -59,6 +37,8 @@ export async function syncStripPayment(paymentIntent: PaymentIntent) {
|
|
|
59
37
|
const client = await method.getStripeClient();
|
|
60
38
|
const stripeIntent = await client.paymentIntents.retrieve(paymentIntent.metadata.stripe_id);
|
|
61
39
|
if (stripeIntent) {
|
|
40
|
+
const justSucceed = stripeIntent.status === 'succeeded' && paymentIntent.status !== 'succeeded';
|
|
41
|
+
|
|
62
42
|
// @ts-ignore
|
|
63
43
|
await paymentIntent.update({
|
|
64
44
|
amount: String(stripeIntent.amount),
|
|
@@ -68,6 +48,10 @@ export async function syncStripPayment(paymentIntent: PaymentIntent) {
|
|
|
68
48
|
...pick(stripeIntent, ['status', 'confirmation_method', 'capture_method', 'last_payment_error']),
|
|
69
49
|
});
|
|
70
50
|
logger.info('stripe payment intent synced', { locale: paymentIntent.id, remote: stripeIntent.id });
|
|
51
|
+
|
|
52
|
+
if (justSucceed) {
|
|
53
|
+
await handlePaymentSucceed(paymentIntent);
|
|
54
|
+
}
|
|
71
55
|
}
|
|
72
56
|
}
|
|
73
57
|
|
package/api/src/jobs/payment.ts
CHANGED
|
@@ -19,6 +19,62 @@ type PaymentJob = {
|
|
|
19
19
|
retryOnError?: boolean;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
|
|
23
|
+
let invoice;
|
|
24
|
+
if (paymentIntent.invoice_id) {
|
|
25
|
+
invoice = await Invoice.findByPk(paymentIntent.invoice_id);
|
|
26
|
+
}
|
|
27
|
+
if (!invoice) {
|
|
28
|
+
const checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: paymentIntent.id } });
|
|
29
|
+
if (checkoutSession && checkoutSession.status === 'open') {
|
|
30
|
+
await checkoutSession.update({
|
|
31
|
+
status: 'complete',
|
|
32
|
+
payment_status: 'paid',
|
|
33
|
+
payment_details: paymentIntent.payment_details,
|
|
34
|
+
});
|
|
35
|
+
logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent.id}`);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await invoice.update({
|
|
41
|
+
paid: true,
|
|
42
|
+
status: 'paid',
|
|
43
|
+
amount_due: '0',
|
|
44
|
+
amount_paid: paymentIntent.amount,
|
|
45
|
+
amount_remaining: '0',
|
|
46
|
+
attempt_count: invoice.attempt_count + 1,
|
|
47
|
+
attempted: true,
|
|
48
|
+
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
49
|
+
});
|
|
50
|
+
logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`);
|
|
51
|
+
|
|
52
|
+
if (invoice.subscription_id) {
|
|
53
|
+
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
54
|
+
if (subscription) {
|
|
55
|
+
if (subscription.status === 'incomplete') {
|
|
56
|
+
await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
|
|
57
|
+
logger.info(`Subscription ${subscription.id} updated on payment done ${invoice.id}`);
|
|
58
|
+
} else {
|
|
59
|
+
await subscription.update({ status: 'active' });
|
|
60
|
+
logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (invoice.checkout_session_id) {
|
|
66
|
+
const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
|
|
67
|
+
if (checkoutSession && checkoutSession.status === 'open') {
|
|
68
|
+
await checkoutSession.update({
|
|
69
|
+
status: 'complete',
|
|
70
|
+
payment_status: 'paid',
|
|
71
|
+
payment_details: paymentIntent.payment_details,
|
|
72
|
+
});
|
|
73
|
+
logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent.id}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
22
78
|
export const handlePayment = async (job: PaymentJob) => {
|
|
23
79
|
logger.info('handle payment', job);
|
|
24
80
|
|
|
@@ -110,49 +166,7 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
110
166
|
},
|
|
111
167
|
});
|
|
112
168
|
|
|
113
|
-
|
|
114
|
-
await invoice.update({
|
|
115
|
-
paid: true,
|
|
116
|
-
status: 'paid',
|
|
117
|
-
amount_due: '0',
|
|
118
|
-
amount_paid: paymentIntent.amount,
|
|
119
|
-
amount_remaining: '0',
|
|
120
|
-
attempt_count: invoice.attempt_count + 1,
|
|
121
|
-
attempted: true,
|
|
122
|
-
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
123
|
-
});
|
|
124
|
-
logger.info(`Invoice ${invoice.id} updated on payment done: ${job.paymentIntentId}`);
|
|
125
|
-
|
|
126
|
-
if (invoice.subscription_id) {
|
|
127
|
-
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
128
|
-
if (subscription) {
|
|
129
|
-
if (subscription.status === 'incomplete') {
|
|
130
|
-
await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
|
|
131
|
-
logger.info(`Subscription ${subscription.id} updated on payment done ${invoice.id}`);
|
|
132
|
-
} else {
|
|
133
|
-
await subscription.update({ status: 'active' });
|
|
134
|
-
logger.info(`Subscription ${subscription.id} moved to active after payment done ${invoice.id}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (invoice.checkout_session_id) {
|
|
140
|
-
const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
|
|
141
|
-
if (checkoutSession && checkoutSession.status === 'open') {
|
|
142
|
-
await checkoutSession.update({
|
|
143
|
-
status: 'complete',
|
|
144
|
-
payment_status: 'paid',
|
|
145
|
-
payment_details: {
|
|
146
|
-
arcblock: {
|
|
147
|
-
tx_hash: txHash,
|
|
148
|
-
payer: payer as string,
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${invoice.id}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
169
|
+
await handlePaymentSucceed(paymentIntent);
|
|
156
170
|
} catch (err) {
|
|
157
171
|
logger.error('PaymentIntent capture failed', { error: err, id: paymentIntent.id });
|
|
158
172
|
|
|
@@ -165,10 +179,27 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
165
179
|
payment_method_type: paymentMethod.type,
|
|
166
180
|
};
|
|
167
181
|
|
|
168
|
-
if (
|
|
182
|
+
if (!job.retryOnError) {
|
|
183
|
+
// To a final state without any retry
|
|
184
|
+
await paymentIntent.update({ status: 'requires_action', last_payment_error: error });
|
|
185
|
+
if (invoice) {
|
|
186
|
+
await invoice.update({
|
|
187
|
+
status: 'uncollectible',
|
|
188
|
+
attempt_count: invoice.attempt_count + 1,
|
|
189
|
+
attempted: true,
|
|
190
|
+
status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} else if (invoice) {
|
|
169
194
|
if (invoice.attempt_count > MAX_RETRY_COUNT) {
|
|
170
195
|
await paymentIntent.update({ status: 'requires_action', last_payment_error: error });
|
|
171
|
-
await invoice.update({
|
|
196
|
+
await invoice.update({
|
|
197
|
+
status: 'uncollectible',
|
|
198
|
+
attempt_count: invoice.attempt_count + 1,
|
|
199
|
+
attempted: true,
|
|
200
|
+
status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
|
|
201
|
+
});
|
|
202
|
+
|
|
172
203
|
// FIXME: send email to customer, pause subscription
|
|
173
204
|
logger.error('PaymentIntent capture failed after max retry', { id: paymentIntent.id });
|
|
174
205
|
} else {
|
|
@@ -189,9 +220,6 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
189
220
|
runAt: retryAt,
|
|
190
221
|
});
|
|
191
222
|
}
|
|
192
|
-
} else {
|
|
193
|
-
logger.error('PaymentIntent status reverted on capture error', { id: paymentIntent.id });
|
|
194
|
-
await paymentIntent.update({ status: 'requires_capture' });
|
|
195
223
|
}
|
|
196
224
|
}
|
|
197
225
|
};
|
package/api/src/libs/session.ts
CHANGED
|
@@ -2,10 +2,9 @@ import type { Transaction, TransferV3Tx } from '@ocap/client';
|
|
|
2
2
|
import { fromAddress } from '@ocap/wallet';
|
|
3
3
|
|
|
4
4
|
import { invoiceQueue } from '../../jobs/invoice';
|
|
5
|
-
import { paymentQueue } from '../../jobs/payment';
|
|
5
|
+
import { handlePaymentSucceed, paymentQueue } from '../../jobs/payment';
|
|
6
6
|
import type { CallbackArgs } from '../../libs/auth';
|
|
7
7
|
import { wallet } from '../../libs/auth';
|
|
8
|
-
import dayjs from '../../libs/dayjs';
|
|
9
8
|
import { getTxMetadata } from '../../libs/util';
|
|
10
9
|
import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
|
|
11
10
|
|
|
@@ -83,16 +82,7 @@ export default {
|
|
|
83
82
|
},
|
|
84
83
|
});
|
|
85
84
|
|
|
86
|
-
await
|
|
87
|
-
paid: true,
|
|
88
|
-
status: 'paid',
|
|
89
|
-
amount_due: '0',
|
|
90
|
-
amount_paid: invoice.amount_due,
|
|
91
|
-
amount_remaining: '0',
|
|
92
|
-
attempt_count: invoice.attempt_count + 1,
|
|
93
|
-
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
94
|
-
collection_method: 'send_invoice',
|
|
95
|
-
});
|
|
85
|
+
await handlePaymentSucceed(paymentIntent);
|
|
96
86
|
|
|
97
87
|
// cleanup the queue
|
|
98
88
|
let exist = await paymentQueue.get(paymentIntent.id);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Transaction, TransferV3Tx } from '@ocap/client';
|
|
2
2
|
import { fromAddress } from '@ocap/wallet';
|
|
3
3
|
|
|
4
|
+
import { handlePaymentSucceed } from '../../jobs/payment';
|
|
4
5
|
import type { CallbackArgs } from '../../libs/auth';
|
|
5
6
|
import { wallet } from '../../libs/auth';
|
|
6
|
-
import dayjs from '../../libs/dayjs';
|
|
7
7
|
import { getTxMetadata } from '../../libs/util';
|
|
8
8
|
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
|
|
9
9
|
|
|
@@ -60,7 +60,7 @@ export default {
|
|
|
60
60
|
throw new Error('Payment intent not found');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
|
|
64
64
|
|
|
65
65
|
if (paymentMethod.type === 'arcblock') {
|
|
66
66
|
await paymentIntent.update({ status: 'processing' });
|
|
@@ -79,16 +79,6 @@ export default {
|
|
|
79
79
|
{ headers: client.pickGasPayerHeaders(request) }
|
|
80
80
|
);
|
|
81
81
|
|
|
82
|
-
await checkoutSession.update({
|
|
83
|
-
status: 'complete',
|
|
84
|
-
payment_status: 'paid',
|
|
85
|
-
payment_details: {
|
|
86
|
-
arcblock: {
|
|
87
|
-
tx_hash: txHash,
|
|
88
|
-
payer: userDid,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
82
|
await paymentIntent.update({
|
|
93
83
|
status: 'succeeded',
|
|
94
84
|
amount_received: paymentIntent.amount,
|
|
@@ -100,18 +90,7 @@ export default {
|
|
|
100
90
|
},
|
|
101
91
|
});
|
|
102
92
|
|
|
103
|
-
|
|
104
|
-
await invoice.update({
|
|
105
|
-
paid: true,
|
|
106
|
-
status: 'paid',
|
|
107
|
-
amount_due: '0',
|
|
108
|
-
amount_paid: paymentIntent.amount,
|
|
109
|
-
amount_remaining: '0',
|
|
110
|
-
attempt_count: invoice.attempt_count + 1,
|
|
111
|
-
attempted: true,
|
|
112
|
-
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
113
|
-
});
|
|
114
|
-
}
|
|
93
|
+
await handlePaymentSucceed(paymentIntent);
|
|
115
94
|
|
|
116
95
|
return { hash: txHash };
|
|
117
96
|
}
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fromTokenToUnit } from '@ocap/util';
|
|
1
|
+
import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import pick from 'lodash/pick';
|
|
@@ -114,20 +114,20 @@ router.get('/:id/upsell', auth, async (req, res) => {
|
|
|
114
114
|
// update price
|
|
115
115
|
// FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
|
|
116
116
|
router.put('/:id', auth, async (req, res) => {
|
|
117
|
-
const
|
|
117
|
+
const doc = await Price.findByPkOrLookupKey(req.params.id as string);
|
|
118
118
|
|
|
119
|
-
if (!
|
|
119
|
+
if (!doc) {
|
|
120
120
|
return res.status(404).json({ error: 'price not found' });
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
if (
|
|
123
|
+
if (doc.active === false) {
|
|
124
124
|
return res.status(403).json({ error: 'price archived' });
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const updates: Partial<Price> = Price.formatBeforeSave(
|
|
128
128
|
pick(
|
|
129
129
|
req.body,
|
|
130
|
-
|
|
130
|
+
doc.locked
|
|
131
131
|
? ['nickname', 'description', 'metadata', 'currency_options', 'upsell']
|
|
132
132
|
: ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
|
|
133
133
|
)
|
|
@@ -135,38 +135,41 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
135
135
|
|
|
136
136
|
if (updates.lookup_key) {
|
|
137
137
|
const exist = await Price.findOne({ where: { lookup_key: updates.lookup_key } });
|
|
138
|
-
if (exist && exist.id !==
|
|
138
|
+
if (exist && exist.id !== doc.id) {
|
|
139
139
|
return res.status(400).json({ error: `lookup_key ${updates.lookup_key} already used by ${exist.id}` });
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
const currencies = await PaymentCurrency.findAll({ where: { active: true } });
|
|
144
|
-
const currency = currencies.find((x) => x.id ===
|
|
144
|
+
const currency = currencies.find((x) => x.id === doc.currency_id);
|
|
145
145
|
if (!currency) {
|
|
146
|
-
return res.status(400).json({ error: `currency used in price not found or not active: ${
|
|
146
|
+
return res.status(400).json({ error: `currency used in price not found or not active: ${doc.currency_id}` });
|
|
147
147
|
}
|
|
148
148
|
if (updates.unit_amount) {
|
|
149
149
|
updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
|
|
150
|
+
if (updates.currency_options) {
|
|
151
|
+
const exist = updates.currency_options.find((x) => x.currency_id === doc.currency_id);
|
|
152
|
+
if (exist) {
|
|
153
|
+
exist.unit_amount = fromUnitToToken(updates.unit_amount as string, currency.decimal);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
150
156
|
}
|
|
151
157
|
if (updates.currency_options) {
|
|
152
158
|
updates.currency_options = Price.formatCurrencies(updates.currency_options, currencies);
|
|
153
|
-
|
|
159
|
+
const index = updates.currency_options.findIndex((x) => x.currency_id === doc.currency_id);
|
|
160
|
+
if (index > -1) {
|
|
161
|
+
updates.unit_amount = updates.currency_options[index]?.unit_amount;
|
|
162
|
+
} else {
|
|
154
163
|
updates.currency_options.unshift({
|
|
155
|
-
currency_id:
|
|
156
|
-
unit_amount:
|
|
164
|
+
currency_id: doc.currency_id,
|
|
165
|
+
unit_amount: doc.unit_amount,
|
|
157
166
|
tiers: null,
|
|
158
167
|
custom_unit_amount: null,
|
|
159
168
|
});
|
|
160
169
|
}
|
|
161
|
-
if (updates.unit_amount) {
|
|
162
|
-
const base = price.currency_options.find((x) => x.currency_id === price.currency_id);
|
|
163
|
-
if (base) {
|
|
164
|
-
base.unit_amount = updates.unit_amount;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
await
|
|
172
|
+
await doc.update(Price.formatBeforeSave(updates));
|
|
170
173
|
|
|
171
174
|
return res.json(await getExpandedPrice(req.params.id as string));
|
|
172
175
|
});
|
|
@@ -344,9 +344,11 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
|
|
|
344
344
|
},
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
+
const currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
|
|
348
|
+
|
|
347
349
|
raw.livemode = doc.livemode;
|
|
348
350
|
raw.created_via = 'portal';
|
|
349
|
-
raw.currency_id =
|
|
351
|
+
raw.currency_id = currency?.id;
|
|
350
352
|
|
|
351
353
|
if (req.query.redirect) {
|
|
352
354
|
raw.success_url = req.query.redirect as string;
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.46",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"@abtnode/types": "1.16.17",
|
|
104
104
|
"@arcblock/eslint-config": "^0.2.4",
|
|
105
105
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
106
|
-
"@did-pay/types": "1.13.
|
|
106
|
+
"@did-pay/types": "1.13.46",
|
|
107
107
|
"@types/cookie-parser": "^1.4.5",
|
|
108
108
|
"@types/cors": "^2.8.15",
|
|
109
109
|
"@types/dotenv-flow": "^3.3.2",
|
|
@@ -140,5 +140,5 @@
|
|
|
140
140
|
"parser": "typescript"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
-
"gitHead": "
|
|
143
|
+
"gitHead": "7c37d3e6f4814056ddc98033b257707fbefba2d9"
|
|
144
144
|
}
|
|
@@ -28,6 +28,14 @@ const waitForCheckoutComplete = (sessionId: string) => {
|
|
|
28
28
|
return pWaitFor(
|
|
29
29
|
async () => {
|
|
30
30
|
const { data } = await api.get(`/api/checkout-sessions/retrieve/${sessionId}`);
|
|
31
|
+
if (
|
|
32
|
+
data.paymentIntent &&
|
|
33
|
+
data.paymentIntent.status === 'requires_action' &&
|
|
34
|
+
data.paymentIntent.last_payment_error
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(data.paymentIntent.last_payment_error.message);
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
return (
|
|
32
40
|
data.checkoutSession?.status === 'complete' &&
|
|
33
41
|
['paid', 'no_payment_required'].includes(data.checkoutSession?.payment_status)
|
|
@@ -23,10 +23,10 @@ ProductItem.defaultProps = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export default function ProductItem({ item, session, currency, mode, children, onUpsell, onDownsell }: Props) {
|
|
26
|
-
const { t } = useLocaleContext();
|
|
27
|
-
const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0);
|
|
26
|
+
const { t, locale } = useLocaleContext();
|
|
27
|
+
const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0, locale);
|
|
28
28
|
const saving = formatUpsellSaving(session, currency);
|
|
29
|
-
const metered = item.price?.recurring?.usage_type === 'metered' ? '
|
|
29
|
+
const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
|
|
30
30
|
const canUpsell = mode === 'normal' && session.line_items.length === 1;
|
|
31
31
|
return (
|
|
32
32
|
<Stack direction="column" alignItems="flex-start" spacing={1} sx={{ width: '100%' }}>
|
|
@@ -38,7 +38,7 @@ export default function ProductItem({ item, session, currency, mode, children, o
|
|
|
38
38
|
description={item.price.product?.description}
|
|
39
39
|
extra={
|
|
40
40
|
item.price.type === 'recurring' && item.price.recurring
|
|
41
|
-
? [pricing.quantity,
|
|
41
|
+
? [pricing.quantity, t('common.billed', { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, 'per', locale)} ${metered}` })].filter(Boolean).join(', ') // prettier-ignore
|
|
42
42
|
: pricing.quantity
|
|
43
43
|
}
|
|
44
44
|
/>
|
|
@@ -71,12 +71,12 @@ export default function ProductItem({ item, session, currency, mode, children, o
|
|
|
71
71
|
onChange={() => onUpsell(item.price_id, item.price.upsell?.upsells_to_id)}
|
|
72
72
|
/>
|
|
73
73
|
{t('checkout.upsell.save', {
|
|
74
|
-
recurring:
|
|
74
|
+
recurring: formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring, true, 'per', locale),
|
|
75
75
|
})}
|
|
76
76
|
<Status label={t('checkout.upsell.off', { saving })} color="primary" variant="outlined" sx={{ ml: 1 }} />
|
|
77
77
|
</Typography>
|
|
78
78
|
<Typography component="span" sx={{ fontSize: 12 }}>
|
|
79
|
-
{formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label)}
|
|
79
|
+
{formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label, 1, true, locale)}
|
|
80
80
|
</Typography>
|
|
81
81
|
</Stack>
|
|
82
82
|
)}
|
|
@@ -102,7 +102,7 @@ export default function ProductItem({ item, session, currency, mode, children, o
|
|
|
102
102
|
})}
|
|
103
103
|
</Typography>
|
|
104
104
|
<Typography component="span" sx={{ fontSize: 12 }}>
|
|
105
|
-
{formatPrice(item.price, currency, item.price.product?.unit_label)}
|
|
105
|
+
{formatPrice(item.price, currency, item.price.product?.unit_label, 1, true, locale)}
|
|
106
106
|
</Typography>
|
|
107
107
|
</Stack>
|
|
108
108
|
)}
|
|
@@ -41,10 +41,10 @@ export default function PaymentSummary({
|
|
|
41
41
|
onApplyCrossSell,
|
|
42
42
|
onCancelCrossSell,
|
|
43
43
|
}: Props) {
|
|
44
|
-
const { t } = useLocaleContext();
|
|
44
|
+
const { t, locale } = useLocaleContext();
|
|
45
45
|
const [state, setState] = useSetState({ loading: false });
|
|
46
46
|
const { data, runAsync } = useRequest(() => fetchCrossSell(checkoutSession.id));
|
|
47
|
-
const headlines = formatCheckoutHeadlines(checkoutSession, currency);
|
|
47
|
+
const headlines = formatCheckoutHeadlines(checkoutSession, currency, locale);
|
|
48
48
|
|
|
49
49
|
const handleUpsell = async (from: string, to: string) => {
|
|
50
50
|
await onUpsell(from, to);
|
|
@@ -13,7 +13,7 @@ type Props = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export default function CrossSellSelect({ data, onSelect }: Props) {
|
|
16
|
-
const { t } = useLocaleContext();
|
|
16
|
+
const { t, locale } = useLocaleContext();
|
|
17
17
|
const { products } = useProductsContext();
|
|
18
18
|
const { settings } = useSettingsContext();
|
|
19
19
|
|
|
@@ -41,7 +41,7 @@ export default function CrossSellSelect({ data, onSelect }: Props) {
|
|
|
41
41
|
<MenuItem key={x.id} value={x.id}>
|
|
42
42
|
<InfoCard
|
|
43
43
|
name={x.name}
|
|
44
|
-
description={formatProductPrice(x as any, settings.baseCurrency)}
|
|
44
|
+
description={formatProductPrice(x as any, settings.baseCurrency, locale)}
|
|
45
45
|
logo={x.images[0]}
|
|
46
46
|
/>
|
|
47
47
|
</MenuItem>
|
|
@@ -14,6 +14,7 @@ import InfoRow from '../info-row';
|
|
|
14
14
|
import CrossSellSelect from './cross-sell-select';
|
|
15
15
|
|
|
16
16
|
export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onChange: Function }) {
|
|
17
|
+
const { locale } = useLocaleContext();
|
|
17
18
|
const { settings } = useSettingsContext();
|
|
18
19
|
const [state, setState] = useSetState({
|
|
19
20
|
loading: false,
|
|
@@ -55,7 +56,7 @@ export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onCh
|
|
|
55
56
|
<Stack spacing={1} direction="row" alignItems="center">
|
|
56
57
|
<InfoCard
|
|
57
58
|
name={to.name}
|
|
58
|
-
description={formatProductPrice(to as any, settings.baseCurrency)}
|
|
59
|
+
description={formatProductPrice(to as any, settings.baseCurrency, locale)}
|
|
59
60
|
logo={to.images[0]}
|
|
60
61
|
/>
|
|
61
62
|
<IconButton size="small" sx={{ ml: 1 }} onClick={onRemoveUpsell}>
|
package/src/libs/util.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-nested-ternary */
|
|
1
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
3
|
import type {
|
|
3
4
|
LineItem,
|
|
@@ -17,6 +18,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|
|
17
18
|
import isEqual from 'lodash/isEqual';
|
|
18
19
|
import { defaultCountries } from 'react-international-phone';
|
|
19
20
|
|
|
21
|
+
import { t } from '../locales/index';
|
|
20
22
|
import dayjs from './dayjs';
|
|
21
23
|
|
|
22
24
|
export function getExplorerLink(chainHost: string, did: string, type: string) {
|
|
@@ -119,17 +121,18 @@ export const formatError = (err: any) => {
|
|
|
119
121
|
|
|
120
122
|
export const formatProductPrice = (
|
|
121
123
|
{ prices, unit_label }: { prices: TPrice[]; unit_label: string },
|
|
122
|
-
currency: TPaymentCurrency
|
|
124
|
+
currency: TPaymentCurrency,
|
|
125
|
+
locale: string = 'en'
|
|
123
126
|
): string => {
|
|
124
127
|
if (prices.length > 1) {
|
|
125
|
-
return
|
|
128
|
+
return t('admin.price.count', locale, { count: prices.length });
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
if (prices.length === 1) {
|
|
129
|
-
return formatPrice(prices[0] as TPrice, currency, unit_label);
|
|
132
|
+
return formatPrice(prices[0] as TPrice, currency, unit_label, 1, true, locale);
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
return '
|
|
135
|
+
return t('admin.price.empty', locale);
|
|
133
136
|
};
|
|
134
137
|
|
|
135
138
|
export const formatPrice = (
|
|
@@ -137,14 +140,15 @@ export const formatPrice = (
|
|
|
137
140
|
currency: TPaymentCurrency,
|
|
138
141
|
unit_label?: string,
|
|
139
142
|
quantity: number = 1,
|
|
140
|
-
bn: boolean = true
|
|
143
|
+
bn: boolean = true,
|
|
144
|
+
locale: string = 'en'
|
|
141
145
|
) => {
|
|
142
146
|
const unit = getPriceUintAmountByCurrency(price, currency);
|
|
143
147
|
const amount = bn
|
|
144
148
|
? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString()
|
|
145
149
|
: +unit * quantity;
|
|
146
150
|
if (price?.type === 'recurring' && price.recurring) {
|
|
147
|
-
const recurring = formatRecurring(price.recurring, false, '
|
|
151
|
+
const recurring = formatRecurring(price.recurring, false, 'slash', locale);
|
|
148
152
|
|
|
149
153
|
if (unit_label) {
|
|
150
154
|
return `${amount} ${currency.symbol} / ${unit_label} ${recurring}`;
|
|
@@ -219,7 +223,12 @@ export function getStatementDescriptor(items: any[]) {
|
|
|
219
223
|
return window.blocklet.appName;
|
|
220
224
|
}
|
|
221
225
|
|
|
222
|
-
export function formatRecurring(
|
|
226
|
+
export function formatRecurring(
|
|
227
|
+
recurring: PriceRecurring,
|
|
228
|
+
translate: boolean = true,
|
|
229
|
+
separator: string = 'per',
|
|
230
|
+
locale: string = 'en'
|
|
231
|
+
) {
|
|
223
232
|
const intervals = {
|
|
224
233
|
hour: 'hourly',
|
|
225
234
|
day: 'daily',
|
|
@@ -229,11 +238,15 @@ export function formatRecurring(recurring: PriceRecurring, translate: boolean =
|
|
|
229
238
|
};
|
|
230
239
|
|
|
231
240
|
if (+recurring.interval_count === 1) {
|
|
241
|
+
const interval = t(`common.${recurring.interval}`, locale);
|
|
232
242
|
// @ts-ignore
|
|
233
|
-
return translate ? intervals[recurring.interval] :
|
|
243
|
+
return translate ? t(`common.${intervals[recurring.interval]}`, locale) : separator ? t(`common.${separator}`, locale, { interval }) : interval; // prettier-ignore
|
|
234
244
|
}
|
|
235
245
|
|
|
236
|
-
return
|
|
246
|
+
return t('common.recurring', locale, {
|
|
247
|
+
count: recurring.interval_count,
|
|
248
|
+
interval: t(`common.${recurring.interval}s`, locale),
|
|
249
|
+
});
|
|
237
250
|
}
|
|
238
251
|
|
|
239
252
|
export function getPriceUintAmountByCurrency(price: TPrice, currency: TPaymentCurrency) {
|
|
@@ -257,11 +270,12 @@ export function getPriceCurrencyOptions(price: TPrice): PriceCurrency[] {
|
|
|
257
270
|
export function formatLineItemPricing(
|
|
258
271
|
item: TLineItemExpanded,
|
|
259
272
|
currency: TPaymentCurrency,
|
|
260
|
-
trial: number
|
|
273
|
+
trial: number,
|
|
274
|
+
locale: string = 'en'
|
|
261
275
|
): { primary: string; secondary?: string; quantity: string } {
|
|
262
276
|
const price = item.upsell_price || item.price;
|
|
263
277
|
|
|
264
|
-
let quantity =
|
|
278
|
+
let quantity = t('common.qty', locale, { count: item.quantity });
|
|
265
279
|
if (price.recurring?.usage_type === 'metered' || +item.quantity === 1) {
|
|
266
280
|
quantity = '';
|
|
267
281
|
}
|
|
@@ -274,20 +288,20 @@ export function formatLineItemPricing(
|
|
|
274
288
|
|
|
275
289
|
const appendUnit = (v: string, alt: string) => {
|
|
276
290
|
if (price.product.unit_label) {
|
|
277
|
-
return `${v}
|
|
291
|
+
return `${v}/${price.product.unit_label}`;
|
|
278
292
|
}
|
|
279
293
|
if (price.recurring?.usage_type === 'metered' || item.quantity === 1) {
|
|
280
294
|
return alt;
|
|
281
295
|
}
|
|
282
296
|
|
|
283
|
-
return quantity ?
|
|
297
|
+
return quantity ? t('common.each', locale, { unit }) : '';
|
|
284
298
|
};
|
|
285
299
|
|
|
286
300
|
if (price.type === 'recurring' && price.recurring) {
|
|
287
301
|
if (trial > 0) {
|
|
288
302
|
return {
|
|
289
|
-
primary:
|
|
290
|
-
secondary: `${appendUnit(total, total)} ${formatRecurring(price.recurring, false, '
|
|
303
|
+
primary: t('common.trial', locale, { count: trial }),
|
|
304
|
+
secondary: `${appendUnit(total, total)} ${formatRecurring(price.recurring, false, 'slash', locale)}`,
|
|
291
305
|
quantity,
|
|
292
306
|
};
|
|
293
307
|
}
|
|
@@ -408,7 +422,8 @@ export function formatUpsellSaving(session: TCheckoutSessionExpanded, currency:
|
|
|
408
422
|
|
|
409
423
|
export function formatCheckoutHeadlines(
|
|
410
424
|
session: TCheckoutSessionExpanded,
|
|
411
|
-
currency: TPaymentCurrency
|
|
425
|
+
currency: TPaymentCurrency,
|
|
426
|
+
locale: string = 'en'
|
|
412
427
|
): {
|
|
413
428
|
action: string;
|
|
414
429
|
amount: string;
|
|
@@ -425,7 +440,7 @@ export function formatCheckoutHeadlines(
|
|
|
425
440
|
// empty
|
|
426
441
|
if (items.length === 0) {
|
|
427
442
|
return {
|
|
428
|
-
action: '
|
|
443
|
+
action: t('checkout.empty', locale),
|
|
429
444
|
amount: '0',
|
|
430
445
|
then: '',
|
|
431
446
|
};
|
|
@@ -435,21 +450,27 @@ export function formatCheckoutHeadlines(
|
|
|
435
450
|
|
|
436
451
|
// all one time
|
|
437
452
|
if (items.every((x) => x.price.type === 'one_time')) {
|
|
453
|
+
const action = t('checkout.pay', locale, { payee: brand });
|
|
438
454
|
if (items.length > 1) {
|
|
439
|
-
return { action
|
|
455
|
+
return { action, amount };
|
|
440
456
|
}
|
|
441
457
|
|
|
442
|
-
return { action
|
|
458
|
+
return { action, amount, then: '' };
|
|
443
459
|
}
|
|
444
460
|
|
|
445
461
|
const item = items.find((x) => x.price.type === 'recurring');
|
|
446
|
-
const recurring = formatRecurring(
|
|
462
|
+
const recurring = formatRecurring(
|
|
463
|
+
(item?.upsell_price || item?.price)?.recurring as PriceRecurring,
|
|
464
|
+
false,
|
|
465
|
+
'per',
|
|
466
|
+
locale
|
|
467
|
+
);
|
|
447
468
|
|
|
448
469
|
// all recurring
|
|
449
470
|
if (items.every((x) => x.price.type === 'recurring')) {
|
|
450
471
|
const hasMetered = items.some((x) => x.price.type === 'recurring' && x.price.recurring?.usage_type === 'metered');
|
|
451
472
|
const subscription = [
|
|
452
|
-
hasMetered ? '
|
|
473
|
+
hasMetered ? t('checkout.least', locale) : '',
|
|
453
474
|
fromUnitToToken(
|
|
454
475
|
items.reduce((acc, x) => {
|
|
455
476
|
if (x.price.recurring?.usage_type === 'metered') {
|
|
@@ -466,14 +487,14 @@ export function formatCheckoutHeadlines(
|
|
|
466
487
|
if (items.length > 1) {
|
|
467
488
|
if (trial > 0) {
|
|
468
489
|
return {
|
|
469
|
-
action:
|
|
470
|
-
amount:
|
|
471
|
-
then:
|
|
490
|
+
action: t('checkout.try2', locale, { name, count: items.length - 1 }),
|
|
491
|
+
amount: t('checkout.free', locale, { count: trial }),
|
|
492
|
+
then: t('checkout.then', locale, { subscription, recurring }),
|
|
472
493
|
};
|
|
473
494
|
}
|
|
474
495
|
|
|
475
496
|
return {
|
|
476
|
-
action:
|
|
497
|
+
action: t('checkout.sub2', locale, { name, count: items.length - 1 }),
|
|
477
498
|
amount,
|
|
478
499
|
then: recurring,
|
|
479
500
|
};
|
|
@@ -481,14 +502,14 @@ export function formatCheckoutHeadlines(
|
|
|
481
502
|
|
|
482
503
|
if (trial > 0) {
|
|
483
504
|
return {
|
|
484
|
-
action:
|
|
485
|
-
amount:
|
|
486
|
-
then:
|
|
505
|
+
action: t('checkout.try1', locale, { name }),
|
|
506
|
+
amount: t('checkout.free', locale, { count: trial }),
|
|
507
|
+
then: t('checkout.then', locale, { subscription, recurring }),
|
|
487
508
|
};
|
|
488
509
|
}
|
|
489
510
|
|
|
490
511
|
return {
|
|
491
|
-
action:
|
|
512
|
+
action: t('checkout.sub1', locale, { name }),
|
|
492
513
|
amount,
|
|
493
514
|
then: recurring,
|
|
494
515
|
};
|
|
@@ -508,9 +529,9 @@ export function formatCheckoutHeadlines(
|
|
|
508
529
|
);
|
|
509
530
|
|
|
510
531
|
return {
|
|
511
|
-
action:
|
|
532
|
+
action: t('checkout.pay', locale, { payee: brand }),
|
|
512
533
|
amount,
|
|
513
|
-
then:
|
|
534
|
+
then: t('checkout.then', locale, { subscription: `${subscription} ${currency.symbol}`, recurring }),
|
|
514
535
|
};
|
|
515
536
|
}
|
|
516
537
|
|
package/src/locales/en.tsx
CHANGED
|
@@ -28,7 +28,8 @@ export default flat({
|
|
|
28
28
|
confirm: 'Confirm',
|
|
29
29
|
cancel: 'Cancel',
|
|
30
30
|
every: 'every',
|
|
31
|
-
per: 'per',
|
|
31
|
+
per: 'per {interval}',
|
|
32
|
+
slash: '/ {interval}',
|
|
32
33
|
unit: 'units',
|
|
33
34
|
edit: 'Edit',
|
|
34
35
|
quantity: 'Quantity',
|
|
@@ -47,16 +48,29 @@ export default flat({
|
|
|
47
48
|
copied: 'Copied',
|
|
48
49
|
previous: 'Back',
|
|
49
50
|
continue: 'Continue',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
qty: 'Qty {count}',
|
|
52
|
+
each: '{unit} each',
|
|
53
|
+
trial: 'Free for {count} days',
|
|
54
|
+
billed: 'billed {rule}',
|
|
55
|
+
metered: 'based on usage',
|
|
56
|
+
hour: 'hour',
|
|
57
|
+
day: 'day',
|
|
58
|
+
week: 'week',
|
|
59
|
+
month: 'month',
|
|
60
|
+
year: 'year',
|
|
61
|
+
hourly: 'hourly',
|
|
62
|
+
daily: 'daily',
|
|
63
|
+
weekly: 'weekly',
|
|
64
|
+
monthly: 'monthly',
|
|
65
|
+
yearly: 'yearly',
|
|
66
|
+
month3: 'every 3 months',
|
|
67
|
+
month6: 'every 6 months',
|
|
68
|
+
recurring: 'every {count} {interval}',
|
|
69
|
+
hours: 'hours',
|
|
70
|
+
days: 'days',
|
|
71
|
+
weeks: 'weeks',
|
|
72
|
+
months: 'months',
|
|
73
|
+
years: 'years',
|
|
60
74
|
metadata: {
|
|
61
75
|
label: 'Metadata',
|
|
62
76
|
add: 'Add more metadata',
|
|
@@ -145,6 +159,8 @@ export default flat({
|
|
|
145
159
|
name: 'Price',
|
|
146
160
|
type: 'Usage type',
|
|
147
161
|
info: 'Price information',
|
|
162
|
+
count: '{count} prices',
|
|
163
|
+
empty: 'No price',
|
|
148
164
|
lookupKey: 'Lookup key',
|
|
149
165
|
setAsDefault: 'Set as default price',
|
|
150
166
|
detail: 'Pricing details',
|
|
@@ -496,6 +512,15 @@ export default flat({
|
|
|
496
512
|
login: 'Login to load and save contact information',
|
|
497
513
|
portal: 'Manage subscriptions',
|
|
498
514
|
cardPay: '{action} with card',
|
|
515
|
+
empty: 'No thing to pay',
|
|
516
|
+
pay: 'Pay {payee}',
|
|
517
|
+
try1: 'Try {name}',
|
|
518
|
+
try2: 'Try {name} and {count} more',
|
|
519
|
+
sub1: 'Subscribe to {name}',
|
|
520
|
+
sub2: 'Subscribe to {name} and {count} more',
|
|
521
|
+
then: 'Then {subscription} {recurring}',
|
|
522
|
+
free: '{count} days free',
|
|
523
|
+
least: 'continue with at least',
|
|
499
524
|
completed: {
|
|
500
525
|
payment: 'Thanks for your purchase',
|
|
501
526
|
subscription: 'Thanks for your subscribing',
|
package/src/locales/index.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-prototype-builtins */
|
|
1
2
|
import en from './en';
|
|
2
3
|
import zh from './zh';
|
|
3
4
|
|
|
@@ -6,3 +7,27 @@ export const translations = {
|
|
|
6
7
|
zh,
|
|
7
8
|
en,
|
|
8
9
|
};
|
|
10
|
+
|
|
11
|
+
export const replace = (template: string, data: Record<string, any> = {}) =>
|
|
12
|
+
template.replace(/{(\w*)}/g, (_, key) => (data.hasOwnProperty(key) ? data[key] : ''));
|
|
13
|
+
|
|
14
|
+
export const createTranslator = ({ fallbackLocale = 'en' }: { fallbackLocale?: string }) => {
|
|
15
|
+
return (key: string, locale = fallbackLocale, data: Record<string, any> = {}) => {
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
if (!translations[locale] || !translations[locale][key]) {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
if (fallbackLocale && translations[fallbackLocale]?.[key]) {
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
return replace(translations[fallbackLocale]?.[key], data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
return replace(translations[locale][key], data);
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const translate = createTranslator({ fallbackLocale: 'en' });
|
|
33
|
+
export const t = translate;
|
package/src/locales/zh.tsx
CHANGED
|
@@ -28,8 +28,9 @@ export default flat({
|
|
|
28
28
|
confirm: '确认',
|
|
29
29
|
cancel: '取消',
|
|
30
30
|
every: '每',
|
|
31
|
-
per: '每',
|
|
32
|
-
|
|
31
|
+
per: '每{interval}',
|
|
32
|
+
slash: '每{interval}',
|
|
33
|
+
unit: '件',
|
|
33
34
|
edit: '编辑',
|
|
34
35
|
quantity: '数量',
|
|
35
36
|
yes: '是',
|
|
@@ -47,16 +48,29 @@ export default flat({
|
|
|
47
48
|
copied: '已复制',
|
|
48
49
|
previous: '返回',
|
|
49
50
|
continue: '继续',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
qty: '{count} 件',
|
|
52
|
+
each: '每件 {unit}',
|
|
53
|
+
trial: '免费试用 {count} 天',
|
|
54
|
+
billed: '{rule}收费',
|
|
55
|
+
metered: '按用量',
|
|
56
|
+
hour: '小时',
|
|
57
|
+
day: '天',
|
|
58
|
+
week: '周',
|
|
59
|
+
month: '月',
|
|
60
|
+
year: '年',
|
|
61
|
+
hourly: '按小时',
|
|
62
|
+
daily: '按天',
|
|
63
|
+
weekly: '按周',
|
|
64
|
+
monthly: '按月',
|
|
65
|
+
yearly: '按年',
|
|
66
|
+
month3: '按季度',
|
|
67
|
+
month6: '按半年',
|
|
68
|
+
recurring: '每{count}{interval}',
|
|
69
|
+
hours: '小时',
|
|
57
70
|
days: '天',
|
|
58
71
|
weeks: '周',
|
|
59
72
|
months: '月',
|
|
73
|
+
years: '年',
|
|
60
74
|
metadata: {
|
|
61
75
|
label: '元数据',
|
|
62
76
|
add: '添加更多元数据',
|
|
@@ -488,6 +502,16 @@ export default flat({
|
|
|
488
502
|
login: '登录以加载并保存联系信息',
|
|
489
503
|
portal: '管理订阅',
|
|
490
504
|
cardPay: '使用卡片{action}',
|
|
505
|
+
empty: '没有可支付的项目',
|
|
506
|
+
per: '每',
|
|
507
|
+
pay: '付款给 {payee}',
|
|
508
|
+
try1: '免费试用 {name}',
|
|
509
|
+
try2: '免费试用 {name} 等{count}个产品',
|
|
510
|
+
sub1: '订阅 {name}',
|
|
511
|
+
sub2: '订阅 {name} 等{count}个产品',
|
|
512
|
+
then: '然后 {subscription} {recurring}',
|
|
513
|
+
free: '{count} 天',
|
|
514
|
+
least: '至少',
|
|
491
515
|
completed: {
|
|
492
516
|
payment: '感谢您的购买',
|
|
493
517
|
subscription: '感谢您的订阅',
|
|
@@ -158,7 +158,8 @@ export default function PaymentLinkDetail(props: { id: string }) {
|
|
|
158
158
|
description={formatProductPrice(
|
|
159
159
|
// @ts-ignore
|
|
160
160
|
{ ...item.price.product, prices: [item.price] },
|
|
161
|
-
settings.baseCurrency
|
|
161
|
+
settings.baseCurrency,
|
|
162
|
+
locale
|
|
162
163
|
)}
|
|
163
164
|
logo={item.price.product.images[0]}
|
|
164
165
|
/>
|
|
@@ -27,7 +27,7 @@ const fetchData = (id: string): Promise<TPriceExpanded> => {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export default function PriceDetail(props: { id: string }) {
|
|
30
|
-
const { t } = useLocaleContext();
|
|
30
|
+
const { t, locale } = useLocaleContext();
|
|
31
31
|
const navigate = useNavigate();
|
|
32
32
|
const [state, setState] = useSetState({
|
|
33
33
|
adding: {
|
|
@@ -121,7 +121,11 @@ export default function PriceDetail(props: { id: string }) {
|
|
|
121
121
|
value={<Link to={`/admin/products/${data.product_id}`}>{data.product.name}</Link>}
|
|
122
122
|
divider
|
|
123
123
|
/>
|
|
124
|
-
<InfoMetric
|
|
124
|
+
<InfoMetric
|
|
125
|
+
label={t('admin.price.amount')}
|
|
126
|
+
value={formatPrice(data, data.currency, data.product.unit_label, 1, true, locale)}
|
|
127
|
+
divider
|
|
128
|
+
/>
|
|
125
129
|
<InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
|
|
126
130
|
<InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
|
|
127
131
|
</Stack>
|
|
@@ -221,7 +225,11 @@ export default function PriceDetail(props: { id: string }) {
|
|
|
221
225
|
const item = data.currency_options[index] as any;
|
|
222
226
|
return formatPrice(
|
|
223
227
|
{ type: data.type, unit_amount: item.unit_amount, recurring: data.recurring } as TPrice,
|
|
224
|
-
item.currency
|
|
228
|
+
item.currency,
|
|
229
|
+
data.product.unit_label,
|
|
230
|
+
1,
|
|
231
|
+
true,
|
|
232
|
+
locale
|
|
225
233
|
);
|
|
226
234
|
},
|
|
227
235
|
},
|
|
@@ -13,7 +13,7 @@ import { formatPrice, formatTime } from '../../../../libs/util';
|
|
|
13
13
|
import PriceActions from './actions';
|
|
14
14
|
|
|
15
15
|
export default function PricesList({ product, onChange }: { product: Product; onChange: Function }) {
|
|
16
|
-
const { t } = useLocaleContext();
|
|
16
|
+
const { t, locale } = useLocaleContext();
|
|
17
17
|
const { settings } = useSettingsContext();
|
|
18
18
|
|
|
19
19
|
const columns = [
|
|
@@ -32,7 +32,9 @@ export default function PricesList({ product, onChange }: { product: Product; on
|
|
|
32
32
|
<LockOutlined sx={{ color: 'text.secondary' }} />
|
|
33
33
|
</Tooltip>
|
|
34
34
|
)}
|
|
35
|
-
<Typography component="span">
|
|
35
|
+
<Typography component="span">
|
|
36
|
+
{formatPrice(price, settings.baseCurrency, '', 1, true, locale)}
|
|
37
|
+
</Typography>
|
|
36
38
|
<Typography component="span">
|
|
37
39
|
{price.id === product.default_price_id && <Status label="default" color="info" sx={{ height: 18 }} />}
|
|
38
40
|
</Typography>
|
|
@@ -138,7 +138,8 @@ export default function PricingTableDetail(props: { id: string }) {
|
|
|
138
138
|
description={formatProductPrice(
|
|
139
139
|
// @ts-ignore
|
|
140
140
|
{ ...item.product, prices: [item.price] },
|
|
141
|
-
settings.baseCurrency
|
|
141
|
+
settings.baseCurrency,
|
|
142
|
+
locale
|
|
142
143
|
)}
|
|
143
144
|
logo={item.product.images[0]}
|
|
144
145
|
/>
|
|
@@ -18,7 +18,7 @@ import api from '../../../../libs/api';
|
|
|
18
18
|
import { formatError, formatPrice } from '../../../../libs/util';
|
|
19
19
|
|
|
20
20
|
export default function ProductsCreate() {
|
|
21
|
-
const { t } = useLocaleContext();
|
|
21
|
+
const { t, locale } = useLocaleContext();
|
|
22
22
|
const { settings } = useSettingsContext();
|
|
23
23
|
|
|
24
24
|
const methods = useForm<Product>({
|
|
@@ -85,7 +85,7 @@ export default function ProductsCreate() {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// @ts-ignore
|
|
88
|
-
return formatPrice(getPrice(index), settings.baseCurrency, getValues().unit_label, 1, false);
|
|
88
|
+
return formatPrice(getPrice(index), settings.baseCurrency, getValues().unit_label, 1, false, locale);
|
|
89
89
|
}}>
|
|
90
90
|
<PriceForm prefix={`prices.${index}`} />
|
|
91
91
|
</Collapse>
|
|
@@ -29,7 +29,7 @@ const getProduct = (id: string): Promise<TProductExpanded> => {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export default function ProductDetail(props: { id: string }) {
|
|
32
|
-
const { t } = useLocaleContext();
|
|
32
|
+
const { t, locale } = useLocaleContext();
|
|
33
33
|
const navigate = useNavigate();
|
|
34
34
|
const { settings } = useSettingsContext();
|
|
35
35
|
const [state, setState] = useSetState({
|
|
@@ -128,7 +128,7 @@ export default function ProductDetail(props: { id: string }) {
|
|
|
128
128
|
logo={data.images[0]}
|
|
129
129
|
name={data.name}
|
|
130
130
|
// @ts-ignore
|
|
131
|
-
description={formatProductPrice(data, settings.baseCurrency)}
|
|
131
|
+
description={formatProductPrice(data, settings.baseCurrency, locale)}
|
|
132
132
|
/>
|
|
133
133
|
<ProductActions data={data} onChange={onChange} variant="normal" />
|
|
134
134
|
</Stack>
|
|
@@ -28,7 +28,7 @@ export default function ProductsList() {
|
|
|
28
28
|
const listKey = 'products';
|
|
29
29
|
const persisted = getDurableData(listKey);
|
|
30
30
|
|
|
31
|
-
const { t } = useLocaleContext();
|
|
31
|
+
const { t, locale } = useLocaleContext();
|
|
32
32
|
const navigate = useNavigate();
|
|
33
33
|
const { settings } = useSettingsContext();
|
|
34
34
|
const [search, setSearch] = useState<{ active: string; pageSize: number; page: number }>({
|
|
@@ -62,7 +62,7 @@ export default function ProductsList() {
|
|
|
62
62
|
return (
|
|
63
63
|
<InfoCard
|
|
64
64
|
name={product.name}
|
|
65
|
-
description={formatProductPrice(product as any, settings.baseCurrency)}
|
|
65
|
+
description={formatProductPrice(product as any, settings.baseCurrency, locale)}
|
|
66
66
|
logo={product.images[0]}
|
|
67
67
|
/>
|
|
68
68
|
);
|
|
@@ -58,7 +58,7 @@ const groupItemsByRecurring = (items: TPricingTableItem[]) => {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
export default function PricingTable({ id }: Props) {
|
|
61
|
-
const { t } = useLocaleContext();
|
|
61
|
+
const { t, locale } = useLocaleContext();
|
|
62
62
|
const [params] = useSearchParams();
|
|
63
63
|
const { error, loading, data } = useRequest(() => fetchData(id));
|
|
64
64
|
const [state, setState] = useSetState({ interval: '', loading: '' });
|
|
@@ -149,7 +149,7 @@ export default function PricingTable({ id }: Props) {
|
|
|
149
149
|
<ToggleButtonGroup value={state.interval} onChange={(_, value) => setState({ interval: value })} exclusive>
|
|
150
150
|
{Object.keys(recurring).map((x) => (
|
|
151
151
|
<ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
|
|
152
|
-
{formatRecurring(recurring[x] as PriceRecurring)}
|
|
152
|
+
{formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
|
|
153
153
|
</ToggleButton>
|
|
154
154
|
))}
|
|
155
155
|
</ToggleButtonGroup>
|
|
@@ -186,10 +186,10 @@ export default function PricingTable({ id }: Props) {
|
|
|
186
186
|
<PaymentAmount amount={formatPriceAmount(x.price, data.currency, x.product.unit_label)} />
|
|
187
187
|
<Stack direction="column" alignItems="flex-start">
|
|
188
188
|
<Typography component="span" color="text.secondary" fontSize="0.8rem">
|
|
189
|
-
per
|
|
189
|
+
{t('checkout.per')}
|
|
190
190
|
</Typography>
|
|
191
191
|
<Typography component="span" color="text.secondary" fontSize="0.8rem">
|
|
192
|
-
{formatRecurring(x.price.recurring as PriceRecurring, false, '')}
|
|
192
|
+
{formatRecurring(x.price.recurring as PriceRecurring, false, '', locale)}
|
|
193
193
|
</Typography>
|
|
194
194
|
</Stack>
|
|
195
195
|
</Stack>
|