payment-kit 1.18.32 → 1.18.34
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/setup-intent.ts +13 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +20 -2
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/session.ts +1 -1
- package/api/src/routes/checkout-sessions.ts +232 -44
- package/api/src/routes/subscriptions.ts +12 -3
- package/api/src/store/models/subscription.ts +2 -2
- package/blocklet.yml +1 -1
- package/package.json +16 -16
- package/scripts/sdk.js +8 -0
- package/src/components/price/form.tsx +1 -0
- package/src/components/subscription/portal/actions.tsx +12 -0
- package/src/pages/customer/invoice/detail.tsx +1 -0
- package/src/pages/customer/invoice/past-due.tsx +7 -1
- package/src/pages/customer/recharge/account.tsx +6 -0
- package/src/pages/customer/recharge/subscription.tsx +6 -0
- package/src/pages/customer/subscription/change-payment.tsx +7 -1
- package/src/pages/customer/subscription/change-plan.tsx +1 -0
- package/src/pages/customer/subscription/detail.tsx +1 -1
|
@@ -3,6 +3,7 @@ import type Stripe from 'stripe';
|
|
|
3
3
|
import logger from '../../../libs/logger';
|
|
4
4
|
import { CheckoutSession, Lock, SetupIntent, Subscription, TEventExpanded } from '../../../store/models';
|
|
5
5
|
import { updateGroupSubscriptionsPaymentMethod } from '../resource';
|
|
6
|
+
import { getCheckoutSessionSubscriptionIds } from '../../../libs/session';
|
|
6
7
|
|
|
7
8
|
async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
8
9
|
const subscription = await Subscription.findOne({
|
|
@@ -22,8 +23,18 @@ async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeI
|
|
|
22
23
|
|
|
23
24
|
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
24
25
|
if (checkoutSession && checkoutSession.status === 'open') {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
27
|
+
if (subscriptionIds.length <= 1) {
|
|
28
|
+
const updates: Partial<CheckoutSession> = {
|
|
29
|
+
status: 'complete',
|
|
30
|
+
payment_status: 'no_payment_required',
|
|
31
|
+
};
|
|
32
|
+
if (checkoutSession.subscription_id) {
|
|
33
|
+
updates.success_subscription_count = 1;
|
|
34
|
+
}
|
|
35
|
+
await checkoutSession.update(updates);
|
|
36
|
+
logger.info('checkout session become complete on stripe intent succeeded', checkoutSession.id);
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
return;
|
|
@@ -5,6 +5,7 @@ import logger from '../../../libs/logger';
|
|
|
5
5
|
import { finalizeStripeSubscriptionUpdate } from '../../../libs/subscription';
|
|
6
6
|
import { CheckoutSession, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
|
|
7
7
|
import { getCheckoutSessionSubscriptionIds } from '../../../libs/session';
|
|
8
|
+
import { createEvent } from '../../../libs/audit';
|
|
8
9
|
|
|
9
10
|
export async function handleStripeSubscriptionSucceed(subscription: Subscription, status: string) {
|
|
10
11
|
if (!subscription.payment_details?.stripe?.subscription_id) {
|
|
@@ -17,6 +18,20 @@ export async function handleStripeSubscriptionSucceed(subscription: Subscription
|
|
|
17
18
|
const result: any = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id, {
|
|
18
19
|
expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
|
|
19
20
|
});
|
|
21
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
22
|
+
let subscriptionIds: string[] = [subscription.id];
|
|
23
|
+
if (checkoutSession) {
|
|
24
|
+
subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
25
|
+
}
|
|
26
|
+
const isMultipleSubscriptions = subscriptionIds.length > 1 && checkoutSession?.enable_subscription_grouping;
|
|
27
|
+
// if not multiple subscriptions, check if setup is done
|
|
28
|
+
if (!isMultipleSubscriptions && result.pending_setup_intent && result.pending_setup_intent.status !== 'succeeded') {
|
|
29
|
+
logger.warn('subscription can not active because stripe setup not done', {
|
|
30
|
+
id: subscription.id,
|
|
31
|
+
status: result.pending_setup_intent.status,
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
20
35
|
const paymentIntent = result.latest_invoice?.payment_intent;
|
|
21
36
|
const paymentIntentStatus = typeof paymentIntent === 'string' ? paymentIntent : paymentIntent?.status;
|
|
22
37
|
if (result.latest_invoice?.payment_intent && paymentIntentStatus !== 'succeeded') {
|
|
@@ -28,13 +43,16 @@ export async function handleStripeSubscriptionSucceed(subscription: Subscription
|
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
await subscription.update({ status });
|
|
46
|
+
if (subscription.trial_end && subscription.trial_end > Date.now() / 1000 && subscription.status === 'trialing') {
|
|
47
|
+
createEvent('Subscription', 'customer.subscription.trial_start', subscription).catch(console.error);
|
|
48
|
+
} else if (subscription.status === 'active') {
|
|
49
|
+
createEvent('Subscription', 'customer.subscription.started', subscription).catch(console.error);
|
|
50
|
+
}
|
|
31
51
|
logger.info('subscription become active on stripe event', { id: subscription.id, status: subscription.status });
|
|
32
52
|
|
|
33
|
-
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
34
53
|
if (checkoutSession && checkoutSession.status === 'open') {
|
|
35
54
|
await checkoutSession.increment('success_subscription_count', { by: 1 });
|
|
36
55
|
await checkoutSession.reload();
|
|
37
|
-
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
38
56
|
if (checkoutSession.success_subscription_count === subscriptionIds.length) {
|
|
39
57
|
await checkoutSession.update({
|
|
40
58
|
status: 'complete',
|
|
@@ -87,7 +87,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
87
87
|
|
|
88
88
|
return Boolean(
|
|
89
89
|
['disabled', 'minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string) &&
|
|
90
|
-
(invoice?.payment_intent_id || (invoice && +invoice.
|
|
90
|
+
(invoice?.payment_intent_id || (invoice && +invoice.amount_remaining === 0))
|
|
91
91
|
);
|
|
92
92
|
},
|
|
93
93
|
{ timeout: 1000 * 10, interval: 1000 }
|
package/api/src/libs/session.ts
CHANGED
|
@@ -773,7 +773,7 @@ async function createOrUpdateSubscription(params: {
|
|
|
773
773
|
notification_settings: checkoutSession.subscription_data.notification_settings,
|
|
774
774
|
}
|
|
775
775
|
: {}),
|
|
776
|
-
...omit(checkoutSession.metadata || {}, ['days_until_due', 'days_until_cancel']),
|
|
776
|
+
...omit(checkoutSession.metadata || {}, ['days_until_due', 'days_until_cancel', 'page_info']),
|
|
777
777
|
...(itemsSubscriptionData.metadata || {}),
|
|
778
778
|
...metadata,
|
|
779
779
|
};
|
|
@@ -598,6 +598,7 @@ async function processSubscriptionFastCheckout({
|
|
|
598
598
|
lineItems,
|
|
599
599
|
trialEnd,
|
|
600
600
|
now,
|
|
601
|
+
executePayment = true,
|
|
601
602
|
}: {
|
|
602
603
|
checkoutSession: CheckoutSession;
|
|
603
604
|
customer: Customer;
|
|
@@ -608,6 +609,7 @@ async function processSubscriptionFastCheckout({
|
|
|
608
609
|
lineItems: TLineItemExpanded[];
|
|
609
610
|
trialEnd: number;
|
|
610
611
|
now: number;
|
|
612
|
+
executePayment?: boolean;
|
|
611
613
|
}): Promise<{
|
|
612
614
|
success: boolean;
|
|
613
615
|
invoices?: Invoice[];
|
|
@@ -651,38 +653,42 @@ async function processSubscriptionFastCheckout({
|
|
|
651
653
|
subscriptionIds: subscriptions.map((s) => s.id),
|
|
652
654
|
});
|
|
653
655
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
+
if (executePayment) {
|
|
657
|
+
// Update payment settings for all subscriptions
|
|
658
|
+
await Promise.all(subscriptions.map((sub) => sub.update({ payment_settings: paymentSettings })));
|
|
656
659
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
);
|
|
673
|
-
|
|
674
|
-
// Add subscription cycle jobs
|
|
675
|
-
await Promise.all(subscriptions.map((sub) => addSubscriptionJob(sub, 'cycle', false, sub.trial_end)));
|
|
660
|
+
// Create invoices for all subscriptions
|
|
661
|
+
const { invoices } = await ensureInvoicesForSubscriptions({
|
|
662
|
+
checkoutSession,
|
|
663
|
+
customer,
|
|
664
|
+
subscriptions,
|
|
665
|
+
});
|
|
666
|
+
// Update invoice settings and push to queue
|
|
667
|
+
await Promise.all(
|
|
668
|
+
invoices.map(async (invoice) => {
|
|
669
|
+
if (invoice) {
|
|
670
|
+
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
671
|
+
invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
);
|
|
676
675
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
subscriptionIds: subscriptions.map((s) => s.id),
|
|
680
|
-
invoiceIds: invoices.map((inv) => inv.id),
|
|
681
|
-
});
|
|
676
|
+
// Add subscription cycle jobs
|
|
677
|
+
await Promise.all(subscriptions.map((sub) => addSubscriptionJob(sub, 'cycle', false, sub.trial_end)));
|
|
682
678
|
|
|
679
|
+
logger.info('Created and queued invoices for fast checkout with subscriptions', {
|
|
680
|
+
checkoutSessionId: checkoutSession.id,
|
|
681
|
+
subscriptionIds: subscriptions.map((s) => s.id),
|
|
682
|
+
invoiceIds: invoices.map((inv) => inv.id),
|
|
683
|
+
});
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
invoices,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
683
689
|
return {
|
|
684
690
|
success: true,
|
|
685
|
-
invoices,
|
|
691
|
+
invoices: [],
|
|
686
692
|
};
|
|
687
693
|
} catch (error) {
|
|
688
694
|
logger.error('Error processing subscription fast checkout', {
|
|
@@ -1217,6 +1223,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1217
1223
|
|
|
1218
1224
|
const isPayment = checkoutSession.mode === 'payment';
|
|
1219
1225
|
let canFastPay = isPayment && canPayWithDelegation(paymentIntent?.beneficiaries || []);
|
|
1226
|
+
let fastPayInfo = null;
|
|
1220
1227
|
let delegation: SufficientForPaymentResult | null = null;
|
|
1221
1228
|
if (isPayment && paymentIntent && canFastPay) {
|
|
1222
1229
|
// if we can complete purchase without any wallet interaction
|
|
@@ -1226,24 +1233,12 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1226
1233
|
userDid: customer.did,
|
|
1227
1234
|
amount: fastCheckoutAmount,
|
|
1228
1235
|
});
|
|
1229
|
-
if (balance.sufficient) {
|
|
1230
|
-
logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
|
|
1231
|
-
}
|
|
1232
|
-
if (delegation.sufficient) {
|
|
1233
|
-
logger.info(`CheckoutSession ${checkoutSession.id} will pay from delegation ${paymentIntent?.id}`);
|
|
1234
|
-
}
|
|
1235
1236
|
if (balance.sufficient || delegation.sufficient) {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
} else {
|
|
1242
|
-
paymentQueue.push({
|
|
1243
|
-
id: paymentIntent.id,
|
|
1244
|
-
job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1237
|
+
fastPayInfo = {
|
|
1238
|
+
type: balance.sufficient ? 'balance' : 'delegation',
|
|
1239
|
+
amount: fastCheckoutAmount,
|
|
1240
|
+
payer: customer.did,
|
|
1241
|
+
};
|
|
1247
1242
|
}
|
|
1248
1243
|
} else if (
|
|
1249
1244
|
paymentMethod.type === 'arcblock' &&
|
|
@@ -1261,6 +1256,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1261
1256
|
lineItems,
|
|
1262
1257
|
trialEnd,
|
|
1263
1258
|
now,
|
|
1259
|
+
executePayment: false,
|
|
1264
1260
|
});
|
|
1265
1261
|
|
|
1266
1262
|
if (!result.success) {
|
|
@@ -1272,6 +1268,11 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1272
1268
|
sufficient: true,
|
|
1273
1269
|
};
|
|
1274
1270
|
canFastPay = true;
|
|
1271
|
+
fastPayInfo = {
|
|
1272
|
+
type: 'delegation',
|
|
1273
|
+
amount: fastCheckoutAmount,
|
|
1274
|
+
payer: customer.did,
|
|
1275
|
+
};
|
|
1275
1276
|
}
|
|
1276
1277
|
}
|
|
1277
1278
|
|
|
@@ -1403,6 +1404,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1403
1404
|
customer,
|
|
1404
1405
|
delegation: canFastPay ? delegation : null,
|
|
1405
1406
|
balance: canFastPay ? balance : null,
|
|
1407
|
+
fastPayInfo,
|
|
1406
1408
|
});
|
|
1407
1409
|
} catch (err) {
|
|
1408
1410
|
logger.error('Error submitting checkout session', {
|
|
@@ -1480,6 +1482,192 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
1480
1482
|
}
|
|
1481
1483
|
});
|
|
1482
1484
|
|
|
1485
|
+
router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
1486
|
+
try {
|
|
1487
|
+
if (!req.user) {
|
|
1488
|
+
return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
1492
|
+
|
|
1493
|
+
if (checkoutSession.line_items) {
|
|
1494
|
+
try {
|
|
1495
|
+
await validateInventory(checkoutSession.line_items);
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
logger.error('validateInventory failed', {
|
|
1498
|
+
error: err,
|
|
1499
|
+
line_items: checkoutSession.line_items,
|
|
1500
|
+
checkoutSessionId: checkoutSession.id,
|
|
1501
|
+
});
|
|
1502
|
+
return res.status(400).json({ error: err.message });
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
// validate cross sell
|
|
1506
|
+
if (checkoutSession.cross_sell_behavior === 'required') {
|
|
1507
|
+
if (checkoutSession.line_items.some((x) => x.cross_sell) === false) {
|
|
1508
|
+
const result = await getCrossSellItem(checkoutSession);
|
|
1509
|
+
// @ts-ignore
|
|
1510
|
+
if (result.id) {
|
|
1511
|
+
return res
|
|
1512
|
+
.status(400)
|
|
1513
|
+
.json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' });
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const paymentCurrency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
|
|
1519
|
+
if (!paymentCurrency) {
|
|
1520
|
+
return res.status(400).json({ error: 'Payment currency not found' });
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
1524
|
+
if (!paymentMethod) {
|
|
1525
|
+
return res.status(400).json({ error: 'Payment method not found' });
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (paymentMethod.type !== 'arcblock') {
|
|
1529
|
+
return res.status(400).json({ error: 'Payment method not supported for fast checkout' });
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const customer = await Customer.findByPkOrDid(req.user.did);
|
|
1533
|
+
if (!customer) {
|
|
1534
|
+
return res.status(400).json({ error: '' });
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// check if customer can make new purchase
|
|
1538
|
+
const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
|
|
1539
|
+
if (!canMakeNewPurchase) {
|
|
1540
|
+
return res.status(403).json({
|
|
1541
|
+
code: 'CUSTOMER_LIMITED',
|
|
1542
|
+
error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const { lineItems, trialInDays, trialEnd, now } = await calculateAndUpdateAmount(
|
|
1547
|
+
checkoutSession,
|
|
1548
|
+
paymentCurrency.id,
|
|
1549
|
+
true
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
let paymentIntent: PaymentIntent | null = null;
|
|
1553
|
+
if (checkoutSession.mode === 'payment') {
|
|
1554
|
+
const result = await createOrUpdatePaymentIntent(
|
|
1555
|
+
checkoutSession,
|
|
1556
|
+
paymentMethod,
|
|
1557
|
+
paymentCurrency,
|
|
1558
|
+
lineItems,
|
|
1559
|
+
customer.id,
|
|
1560
|
+
customer.email
|
|
1561
|
+
);
|
|
1562
|
+
paymentIntent = result.paymentIntent;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const fastCheckoutAmount = getFastCheckoutAmount(
|
|
1566
|
+
lineItems,
|
|
1567
|
+
checkoutSession.mode,
|
|
1568
|
+
paymentCurrency.id,
|
|
1569
|
+
trialInDays > 0 || trialEnd > now
|
|
1570
|
+
);
|
|
1571
|
+
|
|
1572
|
+
const paymentSettings = {
|
|
1573
|
+
payment_method_types: checkoutSession.payment_method_types,
|
|
1574
|
+
payment_method_options: {
|
|
1575
|
+
[paymentMethod.type]: { payer: customer.did },
|
|
1576
|
+
},
|
|
1577
|
+
};
|
|
1578
|
+
const balance = isCreditSufficientForPayment({
|
|
1579
|
+
paymentMethod,
|
|
1580
|
+
paymentCurrency,
|
|
1581
|
+
customer,
|
|
1582
|
+
amount: fastCheckoutAmount,
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
const isPayment = checkoutSession.mode === 'payment';
|
|
1586
|
+
let fastPaid = false;
|
|
1587
|
+
let canFastPay = isPayment && canPayWithDelegation(paymentIntent?.beneficiaries || []);
|
|
1588
|
+
let delegation: SufficientForPaymentResult | null = null;
|
|
1589
|
+
if (isPayment && paymentIntent && canFastPay) {
|
|
1590
|
+
// if we can complete purchase without any wallet interaction
|
|
1591
|
+
delegation = await isDelegationSufficientForPayment({
|
|
1592
|
+
paymentMethod,
|
|
1593
|
+
paymentCurrency,
|
|
1594
|
+
userDid: customer.did,
|
|
1595
|
+
amount: fastCheckoutAmount,
|
|
1596
|
+
});
|
|
1597
|
+
if (balance.sufficient) {
|
|
1598
|
+
logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
|
|
1599
|
+
}
|
|
1600
|
+
if (delegation.sufficient) {
|
|
1601
|
+
logger.info(`CheckoutSession ${checkoutSession.id} will pay from delegation ${paymentIntent?.id}`);
|
|
1602
|
+
}
|
|
1603
|
+
if (balance.sufficient || delegation.sufficient) {
|
|
1604
|
+
fastPaid = true;
|
|
1605
|
+
await paymentIntent.update({ status: 'requires_capture' });
|
|
1606
|
+
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
|
|
1607
|
+
if (invoice) {
|
|
1608
|
+
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
1609
|
+
invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
1610
|
+
} else {
|
|
1611
|
+
paymentQueue.push({
|
|
1612
|
+
id: paymentIntent.id,
|
|
1613
|
+
job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
} else if (
|
|
1618
|
+
paymentMethod.type === 'arcblock' &&
|
|
1619
|
+
checkoutSession.mode === 'subscription' &&
|
|
1620
|
+
checkoutSession.subscription_data?.no_stake
|
|
1621
|
+
) {
|
|
1622
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
1623
|
+
const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
|
|
1624
|
+
// if we can complete purchase without any wallet interaction
|
|
1625
|
+
const result = await processSubscriptionFastCheckout({
|
|
1626
|
+
checkoutSession,
|
|
1627
|
+
customer,
|
|
1628
|
+
subscriptions,
|
|
1629
|
+
paymentMethod,
|
|
1630
|
+
paymentCurrency,
|
|
1631
|
+
paymentSettings,
|
|
1632
|
+
lineItems,
|
|
1633
|
+
trialEnd,
|
|
1634
|
+
now,
|
|
1635
|
+
});
|
|
1636
|
+
if (!result.success) {
|
|
1637
|
+
logger.warn(`Fast checkout processing failed: ${result.message}`, {
|
|
1638
|
+
checkoutSessionId: checkoutSession.id,
|
|
1639
|
+
});
|
|
1640
|
+
} else {
|
|
1641
|
+
fastPaid = true;
|
|
1642
|
+
delegation = {
|
|
1643
|
+
sufficient: true,
|
|
1644
|
+
};
|
|
1645
|
+
canFastPay = true;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
logger.info('Checkout session submitted successfully', {
|
|
1650
|
+
sessionId: req.params.id,
|
|
1651
|
+
paymentIntentId: paymentIntent?.id,
|
|
1652
|
+
customerId: customer?.id,
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
return res.json({
|
|
1656
|
+
paymentIntent,
|
|
1657
|
+
checkoutSession,
|
|
1658
|
+
customer,
|
|
1659
|
+
fastPaid,
|
|
1660
|
+
});
|
|
1661
|
+
} catch (err) {
|
|
1662
|
+
logger.error('Error confirming fast checkout', {
|
|
1663
|
+
sessionId: req.params.id,
|
|
1664
|
+
error: err.message,
|
|
1665
|
+
stack: err.stack,
|
|
1666
|
+
});
|
|
1667
|
+
res.status(500).json({ code: err.code, error: err.message });
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1483
1671
|
// upsell
|
|
1484
1672
|
router.put('/:id/upsell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
1485
1673
|
try {
|
|
@@ -2032,21 +2032,30 @@ router.post('/:id/overdraft-protection', authPortal, async (req, res) => {
|
|
|
2032
2032
|
if (overdraftProtectionError) {
|
|
2033
2033
|
return res.status(400).json({ error: `Overdraft protection invalid: ${overdraftProtectionError.message}` });
|
|
2034
2034
|
}
|
|
2035
|
+
|
|
2035
2036
|
const subscription = await Subscription.findByPk(req.params.id);
|
|
2036
2037
|
if (!subscription) {
|
|
2037
2038
|
return res.status(404).json({ error: 'Subscription not found' });
|
|
2038
2039
|
}
|
|
2040
|
+
const previousOverdraftProtection = {
|
|
2041
|
+
enabled: subscription.overdraft_protection?.enabled || false,
|
|
2042
|
+
payment_method_id: subscription.overdraft_protection?.payment_method_id || null,
|
|
2043
|
+
payment_details: subscription.overdraft_protection?.payment_details || null,
|
|
2044
|
+
};
|
|
2039
2045
|
const customer = await Customer.findByPkOrDid(req.user?.did as string);
|
|
2040
2046
|
if (!customer) {
|
|
2041
2047
|
return res.status(404).json({ error: 'Customer not found' });
|
|
2042
2048
|
}
|
|
2043
|
-
const { remaining, used } = await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
2049
|
+
const { remaining, used, unused } = await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
2050
|
+
if (unused === '0' && !amount && enabled) {
|
|
2051
|
+
return res.status(400).json({ error: 'Please add stake to enable SubGuard™' });
|
|
2052
|
+
}
|
|
2044
2053
|
if (returnStake && remaining !== '0' && !enabled) {
|
|
2045
2054
|
// disable overdraft protection
|
|
2046
2055
|
await subscription.update({
|
|
2047
2056
|
// @ts-ignore
|
|
2048
2057
|
overdraft_protection: {
|
|
2049
|
-
...
|
|
2058
|
+
...previousOverdraftProtection,
|
|
2050
2059
|
enabled: false,
|
|
2051
2060
|
},
|
|
2052
2061
|
});
|
|
@@ -2077,7 +2086,7 @@ router.post('/:id/overdraft-protection', authPortal, async (req, res) => {
|
|
|
2077
2086
|
await subscription.update({
|
|
2078
2087
|
// @ts-ignore
|
|
2079
2088
|
overdraft_protection: {
|
|
2080
|
-
...
|
|
2089
|
+
...previousOverdraftProtection,
|
|
2081
2090
|
enabled,
|
|
2082
2091
|
},
|
|
2083
2092
|
});
|
|
@@ -320,11 +320,11 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
320
320
|
overdraft_protection: {
|
|
321
321
|
type: DataTypes.JSON,
|
|
322
322
|
allowNull: true,
|
|
323
|
-
defaultValue:
|
|
323
|
+
defaultValue: {
|
|
324
324
|
enabled: false,
|
|
325
325
|
payment_method_id: null,
|
|
326
326
|
payment_details: null,
|
|
327
|
-
}
|
|
327
|
+
},
|
|
328
328
|
},
|
|
329
329
|
},
|
|
330
330
|
{
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.34",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -45,29 +45,29 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@abtnode/cron": "^1.16.42",
|
|
48
|
-
"@arcblock/did": "^1.20.
|
|
48
|
+
"@arcblock/did": "^1.20.2",
|
|
49
49
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
50
|
-
"@arcblock/did-connect": "^2.13.
|
|
51
|
-
"@arcblock/did-util": "^1.20.
|
|
52
|
-
"@arcblock/jwt": "^1.20.
|
|
53
|
-
"@arcblock/ux": "^2.13.
|
|
54
|
-
"@arcblock/validator": "^1.20.
|
|
50
|
+
"@arcblock/did-connect": "^2.13.12",
|
|
51
|
+
"@arcblock/did-util": "^1.20.2",
|
|
52
|
+
"@arcblock/jwt": "^1.20.2",
|
|
53
|
+
"@arcblock/ux": "^2.13.12",
|
|
54
|
+
"@arcblock/validator": "^1.20.2",
|
|
55
55
|
"@blocklet/js-sdk": "^1.16.42",
|
|
56
56
|
"@blocklet/logger": "^1.16.42",
|
|
57
|
-
"@blocklet/payment-react": "1.18.
|
|
57
|
+
"@blocklet/payment-react": "1.18.34",
|
|
58
58
|
"@blocklet/sdk": "^1.16.42",
|
|
59
|
-
"@blocklet/ui-react": "^2.13.
|
|
59
|
+
"@blocklet/ui-react": "^2.13.12",
|
|
60
60
|
"@blocklet/uploader": "^0.1.83",
|
|
61
61
|
"@blocklet/xss": "^0.1.32",
|
|
62
62
|
"@mui/icons-material": "^5.16.6",
|
|
63
63
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
64
64
|
"@mui/material": "^5.16.6",
|
|
65
65
|
"@mui/system": "^5.16.6",
|
|
66
|
-
"@ocap/asset": "^1.20.
|
|
67
|
-
"@ocap/client": "^1.20.
|
|
68
|
-
"@ocap/mcrypto": "^1.20.
|
|
69
|
-
"@ocap/util": "^1.20.
|
|
70
|
-
"@ocap/wallet": "^1.20.
|
|
66
|
+
"@ocap/asset": "^1.20.2",
|
|
67
|
+
"@ocap/client": "^1.20.2",
|
|
68
|
+
"@ocap/mcrypto": "^1.20.2",
|
|
69
|
+
"@ocap/util": "^1.20.2",
|
|
70
|
+
"@ocap/wallet": "^1.20.2",
|
|
71
71
|
"@stripe/react-stripe-js": "^2.7.3",
|
|
72
72
|
"@stripe/stripe-js": "^2.4.0",
|
|
73
73
|
"ahooks": "^3.8.0",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"devDependencies": {
|
|
123
123
|
"@abtnode/types": "^1.16.42",
|
|
124
124
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
125
|
-
"@blocklet/payment-types": "1.18.
|
|
125
|
+
"@blocklet/payment-types": "1.18.34",
|
|
126
126
|
"@types/cookie-parser": "^1.4.7",
|
|
127
127
|
"@types/cors": "^2.8.17",
|
|
128
128
|
"@types/debug": "^4.1.12",
|
|
@@ -168,5 +168,5 @@
|
|
|
168
168
|
"parser": "typescript"
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
|
-
"gitHead": "
|
|
171
|
+
"gitHead": "0ddb7c08956891409919c8027b56b77eba1726d4"
|
|
172
172
|
}
|
package/scripts/sdk.js
CHANGED
|
@@ -69,6 +69,14 @@ const checkoutModule = {
|
|
|
69
69
|
subscription_data: {
|
|
70
70
|
no_stake: true,
|
|
71
71
|
},
|
|
72
|
+
metadata: {
|
|
73
|
+
page_info: {
|
|
74
|
+
form_purpose_description: {
|
|
75
|
+
en: 'Information collected helps us process your payment and deliver our services.',
|
|
76
|
+
zh: '收集的信息帮助我们处理您的付款并提供服务。',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
72
80
|
});
|
|
73
81
|
console.log('createBatchSubscription', checkoutSession);
|
|
74
82
|
return checkoutSession;
|
|
@@ -467,6 +467,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
467
467
|
<option value="day">{t('common.days')}</option>
|
|
468
468
|
<option value="week">{t('common.weeks')}</option>
|
|
469
469
|
<option value="month">{t('common.months')}</option>
|
|
470
|
+
<option value="year">{t('common.years')}</option>
|
|
470
471
|
</select>
|
|
471
472
|
</InputAdornment>
|
|
472
473
|
),
|
|
@@ -241,8 +241,14 @@ export function SubscriptionActionsInner({
|
|
|
241
241
|
containerEl: undefined as unknown as Element,
|
|
242
242
|
saveConnect: false,
|
|
243
243
|
action: 'delegation',
|
|
244
|
+
locale: locale as 'en' | 'zh',
|
|
244
245
|
prefix: joinURL(getPrefix(), '/api/did'),
|
|
245
246
|
extraParams: { subscriptionId: subscription.id, sessionUserDid: session.user.did },
|
|
247
|
+
messages: {
|
|
248
|
+
scan: t('common.connect.defaultScan'),
|
|
249
|
+
title: t('customer.delegation.title'),
|
|
250
|
+
confirm: t('common.connect.confirm'),
|
|
251
|
+
} as any,
|
|
246
252
|
onSuccess: () => {
|
|
247
253
|
connect.close();
|
|
248
254
|
Toast.success(t('customer.delegation.success'));
|
|
@@ -287,9 +293,15 @@ export function SubscriptionActionsInner({
|
|
|
287
293
|
connect.open({
|
|
288
294
|
containerEl: undefined as unknown as Element,
|
|
289
295
|
saveConnect: false,
|
|
296
|
+
locale: locale as 'en' | 'zh',
|
|
290
297
|
action: 'overdraft-protection',
|
|
291
298
|
prefix: joinURL(getPrefix(), '/api/did'),
|
|
292
299
|
extraParams: { subscriptionId: subscription.id, amount, sessionUserDid: session.user.did },
|
|
300
|
+
messages: {
|
|
301
|
+
scan: t('common.connect.defaultScan'),
|
|
302
|
+
title: t('customer.overdraftProtection.title'),
|
|
303
|
+
confirm: t('common.connect.confirm'),
|
|
304
|
+
} as any,
|
|
293
305
|
onSuccess: () => {
|
|
294
306
|
connect.close();
|
|
295
307
|
Toast.success(t('customer.overdraftProtection.settingSuccess'));
|
|
@@ -28,7 +28,7 @@ const fetchData = (): Promise<TCustomerExpanded> => {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export default function CustomerInvoicePastDue() {
|
|
31
|
-
const { t } = useLocaleContext();
|
|
31
|
+
const { t, locale } = useLocaleContext();
|
|
32
32
|
const { events } = useSessionContext();
|
|
33
33
|
const { connect, session } = usePaymentContext();
|
|
34
34
|
const [params] = useSearchParams();
|
|
@@ -67,8 +67,14 @@ export default function CustomerInvoicePastDue() {
|
|
|
67
67
|
containerEl: undefined as unknown as Element,
|
|
68
68
|
saveConnect: false,
|
|
69
69
|
action: 'collect-batch',
|
|
70
|
+
locale: locale as 'en' | 'zh',
|
|
70
71
|
prefix: joinURL(getPrefix(), '/api/did'),
|
|
71
72
|
extraParams: { subscriptionId, currencyId },
|
|
73
|
+
messages: {
|
|
74
|
+
scan: t('common.connect.defaultScan'),
|
|
75
|
+
title: t('payment.customer.invoice.payBatch'),
|
|
76
|
+
confirm: t('common.connect.confirm'),
|
|
77
|
+
} as any,
|
|
72
78
|
onSuccess: () => {
|
|
73
79
|
connect.close();
|
|
74
80
|
},
|
|
@@ -225,12 +225,18 @@ export default function BalanceRechargePage() {
|
|
|
225
225
|
containerEl: undefined as unknown as Element,
|
|
226
226
|
saveConnect: false,
|
|
227
227
|
action: 'recharge-account',
|
|
228
|
+
locale: locale as 'en' | 'zh',
|
|
228
229
|
prefix: joinURL(getPrefix(), '/api/did'),
|
|
229
230
|
extraParams: {
|
|
230
231
|
customerDid: session?.user?.did,
|
|
231
232
|
currencyId: currency.id,
|
|
232
233
|
amount: Number(amount),
|
|
233
234
|
},
|
|
235
|
+
messages: {
|
|
236
|
+
scan: t('common.connect.defaultScan'),
|
|
237
|
+
title: t('customer.recharge.title'),
|
|
238
|
+
confirm: t('common.connect.confirm'),
|
|
239
|
+
} as any,
|
|
234
240
|
onSuccess: () => {
|
|
235
241
|
connect.close();
|
|
236
242
|
Toast.success(t('customer.recharge.success'));
|
|
@@ -163,8 +163,14 @@ export default function RechargePage() {
|
|
|
163
163
|
containerEl: undefined as unknown as Element,
|
|
164
164
|
saveConnect: false,
|
|
165
165
|
action: 'recharge',
|
|
166
|
+
locale: locale as 'en' | 'zh',
|
|
166
167
|
prefix: joinURL(getPrefix(), '/api/did'),
|
|
167
168
|
extraParams: { subscriptionId, amount: Number(amount) },
|
|
169
|
+
messages: {
|
|
170
|
+
scan: t('common.connect.defaultScan'),
|
|
171
|
+
title: t('customer.recharge.title'),
|
|
172
|
+
confirm: t('common.connect.confirm'),
|
|
173
|
+
} as any,
|
|
168
174
|
onSuccess: () => {
|
|
169
175
|
connect.close();
|
|
170
176
|
Toast.success(t('customer.recharge.success'));
|
|
@@ -75,7 +75,7 @@ const waitForCheckoutComplete = async (sessionId: string) => {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
function CustomerSubscriptionChangePayment({ subscription, customer, onComplete }: Props) {
|
|
78
|
-
const { t } = useLocaleContext();
|
|
78
|
+
const { t, locale } = useLocaleContext();
|
|
79
79
|
const navigate = useNavigate();
|
|
80
80
|
const [searchParams] = useSearchParams();
|
|
81
81
|
const { settings, connect } = usePaymentContext();
|
|
@@ -159,11 +159,17 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
159
159
|
await handleConnected();
|
|
160
160
|
} else {
|
|
161
161
|
connect.open({
|
|
162
|
+
locale: locale as 'en' | 'zh',
|
|
162
163
|
containerEl: undefined as unknown as Element,
|
|
163
164
|
action: 'change-payment',
|
|
164
165
|
prefix: joinURL(getPrefix(), '/api/did'),
|
|
165
166
|
saveConnect: false,
|
|
166
167
|
extraParams: { subscriptionId: subscription.id },
|
|
168
|
+
messages: {
|
|
169
|
+
scan: t('common.connect.defaultScan'),
|
|
170
|
+
title: t('payment.customer.changePayment.title'),
|
|
171
|
+
confirm: t('common.connect.confirm'),
|
|
172
|
+
} as any,
|
|
167
173
|
onSuccess: async () => {
|
|
168
174
|
connect.close();
|
|
169
175
|
await handleConnected();
|
|
@@ -79,7 +79,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
79
79
|
loading: overdraftProtectionLoading,
|
|
80
80
|
run: refreshOverdraftProtection,
|
|
81
81
|
} = useRequest(() => fetchOverdraftProtection(id), {
|
|
82
|
-
ready: ['active', 'trialing', 'past_due'].includes(data?.status || ''),
|
|
82
|
+
ready: ['active', 'trialing', 'past_due'].includes(data?.status || '') && data?.paymentMethod?.type === 'arcblock',
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
const {
|