payment-kit 1.13.30 → 1.13.32
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/blockchain/nft.ts +0 -1
- package/api/src/integrations/blocklet/passport.ts +1 -1
- package/api/src/integrations/stripe/handlers/invoice.ts +44 -2
- package/api/src/integrations/stripe/handlers/payment-intent.ts +32 -8
- package/api/src/integrations/stripe/resource.ts +7 -4
- package/api/src/jobs/subscription.ts +1 -1
- package/api/src/libs/payment.ts +6 -1
- package/api/src/libs/session.ts +78 -27
- package/api/src/libs/util.ts +15 -0
- package/api/src/routes/checkout-sessions.ts +161 -20
- package/api/src/routes/connect/collect.ts +5 -9
- package/api/src/routes/connect/pay.ts +5 -9
- package/api/src/routes/connect/setup.ts +22 -10
- package/api/src/routes/connect/shared.ts +13 -10
- package/api/src/routes/connect/subscribe.ts +29 -20
- package/api/src/routes/invoices.ts +5 -1
- package/api/src/routes/payment-intents.ts +5 -1
- package/api/src/routes/payment-links.ts +3 -2
- package/api/src/routes/prices.ts +32 -21
- package/api/src/store/migrations/20231023-upsell.ts +11 -0
- package/api/src/store/models/index.ts +10 -2
- package/api/src/store/models/price.ts +89 -23
- package/api/src/store/models/types.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +17 -17
- package/src/components/blockchain/tx.tsx +3 -1
- package/src/components/checkout/pay.tsx +39 -19
- package/src/components/checkout/product-card.tsx +2 -6
- package/src/components/checkout/product-item.tsx +84 -21
- package/src/components/checkout/summary.tsx +11 -2
- package/src/components/info-row.tsx +3 -1
- package/src/components/invoice/table.tsx +1 -1
- package/src/components/price/upsell-select.tsx +83 -0
- package/src/components/price/upsell.tsx +74 -0
- package/src/components/status.tsx +1 -1
- package/src/components/subscription/actions/cancel.tsx +25 -27
- package/src/components/subscription/items/index.tsx +1 -1
- package/src/libs/util.ts +51 -31
- package/src/locales/en.tsx +23 -2
- package/src/locales/zh.tsx +52 -40
- package/src/pages/admin/billing/index.tsx +3 -3
- package/src/pages/admin/customers/customers/detail.tsx +1 -0
- package/src/pages/admin/index.tsx +1 -0
- package/src/pages/admin/products/prices/detail.tsx +7 -0
- package/src/pages/customer/invoice.tsx +7 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
3
|
import pick from 'lodash/pick';
|
|
4
|
-
import {
|
|
4
|
+
import type { WhereOptions } from 'sequelize';
|
|
5
5
|
|
|
6
6
|
import { authenticate } from '../libs/security';
|
|
7
7
|
import { isLineItemAligned } from '../libs/session';
|
|
@@ -141,7 +141,7 @@ const paginationSchema = Joi.object<{
|
|
|
141
141
|
});
|
|
142
142
|
router.get('/', auth, async (req, res) => {
|
|
143
143
|
const { page, pageSize, ...query } = await paginationSchema.validateAsync(req.query, { stripUnknown: true });
|
|
144
|
-
const where: WhereOptions<PaymentLink> = {
|
|
144
|
+
const where: WhereOptions<PaymentLink> = {};
|
|
145
145
|
|
|
146
146
|
if (typeof query.active === 'boolean') {
|
|
147
147
|
where.active = query.active;
|
|
@@ -255,6 +255,7 @@ router.post('/stash', auth, async (req, res) => {
|
|
|
255
255
|
raw.livemode = !!req.livemode;
|
|
256
256
|
raw.created_via = req.user?.via;
|
|
257
257
|
raw.currency_id = raw.currency_id || req.currency.id;
|
|
258
|
+
raw.metadata = { preview: '1' };
|
|
258
259
|
|
|
259
260
|
let doc = await PaymentLink.findByPk(raw.id);
|
|
260
261
|
if (doc) {
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Router } from 'express';
|
|
|
3
3
|
import pick from 'lodash/pick';
|
|
4
4
|
|
|
5
5
|
import { authenticate } from '../libs/security';
|
|
6
|
+
import { canUpsell } from '../libs/session';
|
|
6
7
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
7
8
|
import { Price } from '../store/models/price';
|
|
8
9
|
import { Product } from '../store/models/product';
|
|
@@ -55,32 +56,42 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
if (price) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const currencies = await PaymentCurrency.findAll();
|
|
60
|
+
const doc = Price.formatAfterRead(price.toJSON(), currencies);
|
|
61
|
+
|
|
62
|
+
if (doc.upsell?.upsells_to_id) {
|
|
63
|
+
const to = await Price.findByPk(doc.upsell.upsells_to_id);
|
|
64
|
+
if (to) {
|
|
62
65
|
// @ts-ignore
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
doc.currency_options = [
|
|
67
|
-
{
|
|
68
|
-
currency_id: doc.currency_id,
|
|
69
|
-
unit_amount: doc.unit_amount,
|
|
70
|
-
// @ts-ignore
|
|
71
|
-
currency: doc.currency,
|
|
72
|
-
tiers: null,
|
|
73
|
-
custom_unit_amount: null,
|
|
74
|
-
},
|
|
75
|
-
];
|
|
66
|
+
doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
|
|
67
|
+
}
|
|
76
68
|
}
|
|
69
|
+
|
|
77
70
|
res.json(doc);
|
|
78
71
|
} else {
|
|
79
72
|
res.json(null);
|
|
80
73
|
}
|
|
81
74
|
});
|
|
82
75
|
|
|
76
|
+
router.get('/:id/upsell', auth, async (req, res) => {
|
|
77
|
+
const price = await Price.findByPkOrLookupKey(req.params.id as string, {
|
|
78
|
+
include: [{ model: PaymentCurrency, as: 'currency' }],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (price) {
|
|
82
|
+
const prices = await Price.findAll({
|
|
83
|
+
where: { product_id: price.product_id },
|
|
84
|
+
include: [{ model: PaymentCurrency, as: 'currency' }],
|
|
85
|
+
});
|
|
86
|
+
const upsells = prices.filter((x) => canUpsell(price, x));
|
|
87
|
+
res.json(upsells);
|
|
88
|
+
} else {
|
|
89
|
+
res.json(null);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
83
93
|
// update price
|
|
94
|
+
// FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
|
|
84
95
|
router.put('/:id', auth, async (req, res) => {
|
|
85
96
|
const price = await Price.findByPkOrLookupKey(req.params.id as string);
|
|
86
97
|
|
|
@@ -92,12 +103,12 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
92
103
|
return res.status(403).json({ error: 'price archived' });
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
const updates: Partial<Price> = Price.
|
|
106
|
+
const updates: Partial<Price> = Price.formatBeforeSave(
|
|
96
107
|
pick(
|
|
97
108
|
req.body,
|
|
98
109
|
price.locked
|
|
99
|
-
? ['nickname', 'description', 'metadata']
|
|
100
|
-
: ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options'] // prettier-ignore
|
|
110
|
+
? ['nickname', 'description', 'metadata', 'upsell']
|
|
111
|
+
: ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
|
|
101
112
|
)
|
|
102
113
|
);
|
|
103
114
|
|
|
@@ -140,7 +151,7 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
await price.update(Price.
|
|
154
|
+
await price.update(Price.formatBeforeSave(updates));
|
|
144
155
|
|
|
145
156
|
return res.json(price);
|
|
146
157
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import type { Migration } from '../migrate';
|
|
4
|
+
|
|
5
|
+
export const up: Migration = async ({ context }) => {
|
|
6
|
+
await context.addColumn('prices', 'upsell', { type: DataTypes.JSON, allowNull: true });
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const down: Migration = async ({ context }) => {
|
|
10
|
+
await context.removeColumn('prices', 'upsell');
|
|
11
|
+
};
|
|
@@ -85,9 +85,17 @@ export * from './webhook-attempt';
|
|
|
85
85
|
export * from './webhook-endpoint';
|
|
86
86
|
export * from './types';
|
|
87
87
|
|
|
88
|
-
export type TPriceExpanded = TPrice & {
|
|
88
|
+
export type TPriceExpanded = TPrice & {
|
|
89
|
+
object: 'price';
|
|
90
|
+
product: TProduct;
|
|
91
|
+
currency: TPaymentCurrency;
|
|
92
|
+
upsell?: {
|
|
93
|
+
upsells_to: TPriceExpanded;
|
|
94
|
+
upsells_to_id: string;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
89
97
|
|
|
90
|
-
export type TLineItemExpanded = LineItem & { price: TPriceExpanded };
|
|
98
|
+
export type TLineItemExpanded = LineItem & { price: TPriceExpanded; upsell_price: TPriceExpanded };
|
|
91
99
|
|
|
92
100
|
export type TProductExpanded = TProduct & { object: 'price'; prices: TPrice[]; default_price: TPrice };
|
|
93
101
|
|
|
@@ -20,6 +20,18 @@ import type { CustomUnitAmount, LineItem, PriceCurrency, PriceRecurring, PriceTi
|
|
|
20
20
|
|
|
21
21
|
const nextId = createIdGenerator('price', 24);
|
|
22
22
|
|
|
23
|
+
// Duplicate here
|
|
24
|
+
type TPriceExpanded = TPrice & {
|
|
25
|
+
object: 'price';
|
|
26
|
+
product: any;
|
|
27
|
+
currency: TPaymentCurrency;
|
|
28
|
+
upsell?: {
|
|
29
|
+
upsells_to: TPriceExpanded;
|
|
30
|
+
upsells_to_id: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
type TLineItemExpanded = LineItem & { price: TPriceExpanded; upsell_price: TPriceExpanded };
|
|
34
|
+
|
|
23
35
|
// @link https://stripe.com/docs/api/prices
|
|
24
36
|
export class Price extends Model<InferAttributes<Price>, InferCreationAttributes<Price>> {
|
|
25
37
|
// Unique identifier for the object.
|
|
@@ -148,6 +160,10 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
148
160
|
type: DataTypes.JSON,
|
|
149
161
|
defaultValue: [],
|
|
150
162
|
},
|
|
163
|
+
upsell: {
|
|
164
|
+
type: DataTypes.JSON,
|
|
165
|
+
allowNull: true,
|
|
166
|
+
},
|
|
151
167
|
created_at: {
|
|
152
168
|
type: DataTypes.DATE,
|
|
153
169
|
defaultValue: DataTypes.NOW,
|
|
@@ -207,29 +223,33 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
207
223
|
return 'standard';
|
|
208
224
|
}
|
|
209
225
|
|
|
210
|
-
public static
|
|
211
|
-
if (price.type
|
|
212
|
-
if (
|
|
213
|
-
|
|
226
|
+
public static formatBeforeSave(price: Partial<TPrice & { model: string }>) {
|
|
227
|
+
if (price.type) {
|
|
228
|
+
if (price.type === 'recurring') {
|
|
229
|
+
if (!price.recurring) {
|
|
230
|
+
throw new Error('recurring config is required for recurring prices');
|
|
231
|
+
}
|
|
232
|
+
price.recurring.interval_count = Number(price.recurring.interval_count);
|
|
233
|
+
} else {
|
|
234
|
+
price.recurring = null;
|
|
214
235
|
}
|
|
215
|
-
price.recurring.interval_count = Number(price.recurring.interval_count);
|
|
216
|
-
} else {
|
|
217
|
-
price.recurring = null;
|
|
218
236
|
}
|
|
219
237
|
|
|
220
|
-
if (price.model
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
if (price.model) {
|
|
239
|
+
if (['graduated', 'volume'].includes(price.model)) {
|
|
240
|
+
price.billing_scheme = 'tiered';
|
|
241
|
+
price.tiers_mode = price.model;
|
|
242
|
+
if (isEmpty(price.tiers)) {
|
|
243
|
+
throw new Error('tiers is required for graduated and volume prices');
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
price.billing_scheme = 'per_unit';
|
|
247
|
+
price.tiers = null;
|
|
225
248
|
}
|
|
226
|
-
} else {
|
|
227
|
-
price.billing_scheme = 'per_unit';
|
|
228
|
-
price.tiers = null;
|
|
229
|
-
}
|
|
230
249
|
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
if (price.model !== 'package') {
|
|
251
|
+
price.transform_quantity = null;
|
|
252
|
+
}
|
|
233
253
|
}
|
|
234
254
|
|
|
235
255
|
price.metadata = formatMetadata(price.metadata);
|
|
@@ -237,6 +257,28 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
237
257
|
return price;
|
|
238
258
|
}
|
|
239
259
|
|
|
260
|
+
public static formatAfterRead(doc: TPrice, currencies: TPaymentCurrency[]) {
|
|
261
|
+
if (doc.currency_options) {
|
|
262
|
+
doc.currency_options.forEach((x) => {
|
|
263
|
+
// @ts-ignore
|
|
264
|
+
x.currency = currencies.find((c) => c.id === x.currency_id);
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
doc.currency_options = [
|
|
268
|
+
{
|
|
269
|
+
currency_id: doc.currency_id,
|
|
270
|
+
unit_amount: doc.unit_amount,
|
|
271
|
+
// @ts-ignore
|
|
272
|
+
currency: doc.currency,
|
|
273
|
+
tiers: null,
|
|
274
|
+
custom_unit_amount: null,
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return doc;
|
|
280
|
+
}
|
|
281
|
+
|
|
240
282
|
public static formatCurrencies(options: PriceCurrency[], currencies: TPaymentCurrency[]) {
|
|
241
283
|
return options.map((x) => {
|
|
242
284
|
const currency = currencies.find((c) => c.id === x.currency_id);
|
|
@@ -254,13 +296,20 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
254
296
|
});
|
|
255
297
|
}
|
|
256
298
|
|
|
257
|
-
public static async expand(
|
|
258
|
-
|
|
299
|
+
public static async expand(
|
|
300
|
+
items: LineItem[],
|
|
301
|
+
{ product = true, upsell }: { product?: boolean; upsell?: boolean } = {}
|
|
302
|
+
): Promise<TLineItemExpanded[]> {
|
|
303
|
+
const priceIds: string[] = items
|
|
304
|
+
.map((i) => i.price_id)
|
|
305
|
+
.concat(items.map((i) => i.upsell_price_id as string))
|
|
306
|
+
.filter(Boolean);
|
|
259
307
|
const prices = await Price.findAll({
|
|
260
308
|
where: { id: priceIds },
|
|
261
|
-
include:
|
|
309
|
+
include: product ? [{ model: sequelize.models.Product, as: 'product' }] : [],
|
|
262
310
|
});
|
|
263
311
|
|
|
312
|
+
// expand currency_options
|
|
264
313
|
prices.forEach((x) => {
|
|
265
314
|
if (!x.currency_options || x.currency_options?.length === 0) {
|
|
266
315
|
x.currency_options = [
|
|
@@ -274,10 +323,27 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
274
323
|
}
|
|
275
324
|
});
|
|
276
325
|
|
|
326
|
+
// expand upsell
|
|
327
|
+
if (upsell) {
|
|
328
|
+
await Promise.all(
|
|
329
|
+
prices.map(async (x) => {
|
|
330
|
+
if (x.upsell?.upsells_to_id) {
|
|
331
|
+
const to = await Price.findByPk(x.upsell?.upsells_to_id);
|
|
332
|
+
if (to) {
|
|
333
|
+
// @ts-ignore
|
|
334
|
+
x.upsell.upsells_to = to;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// @ts-ignore
|
|
277
342
|
return items.map((x) => ({
|
|
278
343
|
...x,
|
|
279
344
|
price: prices.find((p) => p.id === x.price_id),
|
|
280
|
-
|
|
345
|
+
upsell_price: x.upsell_price_id ? prices.find((p) => p.id === x.upsell_price_id) : null,
|
|
346
|
+
})) as TLineItemExpanded[];
|
|
281
347
|
}
|
|
282
348
|
|
|
283
349
|
public static async insert(price: TPrice & { model: string }) {
|
|
@@ -289,7 +355,7 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
289
355
|
}
|
|
290
356
|
|
|
291
357
|
// @ts-ignore
|
|
292
|
-
return this.create(this.
|
|
358
|
+
return this.create(this.formatBeforeSave(price));
|
|
293
359
|
}
|
|
294
360
|
|
|
295
361
|
public static findByPkOrLookupKey(id: string, options: FindOptions<Price> = {}) {
|
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.32",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -40,25 +40,25 @@
|
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@arcblock/did": "^1.18.
|
|
43
|
+
"@arcblock/did": "^1.18.93",
|
|
44
44
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
45
|
-
"@arcblock/did-connect": "^2.
|
|
46
|
-
"@arcblock/did-util": "^1.18.
|
|
47
|
-
"@arcblock/ux": "^2.
|
|
48
|
-
"@blocklet/logger": "1.16.17-beta-
|
|
49
|
-
"@blocklet/sdk": "1.16.17-beta-
|
|
50
|
-
"@blocklet/ui-react": "^2.
|
|
51
|
-
"@blocklet/uploader": "^0.0.
|
|
45
|
+
"@arcblock/did-connect": "^2.8.2",
|
|
46
|
+
"@arcblock/did-util": "^1.18.93",
|
|
47
|
+
"@arcblock/ux": "^2.8.2",
|
|
48
|
+
"@blocklet/logger": "1.16.17-beta-952ef53d",
|
|
49
|
+
"@blocklet/sdk": "1.16.17-beta-952ef53d",
|
|
50
|
+
"@blocklet/ui-react": "^2.8.2",
|
|
51
|
+
"@blocklet/uploader": "^0.0.32",
|
|
52
52
|
"@mui/icons-material": "^5.14.13",
|
|
53
53
|
"@mui/lab": "^5.0.0-alpha.148",
|
|
54
54
|
"@mui/material": "^5.14.13",
|
|
55
55
|
"@mui/styles": "^5.14.13",
|
|
56
56
|
"@mui/system": "^5.14.13",
|
|
57
|
-
"@ocap/asset": "^1.18.
|
|
58
|
-
"@ocap/client": "^1.18.
|
|
59
|
-
"@ocap/mcrypto": "^1.18.
|
|
60
|
-
"@ocap/util": "^1.18.
|
|
61
|
-
"@ocap/wallet": "^1.18.
|
|
57
|
+
"@ocap/asset": "^1.18.93",
|
|
58
|
+
"@ocap/client": "^1.18.93",
|
|
59
|
+
"@ocap/mcrypto": "^1.18.93",
|
|
60
|
+
"@ocap/util": "^1.18.93",
|
|
61
|
+
"@ocap/wallet": "^1.18.93",
|
|
62
62
|
"@stripe/react-stripe-js": "^2.3.1",
|
|
63
63
|
"@stripe/stripe-js": "^2.1.7",
|
|
64
64
|
"ahooks": "^3.7.8",
|
|
@@ -100,10 +100,10 @@
|
|
|
100
100
|
"validator": "^13.11.0"
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@abtnode/types": "1.16.17-beta-
|
|
103
|
+
"@abtnode/types": "1.16.17-beta-952ef53d",
|
|
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.32",
|
|
107
107
|
"@types/cookie-parser": "^1.4.4",
|
|
108
108
|
"@types/cors": "^2.8.14",
|
|
109
109
|
"@types/dotenv-flow": "^3.3.1",
|
|
@@ -140,5 +140,5 @@
|
|
|
140
140
|
"parser": "typescript"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
-
"gitHead": "
|
|
143
|
+
"gitHead": "354fff1bd72e201fb51e8cf0ee6d26dbf5211b7c"
|
|
144
144
|
}
|
|
@@ -50,7 +50,9 @@ export default function TxLink(props: { details: PaymentDetails; method: TPaymen
|
|
|
50
50
|
return (
|
|
51
51
|
<Link href={link} target="_blank" rel="noopener noreferrer">
|
|
52
52
|
<Stack component="span" direction="row" alignItems="center" spacing={1}>
|
|
53
|
-
<Typography component="span">
|
|
53
|
+
<Typography component="span" color="primary">
|
|
54
|
+
{text.length > 40 ? [text.slice(0, 8), text.slice(-8)].join('...') : text}
|
|
55
|
+
</Typography>
|
|
54
56
|
<OpenInNewOutlined fontSize="small" />
|
|
55
57
|
</Stack>
|
|
56
58
|
</Link>
|
|
@@ -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 {
|
|
3
4
|
TCheckoutSessionExpanded,
|
|
4
5
|
TCustomer,
|
|
@@ -9,11 +10,13 @@ import type {
|
|
|
9
10
|
} from '@did-pay/types';
|
|
10
11
|
import { Box, Fade, Stack } from '@mui/material';
|
|
11
12
|
import { styled } from '@mui/system';
|
|
13
|
+
import { useSetState } from 'ahooks';
|
|
12
14
|
import { useEffect } from 'react';
|
|
13
15
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
14
16
|
|
|
15
17
|
import { useSessionContext } from '../../contexts/session';
|
|
16
18
|
import { useSettingsContext } from '../../contexts/settings';
|
|
19
|
+
import api from '../../libs/api';
|
|
17
20
|
import { findCurrency, formatError, getStatementDescriptor, isValidCountry } from '../../libs/util';
|
|
18
21
|
import PaymentError from './error';
|
|
19
22
|
import CheckoutFooter from './footer';
|
|
@@ -57,6 +60,7 @@ export default function CheckoutPay({
|
|
|
57
60
|
onPaid,
|
|
58
61
|
onError,
|
|
59
62
|
}: Props) {
|
|
63
|
+
const { t } = useLocaleContext();
|
|
60
64
|
const { refresh, livemode, setLivemode } = useSettingsContext();
|
|
61
65
|
|
|
62
66
|
useEffect(() => {
|
|
@@ -92,22 +96,12 @@ export default function CheckoutPay({
|
|
|
92
96
|
|
|
93
97
|
// expired session
|
|
94
98
|
if (checkoutSession.expires_at <= Math.round(Date.now() / 1000)) {
|
|
95
|
-
return (
|
|
96
|
-
<PaymentError
|
|
97
|
-
title="Expired Link"
|
|
98
|
-
description="This link has expired. This means that your payment has already been processed or your session has expired."
|
|
99
|
-
/>
|
|
100
|
-
);
|
|
99
|
+
return <PaymentError title={t('checkout.expired.title')} description={t('checkout.expired.description')} />;
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
// completed session
|
|
104
103
|
if (checkoutSession.status === 'complete') {
|
|
105
|
-
return (
|
|
106
|
-
<PaymentError
|
|
107
|
-
title="Checkout Completed"
|
|
108
|
-
description="This checkout session has completed. This means that your payment has already been successfully processed."
|
|
109
|
-
/>
|
|
110
|
-
);
|
|
104
|
+
return <PaymentError title={t('checkout.complete.title')} description={t('checkout.complete.description')} />;
|
|
111
105
|
}
|
|
112
106
|
|
|
113
107
|
return (
|
|
@@ -154,8 +148,9 @@ export function CheckoutPayMain({
|
|
|
154
148
|
const { t } = useLocaleContext();
|
|
155
149
|
const { session } = useSessionContext();
|
|
156
150
|
const { settings } = useSettingsContext();
|
|
151
|
+
const [state, setState] = useSetState({ checkoutSession });
|
|
157
152
|
|
|
158
|
-
const defaultCurrencyId = checkoutSession
|
|
153
|
+
const defaultCurrencyId = state.checkoutSession.currency_id || state.checkoutSession.line_items[0]?.price.currency_id;
|
|
159
154
|
const defaultMethodId = paymentMethods.find((m) => m.payment_currencies.some((c) => c.id === defaultCurrencyId))?.id;
|
|
160
155
|
|
|
161
156
|
const methods = useForm({
|
|
@@ -185,30 +180,55 @@ export function CheckoutPayMain({
|
|
|
185
180
|
(findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) ||
|
|
186
181
|
settings.baseCurrency;
|
|
187
182
|
|
|
183
|
+
const onUpsell = async (from: string, to: string) => {
|
|
184
|
+
try {
|
|
185
|
+
const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/upsell`, { from, to });
|
|
186
|
+
setState({ checkoutSession: data });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error(err);
|
|
189
|
+
Toast.error(formatError(err));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const onDownsell = async (from: string) => {
|
|
194
|
+
try {
|
|
195
|
+
const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/downsell`, { from });
|
|
196
|
+
setState({ checkoutSession: data });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(err);
|
|
199
|
+
Toast.error(formatError(err));
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
188
203
|
return (
|
|
189
204
|
<FormProvider {...methods}>
|
|
190
205
|
<PaymentRoot>
|
|
191
206
|
<Stack direction="row" className="cko-container">
|
|
192
207
|
<Fade in>
|
|
193
208
|
<Stack className="cko-overview" direction="column">
|
|
194
|
-
<PaymentHeader checkoutSession={checkoutSession} />
|
|
195
|
-
<PaymentSummary
|
|
209
|
+
<PaymentHeader checkoutSession={state.checkoutSession} />
|
|
210
|
+
<PaymentSummary
|
|
211
|
+
checkoutSession={state.checkoutSession}
|
|
212
|
+
currency={currency}
|
|
213
|
+
onUpsell={onUpsell}
|
|
214
|
+
onDownsell={onDownsell}
|
|
215
|
+
/>
|
|
196
216
|
</Stack>
|
|
197
217
|
</Fade>
|
|
198
218
|
<Stack className="cko-payment" direction="column" spacing={4}>
|
|
199
219
|
{completed && (
|
|
200
220
|
<PaymentSuccess
|
|
201
|
-
payee={getStatementDescriptor(checkoutSession.line_items)}
|
|
202
|
-
action={checkoutSession.mode}
|
|
221
|
+
payee={getStatementDescriptor(state.checkoutSession.line_items)}
|
|
222
|
+
action={state.checkoutSession.mode}
|
|
203
223
|
message={
|
|
204
224
|
paymentLink?.after_completion?.hosted_confirmation?.custom_message ||
|
|
205
|
-
t(`checkout.completed.${checkoutSession.mode}`)
|
|
225
|
+
t(`checkout.completed.${state.checkoutSession.mode}`)
|
|
206
226
|
}
|
|
207
227
|
/>
|
|
208
228
|
)}
|
|
209
229
|
{!completed && (
|
|
210
230
|
<PaymentForm
|
|
211
|
-
checkoutSession={checkoutSession}
|
|
231
|
+
checkoutSession={state.checkoutSession}
|
|
212
232
|
paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
|
|
213
233
|
paymentIntent={paymentIntent}
|
|
214
234
|
customer={customer}
|
|
@@ -25,15 +25,11 @@ export default function ProductCard({ size, variant, name, logo, description, ex
|
|
|
25
25
|
</Avatar>
|
|
26
26
|
)}
|
|
27
27
|
<Stack direction="column" alignItems="flex-start" justifyContent="space-around">
|
|
28
|
-
<Typography variant="body1" sx={{ fontWeight: 500 }} color="text.primary"
|
|
28
|
+
<Typography variant="body1" sx={{ fontWeight: 500 }} color="text.primary">
|
|
29
29
|
{name}
|
|
30
30
|
</Typography>
|
|
31
31
|
{description && (
|
|
32
|
-
<Typography
|
|
33
|
-
variant="body1"
|
|
34
|
-
sx={{ fontSize: '0.85rem', lineHeight: '120%' }}
|
|
35
|
-
color="text.secondary"
|
|
36
|
-
gutterBottom>
|
|
32
|
+
<Typography variant="body1" sx={{ fontSize: '0.85rem', lineHeight: '120%' }} color="text.secondary">
|
|
37
33
|
{description}
|
|
38
34
|
</Typography>
|
|
39
35
|
)}
|