payment-kit 1.21.16 → 1.21.17
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/index.ts +2 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +30 -25
- package/api/src/integrations/stripe/handlers/setup-intent.ts +231 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +31 -9
- package/api/src/integrations/stripe/resource.ts +29 -0
- package/api/src/libs/payment.ts +9 -3
- package/api/src/libs/util.ts +17 -0
- package/api/src/routes/connect/change-payer.ts +148 -0
- package/api/src/routes/connect/shared.ts +30 -0
- package/api/src/routes/invoices.ts +141 -2
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/subscriptions.ts +130 -3
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/components/invoice-pdf/template.tsx +30 -0
- package/src/components/subscription/payment-method-info.tsx +222 -0
- package/src/global.css +4 -0
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/admin/billing/invoices/detail.tsx +5 -3
- package/src/pages/admin/billing/subscriptions/detail.tsx +16 -0
- package/src/pages/admin/overview.tsx +14 -14
- package/src/pages/customer/invoice/detail.tsx +59 -17
- package/src/pages/customer/subscription/detail.tsx +20 -1
|
@@ -1224,6 +1224,36 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
|
|
|
1224
1224
|
};
|
|
1225
1225
|
}
|
|
1226
1226
|
|
|
1227
|
+
export async function ensurePayerChangeContext(subscriptionId: string) {
|
|
1228
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
1229
|
+
if (!subscription) {
|
|
1230
|
+
throw new Error(`Subscription not found: ${subscriptionId}`);
|
|
1231
|
+
}
|
|
1232
|
+
if (!['active', 'trialing', 'past_due'].includes(subscription.status)) {
|
|
1233
|
+
throw new Error(`Subscription ${subscriptionId} is not in a valid status to change payer`);
|
|
1234
|
+
}
|
|
1235
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
1236
|
+
if (!paymentMethod) {
|
|
1237
|
+
throw new Error(`Payment method not found for subscription ${subscriptionId}`);
|
|
1238
|
+
}
|
|
1239
|
+
const payerAddress = getSubscriptionPaymentAddress(subscription, paymentMethod?.type);
|
|
1240
|
+
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
1241
|
+
if (!paymentCurrency) {
|
|
1242
|
+
throw new Error(`PaymentCurrency ${subscription.currency_id} not found for subscription ${subscriptionId}`);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// @ts-ignore
|
|
1246
|
+
subscription.items = await expandSubscriptionItems(subscription.id);
|
|
1247
|
+
|
|
1248
|
+
return {
|
|
1249
|
+
subscription,
|
|
1250
|
+
paymentCurrency,
|
|
1251
|
+
paymentMethod,
|
|
1252
|
+
customer: await Customer.findByPk(subscription.customer_id),
|
|
1253
|
+
payerAddress,
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1227
1257
|
export async function ensureReStakeContext(subscriptionId: string) {
|
|
1228
1258
|
const subscription = await Subscription.findByPk(subscriptionId);
|
|
1229
1259
|
if (!subscription) {
|
|
@@ -8,6 +8,7 @@ import { Op } from 'sequelize';
|
|
|
8
8
|
import { BN } from '@ocap/util';
|
|
9
9
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
10
10
|
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
|
|
11
|
+
import { ensureStripeCustomer, ensureStripeSetupIntentForInvoicePayment } from '../integrations/stripe/resource';
|
|
11
12
|
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
12
13
|
import { authenticate } from '../libs/security';
|
|
13
14
|
import { expandLineItems } from '../libs/session';
|
|
@@ -662,9 +663,11 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
662
663
|
})) as TInvoiceExpanded | null;
|
|
663
664
|
|
|
664
665
|
if (doc) {
|
|
665
|
-
|
|
666
|
+
const shouldSync = req.query.sync === 'true' || !!req.query.forceSync;
|
|
667
|
+
// Sync Stripe invoice when sync=true query parameter is present
|
|
668
|
+
if (doc.metadata?.stripe_id && doc.status !== 'paid') {
|
|
666
669
|
// @ts-ignore
|
|
667
|
-
await syncStripeInvoice(doc);
|
|
670
|
+
await syncStripeInvoice(doc, shouldSync);
|
|
668
671
|
}
|
|
669
672
|
if (doc.payment_intent_id) {
|
|
670
673
|
const paymentIntent = await PaymentIntent.findByPk(doc.payment_intent_id);
|
|
@@ -799,6 +802,142 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
799
802
|
}
|
|
800
803
|
});
|
|
801
804
|
|
|
805
|
+
router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
806
|
+
try {
|
|
807
|
+
const { invoice_ids, subscription_id, customer_id, currency_id } = req.body;
|
|
808
|
+
|
|
809
|
+
if (!currency_id) {
|
|
810
|
+
return res.status(400).json({ error: 'currency_id is required' });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!invoice_ids && !subscription_id && !customer_id) {
|
|
814
|
+
return res.status(400).json({ error: 'Must provide invoice_ids, subscription_id, or customer_id' });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let invoices: Invoice[];
|
|
818
|
+
let customer: Customer | null;
|
|
819
|
+
let paymentMethod: PaymentMethod | null = null;
|
|
820
|
+
|
|
821
|
+
if (invoice_ids && Array.isArray(invoice_ids) && invoice_ids.length > 0) {
|
|
822
|
+
invoices = await Invoice.findAll({
|
|
823
|
+
where: {
|
|
824
|
+
id: { [Op.in]: invoice_ids },
|
|
825
|
+
currency_id,
|
|
826
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
827
|
+
},
|
|
828
|
+
include: [
|
|
829
|
+
{ model: Customer, as: 'customer' },
|
|
830
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
831
|
+
],
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if (invoices.length === 0) {
|
|
835
|
+
return res.status(404).json({ error: 'No payable invoices found' });
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// @ts-ignore
|
|
839
|
+
customer = invoices[0]?.customer;
|
|
840
|
+
paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
|
|
841
|
+
} else if (subscription_id) {
|
|
842
|
+
const subscription = await Subscription.findByPk(subscription_id, {
|
|
843
|
+
include: [{ model: Customer, as: 'customer' }],
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
if (!subscription) {
|
|
847
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// @ts-ignore
|
|
851
|
+
customer = subscription.customer;
|
|
852
|
+
paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
853
|
+
|
|
854
|
+
invoices = await Invoice.findAll({
|
|
855
|
+
where: {
|
|
856
|
+
subscription_id,
|
|
857
|
+
currency_id,
|
|
858
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
859
|
+
},
|
|
860
|
+
include: [
|
|
861
|
+
{ model: Customer, as: 'customer' },
|
|
862
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
863
|
+
],
|
|
864
|
+
});
|
|
865
|
+
} else {
|
|
866
|
+
customer = await Customer.findByPkOrDid(customer_id!);
|
|
867
|
+
if (!customer) {
|
|
868
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
invoices = await Invoice.findAll({
|
|
872
|
+
where: {
|
|
873
|
+
customer_id: customer.id,
|
|
874
|
+
currency_id,
|
|
875
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
876
|
+
},
|
|
877
|
+
include: [
|
|
878
|
+
{ model: Customer, as: 'customer' },
|
|
879
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
880
|
+
],
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (invoices.length === 0) {
|
|
884
|
+
return res.status(404).json({ error: 'No payable invoices found' });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (!customer) {
|
|
891
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (!paymentMethod || paymentMethod.type !== 'stripe') {
|
|
895
|
+
return res.status(400).json({ error: 'Not using Stripe payment method' });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (invoices.length === 0) {
|
|
899
|
+
return res.status(400).json({ error: 'No payable invoices found' });
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
await ensureStripeCustomer(customer, paymentMethod);
|
|
903
|
+
|
|
904
|
+
const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
|
|
905
|
+
|
|
906
|
+
const paymentCurrency = await PaymentCurrency.findByPk(currency_id);
|
|
907
|
+
if (!paymentCurrency) {
|
|
908
|
+
return res.status(404).json({ error: `Payment currency ${currency_id} not found` });
|
|
909
|
+
}
|
|
910
|
+
const totalAmount = invoices.reduce((sum, invoice) => {
|
|
911
|
+
const amount = invoice.amount_remaining || '0';
|
|
912
|
+
return new BN(sum).add(new BN(amount)).toString();
|
|
913
|
+
}, '0');
|
|
914
|
+
|
|
915
|
+
const metadata: any = {
|
|
916
|
+
currency_id,
|
|
917
|
+
customer_id: customer.id,
|
|
918
|
+
invoices: JSON.stringify(invoices.map((inv) => inv.id)),
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const setupIntent = await ensureStripeSetupIntentForInvoicePayment(customer, paymentMethod, metadata);
|
|
922
|
+
|
|
923
|
+
return res.json({
|
|
924
|
+
client_secret: setupIntent.client_secret,
|
|
925
|
+
publishable_key: settings.stripe?.publishable_key,
|
|
926
|
+
setup_intent_id: setupIntent.id,
|
|
927
|
+
invoices: invoices.map((inv) => inv.id),
|
|
928
|
+
amount: totalAmount,
|
|
929
|
+
currency: paymentCurrency,
|
|
930
|
+
customer,
|
|
931
|
+
});
|
|
932
|
+
} catch (err) {
|
|
933
|
+
logger.error('Failed to create setup intent for stripe payment', {
|
|
934
|
+
error: err,
|
|
935
|
+
body: req.body,
|
|
936
|
+
});
|
|
937
|
+
return res.status(400).json({ error: err.message });
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
802
941
|
// eslint-disable-next-line consistent-return
|
|
803
942
|
router.put('/:id', authAdmin, async (req, res) => {
|
|
804
943
|
try {
|
|
@@ -449,7 +449,8 @@ router.get('/:id/benefits', async (req, res) => {
|
|
|
449
449
|
if (!doc) {
|
|
450
450
|
return res.status(404).json({ error: 'payment link not found' });
|
|
451
451
|
}
|
|
452
|
-
const
|
|
452
|
+
const locale = req.query.locale as string;
|
|
453
|
+
const benefits = await getDonationBenefits(doc, '', locale);
|
|
453
454
|
return res.json(benefits);
|
|
454
455
|
} catch (err) {
|
|
455
456
|
logger.error('Get donation benefits error', { error: err.message, stack: err.stack, id: req.params.id });
|
|
@@ -238,7 +238,7 @@ router.get('/search', auth, async (req, res) => {
|
|
|
238
238
|
|
|
239
239
|
router.get('/:id', authPortal, async (req, res) => {
|
|
240
240
|
try {
|
|
241
|
-
const doc = await Subscription.findOne({
|
|
241
|
+
const doc = (await Subscription.findOne({
|
|
242
242
|
where: { id: req.params.id },
|
|
243
243
|
include: [
|
|
244
244
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
@@ -246,10 +246,15 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
246
246
|
{ model: SubscriptionItem, as: 'items' },
|
|
247
247
|
{ model: Customer, as: 'customer' },
|
|
248
248
|
],
|
|
249
|
-
})
|
|
249
|
+
})) as Subscription & {
|
|
250
|
+
paymentMethod: PaymentMethod;
|
|
251
|
+
paymentCurrency: PaymentCurrency;
|
|
252
|
+
items: SubscriptionItem[];
|
|
253
|
+
customer: Customer;
|
|
254
|
+
};
|
|
250
255
|
|
|
251
256
|
if (doc) {
|
|
252
|
-
const json = doc.toJSON();
|
|
257
|
+
const json: any = doc.toJSON();
|
|
253
258
|
const isConsumesCredit = await doc.isConsumesCredit();
|
|
254
259
|
const serviceType = isConsumesCredit ? 'credit' : 'standard';
|
|
255
260
|
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
@@ -270,9 +275,70 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
270
275
|
logger.error('Failed to fetch subscription discount stats', { error, subscriptionId: json.id });
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
// Get payment method details
|
|
279
|
+
let paymentMethodDetails = null;
|
|
280
|
+
try {
|
|
281
|
+
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
282
|
+
if (paymentMethod?.type === 'stripe' && json.payment_details?.stripe?.subscription_id) {
|
|
283
|
+
const client = paymentMethod.getStripeClient();
|
|
284
|
+
const stripeSubscription = await client.subscriptions.retrieve(json.payment_details.stripe.subscription_id, {
|
|
285
|
+
expand: ['default_payment_method'],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (stripeSubscription.default_payment_method) {
|
|
289
|
+
const paymentMethodId =
|
|
290
|
+
typeof stripeSubscription.default_payment_method === 'string'
|
|
291
|
+
? stripeSubscription.default_payment_method
|
|
292
|
+
: stripeSubscription.default_payment_method.id;
|
|
293
|
+
|
|
294
|
+
const paymentMethodData = await client.paymentMethods.retrieve(paymentMethodId);
|
|
295
|
+
|
|
296
|
+
paymentMethodDetails = {
|
|
297
|
+
id: paymentMethodData.id,
|
|
298
|
+
type: paymentMethodData.type,
|
|
299
|
+
billing_details: paymentMethodData.billing_details,
|
|
300
|
+
} as any;
|
|
301
|
+
|
|
302
|
+
if (paymentMethodData.card) {
|
|
303
|
+
paymentMethodDetails.card = {
|
|
304
|
+
brand: paymentMethodData.card.brand,
|
|
305
|
+
last4: paymentMethodData.card.last4,
|
|
306
|
+
exp_month: paymentMethodData.card.exp_month,
|
|
307
|
+
exp_year: paymentMethodData.card.exp_year,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (paymentMethodData.link) {
|
|
312
|
+
paymentMethodDetails.link = {
|
|
313
|
+
email: paymentMethodData.link.email,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (paymentMethodData.us_bank_account) {
|
|
318
|
+
paymentMethodDetails.us_bank_account = {
|
|
319
|
+
account_type: paymentMethodData.us_bank_account.account_type,
|
|
320
|
+
bank_name: paymentMethodData.us_bank_account.bank_name,
|
|
321
|
+
last4: paymentMethodData.us_bank_account.last4,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else if (doc.paymentMethod) {
|
|
326
|
+
const payer = getSubscriptionPaymentAddress(doc, doc.paymentMethod.type);
|
|
327
|
+
if (payer) {
|
|
328
|
+
paymentMethodDetails = {
|
|
329
|
+
type: doc.paymentMethod.type,
|
|
330
|
+
payer,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
logger.error('Failed to fetch payment method details', { error, subscriptionId: json.id });
|
|
336
|
+
}
|
|
337
|
+
|
|
273
338
|
res.json({
|
|
274
339
|
...json,
|
|
275
340
|
discountStats,
|
|
341
|
+
paymentMethodDetails,
|
|
276
342
|
});
|
|
277
343
|
} else {
|
|
278
344
|
res.status(404).json(null);
|
|
@@ -2283,4 +2349,65 @@ router.get('/:id/change-payment/migrate-invoice', auth, async (req, res) => {
|
|
|
2283
2349
|
return res.status(400).json({ error: error.message });
|
|
2284
2350
|
}
|
|
2285
2351
|
});
|
|
2352
|
+
|
|
2353
|
+
router.post('/:id/update-stripe-payment-method', authPortal, async (req, res) => {
|
|
2354
|
+
try {
|
|
2355
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
2356
|
+
if (!subscription) {
|
|
2357
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
if (!['active', 'trialing', 'past_due'].includes(subscription.status)) {
|
|
2361
|
+
return res.status(400).json({ error: 'Subscription is not active' });
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
2365
|
+
if (!paymentMethod || paymentMethod.type !== 'stripe') {
|
|
2366
|
+
return res.status(400).json({ error: 'Subscription is not using Stripe payment method' });
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id;
|
|
2370
|
+
if (!stripeSubscriptionId) {
|
|
2371
|
+
return res.status(400).json({ error: 'Stripe subscription not found' });
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
2375
|
+
if (!customer) {
|
|
2376
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
await ensureStripeCustomer(customer, paymentMethod);
|
|
2380
|
+
|
|
2381
|
+
const client = paymentMethod.getStripeClient();
|
|
2382
|
+
const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
|
|
2383
|
+
|
|
2384
|
+
const setupIntent = await client.setupIntents.create({
|
|
2385
|
+
customer: subscription.payment_details?.stripe?.customer_id,
|
|
2386
|
+
payment_method_types: ['card'],
|
|
2387
|
+
usage: 'off_session',
|
|
2388
|
+
metadata: {
|
|
2389
|
+
subscription_id: subscription.id,
|
|
2390
|
+
action: 'update_payment_method',
|
|
2391
|
+
},
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
logger.info('Setup intent created for updating stripe payment method', {
|
|
2395
|
+
subscription: subscription.id,
|
|
2396
|
+
setupIntent: setupIntent.id,
|
|
2397
|
+
});
|
|
2398
|
+
|
|
2399
|
+
return res.json({
|
|
2400
|
+
client_secret: setupIntent.client_secret,
|
|
2401
|
+
publishable_key: settings.stripe?.publishable_key,
|
|
2402
|
+
setup_intent_id: setupIntent.id,
|
|
2403
|
+
});
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
logger.error('Failed to create setup intent for updating payment method', {
|
|
2406
|
+
error: err,
|
|
2407
|
+
subscriptionId: req.params.id,
|
|
2408
|
+
});
|
|
2409
|
+
return res.status(400).json({ error: err.message });
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2286
2413
|
export default router;
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.17",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"@blocklet/error": "^0.2.5",
|
|
57
57
|
"@blocklet/js-sdk": "^1.16.53-beta-20251011-054719-4ed2f6b7",
|
|
58
58
|
"@blocklet/logger": "^1.16.53-beta-20251011-054719-4ed2f6b7",
|
|
59
|
-
"@blocklet/payment-broker-client": "1.21.
|
|
60
|
-
"@blocklet/payment-react": "1.21.
|
|
61
|
-
"@blocklet/payment-vendor": "1.21.
|
|
59
|
+
"@blocklet/payment-broker-client": "1.21.17",
|
|
60
|
+
"@blocklet/payment-react": "1.21.17",
|
|
61
|
+
"@blocklet/payment-vendor": "1.21.17",
|
|
62
62
|
"@blocklet/sdk": "^1.16.53-beta-20251011-054719-4ed2f6b7",
|
|
63
63
|
"@blocklet/ui-react": "^3.1.46",
|
|
64
64
|
"@blocklet/uploader": "^0.2.15",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"devDependencies": {
|
|
129
129
|
"@abtnode/types": "^1.16.53-beta-20251011-054719-4ed2f6b7",
|
|
130
130
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
131
|
-
"@blocklet/payment-types": "1.21.
|
|
131
|
+
"@blocklet/payment-types": "1.21.17",
|
|
132
132
|
"@types/cookie-parser": "^1.4.9",
|
|
133
133
|
"@types/cors": "^2.8.19",
|
|
134
134
|
"@types/debug": "^4.1.12",
|
|
@@ -175,5 +175,5 @@
|
|
|
175
175
|
"parser": "typescript"
|
|
176
176
|
}
|
|
177
177
|
},
|
|
178
|
-
"gitHead": "
|
|
178
|
+
"gitHead": "a823bc05e706681ee70451437b0460aba909c253"
|
|
179
179
|
}
|
|
@@ -65,6 +65,16 @@ export function InvoiceTemplate({ data, t }: InvoicePDFProps) {
|
|
|
65
65
|
<span style={composeStyles('gray')}>{formatTime(data.period_end * 1000)}</span>
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
68
|
+
<div style={composeStyles('flex mb-5')}>
|
|
69
|
+
<div style={composeStyles('w-40')}>
|
|
70
|
+
<span style={composeStyles('bold')}>{t('admin.paymentCurrency.name')}</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div style={composeStyles('w-60')}>
|
|
73
|
+
<span style={composeStyles('gray')}>
|
|
74
|
+
{data.paymentCurrency.symbol} ({data.paymentMethod.name})
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
68
78
|
</div>
|
|
69
79
|
</div>
|
|
70
80
|
|
|
@@ -137,6 +147,26 @@ export function InvoiceTemplate({ data, t }: InvoicePDFProps) {
|
|
|
137
147
|
);
|
|
138
148
|
})}
|
|
139
149
|
|
|
150
|
+
{detail.length === 0 && (
|
|
151
|
+
<div style={composeStyles('row flex')}>
|
|
152
|
+
<div style={composeStyles('w-38 p-4-8 pb-15')}>
|
|
153
|
+
<span style={composeStyles('gray')}>-</span>
|
|
154
|
+
</div>
|
|
155
|
+
<div style={composeStyles('w-15 p-4-8 pb-15')}>
|
|
156
|
+
<span style={composeStyles('gray right')}>-</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div style={composeStyles('w-15 p-4-8 pb-15')}>
|
|
159
|
+
<span style={composeStyles('gray right')}>-</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div style={composeStyles('w-15 p-4-8 pb-15')}>
|
|
162
|
+
<span style={composeStyles('gray right')}>-</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div style={composeStyles('w-17 p-4-8 pb-15')}>
|
|
165
|
+
<span style={composeStyles('gray right')}>-</span>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
140
170
|
{/* Summary */}
|
|
141
171
|
<div
|
|
142
172
|
style={{
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Button } from '@arcblock/ux';
|
|
2
|
+
import DID from '@arcblock/ux/lib/DID';
|
|
3
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
+
import { StripeForm, api, formatError, usePaymentContext } from '@blocklet/payment-react';
|
|
6
|
+
import type { TCustomer } from '@blocklet/payment-types';
|
|
7
|
+
import { CreditCard, Email, AccountBalance } from '@mui/icons-material';
|
|
8
|
+
import { Box, Stack, Typography } from '@mui/material';
|
|
9
|
+
import { useSetState } from 'ahooks';
|
|
10
|
+
import { useEffect } from 'react';
|
|
11
|
+
|
|
12
|
+
interface PaymentMethodData {
|
|
13
|
+
id?: string;
|
|
14
|
+
type: string;
|
|
15
|
+
// Stripe payment methods
|
|
16
|
+
card?: {
|
|
17
|
+
brand: string;
|
|
18
|
+
last4: string;
|
|
19
|
+
exp_month: number;
|
|
20
|
+
exp_year: number;
|
|
21
|
+
};
|
|
22
|
+
link?: {
|
|
23
|
+
email: string;
|
|
24
|
+
};
|
|
25
|
+
us_bank_account?: {
|
|
26
|
+
account_type: string;
|
|
27
|
+
bank_name: string;
|
|
28
|
+
last4: string;
|
|
29
|
+
};
|
|
30
|
+
billing_details?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
phone?: string;
|
|
34
|
+
};
|
|
35
|
+
// On-chain payment methods (arcblock/ethereum/base)
|
|
36
|
+
payer?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
subscriptionId: string;
|
|
41
|
+
customer: TCustomer;
|
|
42
|
+
paymentMethodDetails: PaymentMethodData | null;
|
|
43
|
+
editable?: boolean;
|
|
44
|
+
paymentMethodType: string;
|
|
45
|
+
onUpdate?: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function PaymentMethodInfo({
|
|
49
|
+
subscriptionId,
|
|
50
|
+
customer,
|
|
51
|
+
paymentMethodDetails,
|
|
52
|
+
editable = false,
|
|
53
|
+
onUpdate = () => {},
|
|
54
|
+
paymentMethodType,
|
|
55
|
+
}: Props) {
|
|
56
|
+
const { t, locale } = useLocaleContext();
|
|
57
|
+
const { connect } = usePaymentContext();
|
|
58
|
+
|
|
59
|
+
const [state, setState] = useSetState<{
|
|
60
|
+
editing: boolean;
|
|
61
|
+
submitting: boolean;
|
|
62
|
+
setupIntentId: string | null;
|
|
63
|
+
clientSecret: string | null;
|
|
64
|
+
publishableKey: string | null;
|
|
65
|
+
}>({
|
|
66
|
+
editing: false,
|
|
67
|
+
submitting: false,
|
|
68
|
+
setupIntentId: null,
|
|
69
|
+
clientSecret: null,
|
|
70
|
+
publishableKey: null,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!state.editing) {
|
|
75
|
+
setState({ clientSecret: null, publishableKey: null, setupIntentId: null });
|
|
76
|
+
}
|
|
77
|
+
}, [state.editing, setState]);
|
|
78
|
+
|
|
79
|
+
const handleEdit = async () => {
|
|
80
|
+
if (paymentMethodType === 'stripe') {
|
|
81
|
+
try {
|
|
82
|
+
setState({ submitting: true });
|
|
83
|
+
const { data } = await api.post(`/api/subscriptions/${subscriptionId}/update-stripe-payment-method`);
|
|
84
|
+
setState({
|
|
85
|
+
editing: true,
|
|
86
|
+
clientSecret: data.client_secret,
|
|
87
|
+
publishableKey: data.publishable_key,
|
|
88
|
+
setupIntentId: data.setup_intent_id,
|
|
89
|
+
submitting: false,
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
Toast.error(formatError(err));
|
|
93
|
+
setState({ submitting: false });
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
connect.open({
|
|
97
|
+
action: 'change-payer',
|
|
98
|
+
saveConnect: false,
|
|
99
|
+
locale: locale as 'en' | 'zh',
|
|
100
|
+
useSocket: true,
|
|
101
|
+
messages: {
|
|
102
|
+
scan: '',
|
|
103
|
+
title: t('admin.subscription.changePayer.connect.title'),
|
|
104
|
+
success: t('admin.subscription.changePayer.connect.success'),
|
|
105
|
+
error: t('admin.subscription.changePayer.connect.error'),
|
|
106
|
+
confirm: '',
|
|
107
|
+
} as any,
|
|
108
|
+
extraParams: { subscriptionId },
|
|
109
|
+
onSuccess: () => {
|
|
110
|
+
connect.close();
|
|
111
|
+
Toast.success(t('admin.subscription.changePayer.connect.success'));
|
|
112
|
+
onUpdate?.();
|
|
113
|
+
},
|
|
114
|
+
onClose: () => {
|
|
115
|
+
connect.close();
|
|
116
|
+
},
|
|
117
|
+
onError: (err: any) => {
|
|
118
|
+
Toast.error(formatError(err));
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleConfirm = () => {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
Toast.success(t('admin.subscription.changePayer.connect.success'));
|
|
127
|
+
setState({ editing: false, submitting: false });
|
|
128
|
+
onUpdate?.();
|
|
129
|
+
}, 2000);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleCancel = () => {
|
|
133
|
+
setState({ editing: false, clientSecret: null, publishableKey: null, setupIntentId: null });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (!paymentMethodDetails) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { card, link, us_bank_account: usBankAccount, payer, type } = paymentMethodDetails;
|
|
141
|
+
|
|
142
|
+
if (state.editing && state.clientSecret && state.publishableKey) {
|
|
143
|
+
return (
|
|
144
|
+
<Box>
|
|
145
|
+
<StripeForm
|
|
146
|
+
clientSecret={state.clientSecret}
|
|
147
|
+
intentType="setup_intent"
|
|
148
|
+
publicKey={state.publishableKey}
|
|
149
|
+
customer={customer}
|
|
150
|
+
mode="setup"
|
|
151
|
+
onConfirm={handleConfirm}
|
|
152
|
+
onCancel={handleCancel}
|
|
153
|
+
returnUrl={window.location.href}
|
|
154
|
+
/>
|
|
155
|
+
</Box>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const renderPaymentMethodInfo = () => {
|
|
160
|
+
if (type === 'card' && card) {
|
|
161
|
+
return (
|
|
162
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
163
|
+
<CreditCard sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
164
|
+
<Typography variant="body2">
|
|
165
|
+
{card.brand.toUpperCase()} •••• {card.last4}
|
|
166
|
+
</Typography>
|
|
167
|
+
</Stack>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (type === 'link' && link) {
|
|
172
|
+
return (
|
|
173
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
174
|
+
<Email sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
175
|
+
<Typography variant="body2">
|
|
176
|
+
{t('admin.subscription.changePayer.stripe.linkType')} ({link.email})
|
|
177
|
+
</Typography>
|
|
178
|
+
</Stack>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (type === 'us_bank_account' && usBankAccount) {
|
|
183
|
+
return (
|
|
184
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
185
|
+
<AccountBalance sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
186
|
+
<Typography variant="body2">
|
|
187
|
+
{usBankAccount.bank_name || t('admin.subscription.changePayer.stripe.bankAccount')} ••••{' '}
|
|
188
|
+
{usBankAccount.last4}
|
|
189
|
+
</Typography>
|
|
190
|
+
</Stack>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (['arcblock', 'ethereum', 'base'].includes(type) && payer) {
|
|
195
|
+
return <DID did={payer} responsive={false} compact copyable={false} />;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
200
|
+
-
|
|
201
|
+
</Typography>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flex: 1 }}>
|
|
207
|
+
{renderPaymentMethodInfo()}
|
|
208
|
+
{editable && (
|
|
209
|
+
<Button
|
|
210
|
+
variant="text"
|
|
211
|
+
size="small"
|
|
212
|
+
sx={{
|
|
213
|
+
color: 'text.link',
|
|
214
|
+
}}
|
|
215
|
+
loading={state.submitting}
|
|
216
|
+
onClick={handleEdit}>
|
|
217
|
+
{t('admin.subscription.changePayer.btn')}
|
|
218
|
+
</Button>
|
|
219
|
+
)}
|
|
220
|
+
</Stack>
|
|
221
|
+
);
|
|
222
|
+
}
|