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.
@@ -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
- if (doc.metadata?.stripe_id && (doc.status !== 'paid' || req.query.forceSync)) {
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 benefits = await getDonationBenefits(doc);
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
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.21.16
17
+ version: 1.21.17
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.21.16",
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.16",
60
- "@blocklet/payment-react": "1.21.16",
61
- "@blocklet/payment-vendor": "1.21.16",
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.16",
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": "16509d9abd2da2f52587972c863c79ba9e4cd49d"
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
+ }