payment-kit 1.18.33 → 1.18.35

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.
@@ -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
- // Update payment settings for all subscriptions
655
- await Promise.all(subscriptions.map((sub) => sub.update({ payment_settings: paymentSettings })));
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
- // Create invoices for all subscriptions
658
- const { invoices } = await ensureInvoicesForSubscriptions({
659
- checkoutSession,
660
- customer,
661
- subscriptions,
662
- });
663
-
664
- // Update invoice settings and push to queue
665
- await Promise.all(
666
- invoices.map(async (invoice) => {
667
- if (invoice) {
668
- await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
669
- invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
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
- logger.info('Created and queued invoices for fast checkout with subscriptions', {
678
- checkoutSessionId: checkoutSession.id,
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
- await paymentIntent.update({ status: 'requires_capture' });
1237
- const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
1238
- if (invoice) {
1239
- await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
1240
- invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
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
- ...(subscription.overdraft_protection || {}),
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
- ...subscription.overdraft_protection,
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: JSON.stringify({
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
@@ -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.18.33
17
+ version: 1.18.35
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.18.33",
3
+ "version": "1.18.35",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -47,16 +47,16 @@
47
47
  "@abtnode/cron": "^1.16.42",
48
48
  "@arcblock/did": "^1.20.2",
49
49
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
50
- "@arcblock/did-connect": "^2.13.9",
50
+ "@arcblock/did-connect": "^2.13.12",
51
51
  "@arcblock/did-util": "^1.20.2",
52
52
  "@arcblock/jwt": "^1.20.2",
53
- "@arcblock/ux": "^2.13.9",
53
+ "@arcblock/ux": "^2.13.12",
54
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.33",
57
+ "@blocklet/payment-react": "1.18.35",
58
58
  "@blocklet/sdk": "^1.16.42",
59
- "@blocklet/ui-react": "^2.13.9",
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",
@@ -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.33",
125
+ "@blocklet/payment-types": "1.18.35",
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": "91ae6c4dda76b8cc7a8ab5f82ae44874d429439b"
171
+ "gitHead": "802a98b5ca81475f8cd7b9dcbb77fce7240b9788"
172
172
  }
package/scripts/sdk.js CHANGED
@@ -74,6 +74,70 @@ const checkoutModule = {
74
74
  return checkoutSession;
75
75
  },
76
76
 
77
+ // 批量订阅 + 免质押 + 自定义表单规则
78
+ async createBatchSubscriptionWithCustomField() {
79
+ const checkoutSession = await payment.checkout.sessions.create({
80
+ mode: 'subscription',
81
+ line_items: [
82
+ {
83
+ price_id: 'price_fQFIS12yi0JR3KePLmitjrhA',
84
+ quantity: 1,
85
+ subscription_data: {
86
+ metadata: { test: 'test price_fQFIS12yi0JR3KePLmitjrhA' },
87
+ },
88
+ },
89
+ {
90
+ price_id: 'price_PXyI9Duz99eqty1AqbaEc73u',
91
+ quantity: 1,
92
+ subscription_data: {
93
+ metadata: { test: 'test price_PXyI9Duz99eqty1AqbaEc73u' },
94
+ },
95
+ },
96
+ ],
97
+ enable_subscription_grouping: true,
98
+ subscription_data: {
99
+ no_stake: true,
100
+ },
101
+ phone_number_collection: {
102
+ enabled: true,
103
+ },
104
+ billing_address_collection: 'required',
105
+ metadata: {
106
+ page_info: {
107
+ form_purpose_description: {
108
+ en: 'Information collected helps us process your payment and deliver our services.',
109
+ zh: '收集的信息帮助我们处理您的付款并提供服务。',
110
+ },
111
+ field_validation: {
112
+ customer_name: {
113
+ pattern: '^[a-zA-Z\\s]{2,50}$',
114
+ pattern_message: {
115
+ en: 'Name should only contain 2-50 letters and spaces',
116
+ zh: '姓名应只包含2-50个字母和空格',
117
+ },
118
+ },
119
+ customer_email: {
120
+ pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
121
+ pattern_message: {
122
+ en: 'Please enter a valid email address',
123
+ zh: '请输入有效的电子邮件地址',
124
+ },
125
+ },
126
+ 'billing_address.line1': {
127
+ pattern: '^.{5,100}$',
128
+ pattern_message: {
129
+ en: 'Address should be 5-100 characters',
130
+ zh: '地址应为5-100个字符',
131
+ },
132
+ },
133
+ },
134
+ },
135
+ },
136
+ });
137
+ console.log('createBatchSubscriptionWithCustomField', checkoutSession);
138
+ return checkoutSession;
139
+ },
140
+
77
141
  // 批量订阅 + 试用 + 免质押
78
142
  async createWithTrial() {
79
143
  const checkoutSession = await payment.checkout.sessions.create({
@@ -461,7 +525,7 @@ const testModules = {
461
525
 
462
526
  async function runTest() {
463
527
  payment.environments.setTestMode(true);
464
- await testModules.checkout.createBatchSubscription();
528
+ await testModules.checkout.createBatchSubscriptionWithCustomField();
465
529
  }
466
530
 
467
531
  async function main() {
@@ -1,17 +1,13 @@
1
1
  import type { TCustomer } from '@blocklet/payment-types';
2
2
  import { Link } from 'react-router-dom';
3
-
3
+ import UserCard from '@arcblock/ux/lib/UserCard';
4
4
  import { getCustomerAvatar } from '@blocklet/payment-react';
5
- import DID from '@arcblock/ux/lib/DID';
6
- import { Box, Typography } from '@mui/material';
7
- import InfoCard from '../info-card';
8
5
 
9
6
  export default function CustomerLink({
10
7
  customer,
11
8
  linked,
12
9
  linkTo,
13
10
  size,
14
- tooltip,
15
11
  }: {
16
12
  customer: TCustomer;
17
13
  linked?: boolean;
@@ -22,61 +18,40 @@ export default function CustomerLink({
22
18
  if (!customer) {
23
19
  return null;
24
20
  }
21
+ const CustomerCard = (
22
+ // @ts-ignore
23
+ <UserCard
24
+ did={customer?.did}
25
+ showHoverCard
26
+ sx={{
27
+ border: 'none',
28
+ p: 0,
29
+ minWidth: 0,
30
+ }}
31
+ avatarProps={{
32
+ size: size === 'small' ? 24 : 40,
33
+ }}
34
+ showDid={size !== 'small'}
35
+ {...(customer.metadata.anonymous === true
36
+ ? {
37
+ user: {
38
+ fullName: customer.name || customer.email,
39
+ did: customer.did,
40
+ email: customer.email,
41
+ avatar: getCustomerAvatar(
42
+ customer?.did,
43
+ customer?.updated_at ? new Date(customer.updated_at).toISOString() : ''
44
+ ),
45
+ },
46
+ }
47
+ : {})}
48
+ />
49
+ );
25
50
  if (linked) {
26
- return (
27
- <Link to={linkTo || `/admin/customers/${customer.id}`}>
28
- <Box sx={{ '.info-card-wrapper': { cursor: 'pointer' }, '.info-card': { minWidth: 0 } }}>
29
- {/* @ts-ignore */}
30
- <InfoCard
31
- logo={getCustomerAvatar(
32
- customer?.did,
33
- customer?.updated_at ? new Date(customer.updated_at).toISOString() : '',
34
- size === 'small' ? 24 : 48
35
- )}
36
- name={
37
- <Typography
38
- sx={{ maxWidth: 208, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
39
- className="customer-link-name">
40
- {customer.name || customer.email}
41
- </Typography>
42
- }
43
- {...(size === 'small'
44
- ? { tooltip: tooltip ? <DID did={customer?.did} /> : false, size: 24 }
45
- : {
46
- description: <DID did={customer?.did} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />,
47
- size: 48,
48
- })}
49
- />
50
- </Box>
51
- </Link>
52
- );
51
+ return <Link to={linkTo || `/admin/customers/${customer.id}`}>{CustomerCard}</Link>;
53
52
  }
54
53
 
55
- return (
56
- <Box sx={{ '.info-card': { minWidth: 0 } }}>
57
- {/* @ts-ignore */}
58
- <InfoCard
59
- logo={getCustomerAvatar(
60
- customer.did,
61
- customer.updated_at ? new Date(customer.updated_at).toISOString() : '',
62
- size === 'small' ? 24 : 48
63
- )}
64
- name={
65
- <Typography
66
- sx={{ maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
67
- className="customer-link-name">
68
- {customer.name || customer.email}
69
- </Typography>
70
- }
71
- {...(size === 'small'
72
- ? { tooltip: tooltip ? <DID did={customer?.did} /> : false, size: 24 }
73
- : {
74
- description: <DID did={customer?.did} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />,
75
- size: 48,
76
- })}
77
- />
78
- </Box>
79
- );
54
+ return CustomerCard;
80
55
  }
81
56
 
82
57
  CustomerLink.defaultProps = {
@@ -282,7 +282,7 @@ export default function InvoiceList({
282
282
  options: {
283
283
  customBodyRenderLite: (_: string, index: number) => {
284
284
  const item = data.list[index] as TInvoiceExpanded;
285
- return <CustomerLink customer={item.customer} />;
285
+ return <CustomerLink customer={item.customer} size="small" />;
286
286
  },
287
287
  },
288
288
  });
@@ -15,6 +15,8 @@ import { useLocalStorageState } from 'ahooks';
15
15
  import { useEffect, useState } from 'react';
16
16
  import { Link } from 'react-router-dom';
17
17
 
18
+ import DID from '@arcblock/ux/lib/DID';
19
+ import ShortenLabel from '@arcblock/ux/lib/UserCard/Content/shorten-label';
18
20
  import { debounce, getAppInfo } from '../../libs/util';
19
21
  import CustomerLink from '../customer/link';
20
22
  import FilterToolbar from '../filter-toolbar';
@@ -202,8 +204,12 @@ export default function PayoutList({ customer_id, payment_intent_id, status, fea
202
204
  if (appInfo) {
203
205
  return (
204
206
  <InfoCard
205
- name={appInfo.name}
206
- description={`${item.destination.slice(0, 6)}...${item.destination.slice(-6)}`}
207
+ name={
208
+ <ShortenLabel sx={{ fontWeight: 500 }} maxLength={30}>
209
+ {appInfo.name}
210
+ </ShortenLabel>
211
+ }
212
+ description={<DID did={item.destination} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />}
207
213
  logo={appInfo.avatar}
208
214
  size={40}
209
215
  />
@@ -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'));
package/src/libs/util.ts CHANGED
@@ -3,14 +3,12 @@
3
3
  /* eslint-disable @typescript-eslint/indent */
4
4
  import { formatCheckoutHeadlines, formatPrice, getPrefix, getPriceCurrencyOptions } from '@blocklet/payment-react';
5
5
  import type {
6
- ChainType,
7
6
  LineItem,
8
7
  PriceRecurring,
9
8
  TInvoiceExpanded,
10
9
  TLineItemExpanded,
11
10
  TPaymentCurrency,
12
11
  TPaymentLinkExpanded,
13
- TPaymentMethod,
14
12
  TPaymentMethodExpanded,
15
13
  TPrice,
16
14
  TProductExpanded,
@@ -350,17 +348,6 @@ export function getAppInfo(address: string): { name: string; avatar: string; typ
350
348
  return null;
351
349
  }
352
350
 
353
- export function getTokenBalanceLink(method: TPaymentMethod, address: string) {
354
- const explorerHost = (method?.settings?.[method?.type as ChainType] as any)?.explorer_host || '';
355
- if (method.type === 'arcblock' && address) {
356
- return joinURL(explorerHost, 'accounts', address, 'tokens');
357
- }
358
- if (['ethereum', 'base'].includes(method.type) && address) {
359
- return joinURL(explorerHost, 'address', address);
360
- }
361
- return '';
362
- }
363
-
364
351
  export function isWillCanceled(subscription: TSubscriptionExpanded) {
365
352
  const now = Date.now() / 1000;
366
353
  if (
@@ -211,8 +211,8 @@ export default function CustomerDetail(props: { id: string }) {
211
211
  52
212
212
  )}
213
213
  alt={data.customer.name}
214
- variant="square"
215
- sx={{ width: 52, height: 52, borderRadius: 'var(--radius-s, 4px)' }}
214
+ variant="circular"
215
+ sx={{ width: 52, height: 52 }}
216
216
  />
217
217
  <Typography variant="h2" sx={{ fontWeight: 600 }}>
218
218
  {data.customer.name}
@@ -75,9 +75,8 @@ export default function CustomersList() {
75
75
  item?.updated_at ? new Date(item.updated_at).toISOString() : '',
76
76
  48
77
77
  )}
78
- variant="square"
78
+ variant="circular"
79
79
  alt={item?.name}
80
- sx={{ borderRadius: 'var(--radius-m, 8px)' }}
81
80
  />
82
81
  <Typography sx={{ wordBreak: 'break-all' }}>{item.name}</Typography>
83
82
  </Stack>
@@ -203,6 +203,7 @@ export default function PayoutDetail(props: { id: string }) {
203
203
  label={t('customer.payout.payer')}
204
204
  value={
205
205
  <InfoCard
206
+ variant="circular"
206
207
  logo={getCustomerAvatar(
207
208
  paymentIntent?.customer?.did,
208
209
  paymentIntent?.customer?.updated_at
@@ -242,7 +243,6 @@ export default function PayoutDetail(props: { id: string }) {
242
243
  />
243
244
  }
244
245
  size={40}
245
- variant="rounded"
246
246
  />
247
247
  }
248
248
  divider
@@ -66,6 +66,7 @@ export default function CustomerInvoiceDetail() {
66
66
  connect.open({
67
67
  action: 'collect',
68
68
  saveConnect: false,
69
+ locale: locale as 'en' | 'zh',
69
70
  messages: {
70
71
  scan: '',
71
72
  title: t(`payment.customer.invoice.${action || 'pay'}`),
@@ -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
  },
@@ -27,6 +27,7 @@ import {
27
27
  formatBNStr,
28
28
  formatPrice,
29
29
  formatNumber,
30
+ getTokenBalanceLink,
30
31
  } from '@blocklet/payment-react';
31
32
  import type { TPaymentCurrency, TPaymentMethod } from '@blocklet/payment-types';
32
33
  import { joinURL } from 'ufo';
@@ -34,7 +35,7 @@ import { AccountBalanceWalletOutlined, ArrowBackOutlined, ArrowForwardOutlined }
34
35
  import Empty from '@arcblock/ux/lib/Empty';
35
36
  import { BN, fromUnitToToken } from '@ocap/util';
36
37
  import RechargeList from '../../../components/invoice/recharge';
37
- import { getTokenBalanceLink, goBackOrFallback } from '../../../libs/util';
38
+ import { goBackOrFallback } from '../../../libs/util';
38
39
  import { useSessionContext } from '../../../contexts/session';
39
40
  import { formatSmartDuration, TimeUnit } from '../../../libs/dayjs';
40
41
 
@@ -225,12 +226,18 @@ export default function BalanceRechargePage() {
225
226
  containerEl: undefined as unknown as Element,
226
227
  saveConnect: false,
227
228
  action: 'recharge-account',
229
+ locale: locale as 'en' | 'zh',
228
230
  prefix: joinURL(getPrefix(), '/api/did'),
229
231
  extraParams: {
230
232
  customerDid: session?.user?.did,
231
233
  currencyId: currency.id,
232
234
  amount: Number(amount),
233
235
  },
236
+ messages: {
237
+ scan: t('common.connect.defaultScan'),
238
+ title: t('customer.recharge.title'),
239
+ confirm: t('common.connect.confirm'),
240
+ } as any,
234
241
  onSuccess: () => {
235
242
  connect.close();
236
243
  Toast.success(t('customer.recharge.success'));
@@ -25,6 +25,7 @@ import {
25
25
  formatTime,
26
26
  api,
27
27
  formatBNStr,
28
+ getTokenBalanceLink,
28
29
  } from '@blocklet/payment-react';
29
30
  import { joinURL } from 'ufo';
30
31
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
@@ -35,7 +36,7 @@ import SubscriptionDescription from '../../../components/subscription/descriptio
35
36
  import InfoRow from '../../../components/info-row';
36
37
  import Currency from '../../../components/currency';
37
38
  import SubscriptionMetrics from '../../../components/subscription/metrics';
38
- import { getTokenBalanceLink, goBackOrFallback } from '../../../libs/util';
39
+ import { goBackOrFallback } from '../../../libs/util';
39
40
  import CustomerLink from '../../../components/customer/link';
40
41
  import { useSessionContext } from '../../../contexts/session';
41
42
  import { formatSmartDuration, TimeUnit } from '../../../libs/dayjs';
@@ -163,8 +164,14 @@ export default function RechargePage() {
163
164
  containerEl: undefined as unknown as Element,
164
165
  saveConnect: false,
165
166
  action: 'recharge',
167
+ locale: locale as 'en' | 'zh',
166
168
  prefix: joinURL(getPrefix(), '/api/did'),
167
169
  extraParams: { subscriptionId, amount: Number(amount) },
170
+ messages: {
171
+ scan: t('common.connect.defaultScan'),
172
+ title: t('customer.recharge.title'),
173
+ confirm: t('common.connect.confirm'),
174
+ } as any,
168
175
  onSuccess: () => {
169
176
  connect.close();
170
177
  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();
@@ -152,6 +152,7 @@ export default function CustomerSubscriptionChangePlan() {
152
152
  try {
153
153
  setState({ paying: true });
154
154
  connect.open({
155
+ locale: locale as 'en' | 'zh',
155
156
  action: result.data.connectAction,
156
157
  saveConnect: false,
157
158
  messages: {
@@ -229,6 +229,7 @@ export default function SubscriptionEmbed() {
229
229
  </Stack>
230
230
  }
231
231
  size={24}
232
+ variant="circular"
232
233
  />
233
234
  ),
234
235
  });