@blocklet/payment-react 1.25.10 → 1.26.0

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.
Files changed (160) hide show
  1. package/es/checkout-v2/checkout-v2.d.ts +2 -0
  2. package/es/checkout-v2/checkout-v2.js +121 -0
  3. package/es/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
  4. package/es/checkout-v2/components/dialogs/checkout-dialogs.js +106 -0
  5. package/es/checkout-v2/components/left/billing-toggle.d.ts +6 -0
  6. package/es/checkout-v2/components/left/billing-toggle.js +118 -0
  7. package/es/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
  8. package/es/checkout-v2/components/left/cross-sell-card.js +167 -0
  9. package/es/checkout-v2/components/left/product-item-card.d.ts +26 -0
  10. package/es/checkout-v2/components/left/product-item-card.js +571 -0
  11. package/es/checkout-v2/components/left/promotion-input.d.ts +19 -0
  12. package/es/checkout-v2/components/left/promotion-input.js +178 -0
  13. package/es/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
  14. package/es/checkout-v2/components/left/staking-breakdown.js +48 -0
  15. package/es/checkout-v2/components/left/trial-info.d.ts +13 -0
  16. package/es/checkout-v2/components/left/trial-info.js +48 -0
  17. package/es/checkout-v2/components/right/currency-grid.d.ts +8 -0
  18. package/es/checkout-v2/components/right/currency-grid.js +48 -0
  19. package/es/checkout-v2/components/right/customer-info-card.d.ts +17 -0
  20. package/es/checkout-v2/components/right/customer-info-card.js +156 -0
  21. package/es/checkout-v2/components/right/status-feedback.d.ts +7 -0
  22. package/es/checkout-v2/components/right/status-feedback.js +17 -0
  23. package/es/checkout-v2/components/right/submit-button.d.ts +10 -0
  24. package/es/checkout-v2/components/right/submit-button.js +29 -0
  25. package/es/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
  26. package/es/checkout-v2/components/right/subscription-disclaimer.js +8 -0
  27. package/es/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
  28. package/es/checkout-v2/components/shared/exchange-rate-footer.js +182 -0
  29. package/es/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
  30. package/es/checkout-v2/components/shared/scenario-badge.js +47 -0
  31. package/es/checkout-v2/components/shared/total-display.d.ts +7 -0
  32. package/es/checkout-v2/components/shared/total-display.js +84 -0
  33. package/es/checkout-v2/index.d.ts +2 -0
  34. package/es/checkout-v2/index.js +1 -0
  35. package/es/checkout-v2/layouts/checkout-layout.d.ts +7 -0
  36. package/es/checkout-v2/layouts/checkout-layout.js +226 -0
  37. package/es/checkout-v2/panels/left/composite-panel.d.ts +1 -0
  38. package/es/checkout-v2/panels/left/composite-panel.js +423 -0
  39. package/es/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
  40. package/es/checkout-v2/panels/left/credit-topup-panel.js +615 -0
  41. package/es/checkout-v2/panels/left/scenario-router.d.ts +1 -0
  42. package/es/checkout-v2/panels/left/scenario-router.js +19 -0
  43. package/es/checkout-v2/panels/right/payment-panel.d.ts +1 -0
  44. package/es/checkout-v2/panels/right/payment-panel.js +644 -0
  45. package/es/checkout-v2/types.d.ts +15 -0
  46. package/es/checkout-v2/types.js +0 -0
  47. package/es/checkout-v2/utils/format.d.ts +59 -0
  48. package/es/checkout-v2/utils/format.js +125 -0
  49. package/es/checkout-v2/utils/scenario-detector.d.ts +3 -0
  50. package/es/checkout-v2/utils/scenario-detector.js +17 -0
  51. package/es/checkout-v2/views/error-view.d.ts +7 -0
  52. package/es/checkout-v2/views/error-view.js +269 -0
  53. package/es/checkout-v2/views/loading-view.d.ts +5 -0
  54. package/es/checkout-v2/views/loading-view.js +158 -0
  55. package/es/checkout-v2/views/success-view.d.ts +29 -0
  56. package/es/checkout-v2/views/success-view.js +614 -0
  57. package/es/components/phone-field.d.ts +14 -0
  58. package/es/components/phone-field.js +96 -0
  59. package/es/index.d.ts +3 -1
  60. package/es/index.js +3 -1
  61. package/es/locales/en.js +45 -6
  62. package/es/locales/zh.js +45 -6
  63. package/es/payment/form/index.js +10 -1
  64. package/lib/checkout-v2/checkout-v2.d.ts +2 -0
  65. package/lib/checkout-v2/checkout-v2.js +151 -0
  66. package/lib/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
  67. package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +131 -0
  68. package/lib/checkout-v2/components/left/billing-toggle.d.ts +6 -0
  69. package/lib/checkout-v2/components/left/billing-toggle.js +126 -0
  70. package/lib/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
  71. package/lib/checkout-v2/components/left/cross-sell-card.js +257 -0
  72. package/lib/checkout-v2/components/left/product-item-card.d.ts +26 -0
  73. package/lib/checkout-v2/components/left/product-item-card.js +738 -0
  74. package/lib/checkout-v2/components/left/promotion-input.d.ts +19 -0
  75. package/lib/checkout-v2/components/left/promotion-input.js +220 -0
  76. package/lib/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
  77. package/lib/checkout-v2/components/left/staking-breakdown.js +96 -0
  78. package/lib/checkout-v2/components/left/trial-info.d.ts +13 -0
  79. package/lib/checkout-v2/components/left/trial-info.js +82 -0
  80. package/lib/checkout-v2/components/right/currency-grid.d.ts +8 -0
  81. package/lib/checkout-v2/components/right/currency-grid.js +96 -0
  82. package/lib/checkout-v2/components/right/customer-info-card.d.ts +17 -0
  83. package/lib/checkout-v2/components/right/customer-info-card.js +246 -0
  84. package/lib/checkout-v2/components/right/status-feedback.d.ts +7 -0
  85. package/lib/checkout-v2/components/right/status-feedback.js +30 -0
  86. package/lib/checkout-v2/components/right/submit-button.d.ts +10 -0
  87. package/lib/checkout-v2/components/right/submit-button.js +35 -0
  88. package/lib/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
  89. package/lib/checkout-v2/components/right/subscription-disclaimer.js +33 -0
  90. package/lib/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
  91. package/lib/checkout-v2/components/shared/exchange-rate-footer.js +282 -0
  92. package/lib/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
  93. package/lib/checkout-v2/components/shared/scenario-badge.js +57 -0
  94. package/lib/checkout-v2/components/shared/total-display.d.ts +7 -0
  95. package/lib/checkout-v2/components/shared/total-display.js +154 -0
  96. package/lib/checkout-v2/index.d.ts +2 -0
  97. package/lib/checkout-v2/index.js +13 -0
  98. package/lib/checkout-v2/layouts/checkout-layout.d.ts +7 -0
  99. package/lib/checkout-v2/layouts/checkout-layout.js +308 -0
  100. package/lib/checkout-v2/panels/left/composite-panel.d.ts +1 -0
  101. package/lib/checkout-v2/panels/left/composite-panel.js +515 -0
  102. package/lib/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
  103. package/lib/checkout-v2/panels/left/credit-topup-panel.js +799 -0
  104. package/lib/checkout-v2/panels/left/scenario-router.d.ts +1 -0
  105. package/lib/checkout-v2/panels/left/scenario-router.js +29 -0
  106. package/lib/checkout-v2/panels/right/payment-panel.d.ts +1 -0
  107. package/lib/checkout-v2/panels/right/payment-panel.js +906 -0
  108. package/lib/checkout-v2/types.d.ts +15 -0
  109. package/lib/checkout-v2/types.js +1 -0
  110. package/lib/checkout-v2/utils/format.d.ts +59 -0
  111. package/lib/checkout-v2/utils/format.js +158 -0
  112. package/lib/checkout-v2/utils/scenario-detector.d.ts +3 -0
  113. package/lib/checkout-v2/utils/scenario-detector.js +23 -0
  114. package/lib/checkout-v2/views/error-view.d.ts +7 -0
  115. package/lib/checkout-v2/views/error-view.js +321 -0
  116. package/lib/checkout-v2/views/loading-view.d.ts +5 -0
  117. package/lib/checkout-v2/views/loading-view.js +168 -0
  118. package/lib/checkout-v2/views/success-view.d.ts +29 -0
  119. package/lib/checkout-v2/views/success-view.js +735 -0
  120. package/lib/components/phone-field.d.ts +14 -0
  121. package/lib/components/phone-field.js +130 -0
  122. package/lib/index.d.ts +3 -1
  123. package/lib/index.js +8 -0
  124. package/lib/locales/en.js +45 -6
  125. package/lib/locales/zh.js +45 -6
  126. package/lib/payment/form/index.js +10 -1
  127. package/package.json +4 -3
  128. package/src/checkout-v2/checkout-v2.tsx +155 -0
  129. package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +134 -0
  130. package/src/checkout-v2/components/left/billing-toggle.tsx +122 -0
  131. package/src/checkout-v2/components/left/cross-sell-card.tsx +170 -0
  132. package/src/checkout-v2/components/left/product-item-card.tsx +634 -0
  133. package/src/checkout-v2/components/left/promotion-input.tsx +207 -0
  134. package/src/checkout-v2/components/left/staking-breakdown.tsx +57 -0
  135. package/src/checkout-v2/components/left/trial-info.tsx +63 -0
  136. package/src/checkout-v2/components/right/currency-grid.tsx +59 -0
  137. package/src/checkout-v2/components/right/customer-info-card.tsx +214 -0
  138. package/src/checkout-v2/components/right/status-feedback.tsx +35 -0
  139. package/src/checkout-v2/components/right/submit-button.tsx +37 -0
  140. package/src/checkout-v2/components/right/subscription-disclaimer.tsx +27 -0
  141. package/src/checkout-v2/components/shared/exchange-rate-footer.tsx +221 -0
  142. package/src/checkout-v2/components/shared/scenario-badge.tsx +51 -0
  143. package/src/checkout-v2/components/shared/total-display.tsx +112 -0
  144. package/src/checkout-v2/index.ts +2 -0
  145. package/src/checkout-v2/layouts/checkout-layout.tsx +232 -0
  146. package/src/checkout-v2/panels/left/composite-panel.tsx +465 -0
  147. package/src/checkout-v2/panels/left/credit-topup-panel.tsx +681 -0
  148. package/src/checkout-v2/panels/left/scenario-router.tsx +22 -0
  149. package/src/checkout-v2/panels/right/payment-panel.tsx +703 -0
  150. package/src/checkout-v2/types.ts +18 -0
  151. package/src/checkout-v2/utils/format.ts +204 -0
  152. package/src/checkout-v2/utils/scenario-detector.ts +30 -0
  153. package/src/checkout-v2/views/error-view.tsx +293 -0
  154. package/src/checkout-v2/views/loading-view.tsx +162 -0
  155. package/src/checkout-v2/views/success-view.tsx +770 -0
  156. package/src/components/phone-field.tsx +119 -0
  157. package/src/index.ts +3 -0
  158. package/src/locales/en.tsx +45 -4
  159. package/src/locales/zh.tsx +43 -4
  160. package/src/payment/form/index.tsx +16 -1
@@ -0,0 +1,119 @@
1
+ import { useEffect, useRef, useCallback } from 'react';
2
+ import { Box, InputBase, InputAdornment, Typography } from '@mui/material';
3
+ import { defaultCountries, usePhoneInput } from 'react-international-phone';
4
+ import { useMount } from 'ahooks';
5
+
6
+ import CountrySelect from './country-select';
7
+ import { isValidCountry } from '../libs/util';
8
+ import { getPhoneUtil } from '../libs/phone-validator';
9
+
10
+ export interface PhoneFieldProps {
11
+ value: string;
12
+ country: string;
13
+ onChange: (phone: string) => void;
14
+ onCountryChange: (country: string) => void;
15
+ label: string;
16
+ error?: string;
17
+ onBlur?: () => void;
18
+ }
19
+
20
+ /**
21
+ * Phone input with country flag + dial code selector.
22
+ * Standalone version of V1 PhoneInput — no react-hook-form dependency.
23
+ */
24
+ export default function PhoneField({
25
+ value,
26
+ country: externalCountry,
27
+ onChange,
28
+ onCountryChange,
29
+ label,
30
+ error = undefined,
31
+ onBlur = undefined,
32
+ }: PhoneFieldProps) {
33
+ const isUpdatingRef = useRef(false);
34
+
35
+ const safeUpdate = useCallback((callback: () => void) => {
36
+ if (isUpdatingRef.current) return;
37
+ try {
38
+ isUpdatingRef.current = true;
39
+ callback();
40
+ } finally {
41
+ requestAnimationFrame(() => {
42
+ isUpdatingRef.current = false;
43
+ });
44
+ }
45
+ }, []);
46
+
47
+ const { phone, handlePhoneValueChange, inputRef, country, setCountry } = usePhoneInput({
48
+ defaultCountry: isValidCountry(externalCountry) ? externalCountry : 'us',
49
+ value: value || '',
50
+ countries: defaultCountries,
51
+ onChange: (data) => {
52
+ safeUpdate(() => {
53
+ onChange(data.phone);
54
+ onCountryChange(data.country);
55
+ });
56
+ },
57
+ });
58
+
59
+ // Preload phone validator (matches V1 behavior)
60
+ useMount(() => {
61
+ getPhoneUtil().catch((err) => {
62
+ console.error('Failed to preload phone validator:', err);
63
+ });
64
+ });
65
+
66
+ // Sync external country changes (e.g. from postal code CountrySelect)
67
+ useEffect(() => {
68
+ if (!externalCountry || externalCountry === country) return;
69
+ safeUpdate(() => {
70
+ setCountry(externalCountry);
71
+ });
72
+ }, [externalCountry, country, setCountry, safeUpdate]);
73
+
74
+ const handleCountryChange = useCallback(
75
+ (v: string) => {
76
+ safeUpdate(() => {
77
+ setCountry(v);
78
+ onCountryChange(v);
79
+ });
80
+ },
81
+ [setCountry, safeUpdate, onCountryChange]
82
+ );
83
+
84
+ return (
85
+ <Box sx={{ mb: 1.5 }}>
86
+ <Typography sx={{ fontSize: 13, fontWeight: 600, color: 'text.primary', mb: 0.5 }}>{label}</Typography>
87
+ <InputBase
88
+ fullWidth
89
+ value={phone}
90
+ onChange={handlePhoneValueChange}
91
+ onBlur={onBlur}
92
+ type="tel"
93
+ inputRef={inputRef}
94
+ startAdornment={
95
+ <InputAdornment position="start" sx={{ mr: 0.25, ml: -0.5 }}>
96
+ <CountrySelect
97
+ value={country}
98
+ onChange={handleCountryChange}
99
+ sx={{
100
+ '.MuiOutlinedInput-notchedOutline': { borderColor: 'transparent !important' },
101
+ '& .MuiSelect-select': { py: 0, pr: '20px !important' },
102
+ }}
103
+ showDialCode
104
+ />
105
+ </InputAdornment>
106
+ }
107
+ sx={{
108
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'grey.50'),
109
+ borderRadius: '8px',
110
+ px: 1.5,
111
+ py: 0.75,
112
+ fontSize: 14,
113
+ '& .MuiInputBase-input': { p: 0 },
114
+ }}
115
+ />
116
+ {error && <Typography sx={{ fontSize: 12, color: 'error.main', mt: 0.25 }}>{error}</Typography>}
117
+ </Box>
118
+ );
119
+ }
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ import PromotionCode from './components/promotion-code';
46
46
  import SourceDataViewer from './components/source-data-viewer';
47
47
  import SlippageConfig from './components/slippage-config';
48
48
  import DynamicPricingUnavailable from './components/dynamic-pricing-unavailable';
49
+ import { CheckoutV2 } from './checkout-v2';
49
50
 
50
51
  export { PaymentThemeProvider } from './theme';
51
52
 
@@ -116,8 +117,10 @@ export {
116
117
  DynamicPricingUnavailable,
117
118
  PromotionSection,
118
119
  TotalSection,
120
+ CheckoutV2,
119
121
  };
120
122
 
121
123
  export type { CountrySelectProps } from './components/country-select';
122
124
  export type { StripePaymentActionProps } from './components/stripe-payment-action';
123
125
  export type { SlippageConfigValue, SlippageConfigProps } from './components/slippage-config';
126
+ export type { CheckoutV2Props } from './checkout-v2';
@@ -21,6 +21,7 @@ export default flat({
21
21
  setup: 'Setup',
22
22
  amount: 'Amount',
23
23
  total: 'Total',
24
+ totalDue: 'Total Due',
24
25
  subtotal: 'Subtotal',
25
26
  status: 'Status',
26
27
  livemode: 'Test mode',
@@ -87,6 +88,7 @@ export default flat({
87
88
  viewConsumptionDetail: 'Consumption Detail',
88
89
  customer: 'Customer',
89
90
  currency: 'Currency',
91
+ network: 'Network',
90
92
  custom: 'Custom',
91
93
  description: 'Description',
92
94
  statementDescriptor: 'Statement descriptor',
@@ -282,11 +284,14 @@ export default flat({
282
284
  free: "{count} {interval}{count > 1 ? 's' : ''} free",
283
285
  least: 'continue with at least',
284
286
  completed: {
285
- payment: 'Thanks for your purchase',
286
- subscription: 'Thanks for subscribing',
287
- setup: 'Thanks for subscribing',
288
- donate: 'Thanks for your tip',
287
+ payment: 'Purchase successful',
288
+ subscription: 'Subscription successful',
289
+ setup: 'Subscription successful',
290
+ donate: 'Donation successful',
289
291
  tip: 'A payment to {payee} has been completed. You can view the details of this payment in your account.',
292
+ summary: {
293
+ paid: "You've paid {amount}",
294
+ },
290
295
  },
291
296
  vendor: {
292
297
  accountRequired: 'This action requires a unified account. Please switch accounts and try again.',
@@ -375,6 +380,7 @@ export default flat({
375
380
  periodic: 'Grant {amount} every {interval}.',
376
381
  withRefresh: 'Grant {amount} every {interval}; unused credits expire with the next grant.',
377
382
  },
383
+ topupDescription: 'Purchase {creditName} at {unitPrice} per credit.',
378
384
  },
379
385
  expired: {
380
386
  title: 'Expired Link',
@@ -390,9 +396,44 @@ export default flat({
390
396
  title: 'Nothing to show here',
391
397
  description: 'It seems this checkout session is not configured properly',
392
398
  },
399
+ error: {
400
+ title: 'Something went wrong',
401
+ },
393
402
  orderSummary: 'Order Summary',
403
+ orderSummarySubtitle: 'Items included in this purchase',
394
404
  paymentDetails: 'Payment Details',
395
405
  productListTotal: 'Includes {total} items',
406
+ headerTitle: {
407
+ subscribe: 'Subscribe to {name}',
408
+ purchase: 'Purchase {name}',
409
+ },
410
+ planFeatures: 'Plan Features',
411
+ typeBadge: {
412
+ subscription: 'SUBSCRIPTION',
413
+ topup: 'TOP-UP',
414
+ oneTime: 'ONE-TIME',
415
+ },
416
+ subtitle: {
417
+ subscriptionInterval: '{interval} subscription',
418
+ creditsTopup: 'Credits top-up',
419
+ addonFor: 'Add-on for {product}',
420
+ oneTime: 'One-time purchase',
421
+ },
422
+ creditTopup: {
423
+ title: 'Get {name}',
424
+ question: 'How many {symbol} do you want?',
425
+ credits: 'Credits',
426
+ increment: 'Increments of {step} {symbol}',
427
+ validFor: 'Credits are valid for {duration} {unit} after purchase.',
428
+ willReceive: "You'll receive",
429
+ packInfo: 'Includes {packs} credit packs ({perPack} per pack)',
430
+ autoMatch: "We'll automatically match the closest credit pack.",
431
+ autoMatchTooltip: '{symbol} are sold in packs of {step}',
432
+ pendingWarning:
433
+ 'You have a usage overage of {pendingAmount}. You need at least {minCredits} to restore access.',
434
+ pendingEnough:
435
+ 'You have a usage overage of {pendingAmount}. After covering the overage, your available balance will be {availableAmount}.',
436
+ },
396
437
  promotion: {
397
438
  add_code: 'Add promotion code',
398
439
  enter_code: 'Enter promotion code',
@@ -21,6 +21,7 @@ export default flat({
21
21
  accessDenied: '您无权访问其他客户的数据',
22
22
  amount: '金额',
23
23
  total: '总计',
24
+ totalDue: '应付总额',
24
25
  subtotal: '小计',
25
26
  status: '状态',
26
27
  livemode: '测试模式',
@@ -87,6 +88,7 @@ export default flat({
87
88
  viewConsumptionDetail: '消费详情',
88
89
  customer: '客户',
89
90
  currency: '币种',
91
+ network: '网络',
90
92
  custom: '自定义',
91
93
  description: '描述',
92
94
  statementDescriptor: '声明描述',
@@ -278,11 +280,14 @@ export default flat({
278
280
  free: '免费试用 {count} {interval}',
279
281
  least: '至少',
280
282
  completed: {
281
- payment: '感谢您的购买',
282
- subscription: '感谢您的订阅',
283
- setup: '感谢您的订阅',
284
- donate: '感谢您的支持',
283
+ payment: '购买成功',
284
+ subscription: '订阅成功',
285
+ setup: '订阅成功',
286
+ donate: '捐赠成功',
285
287
  tip: '向 {payee} 的付款已完成。您可以在您的账户中查看此付款的详细信息。',
288
+ summary: {
289
+ paid: '本次支付 {amount}',
290
+ },
286
291
  },
287
292
  vendor: {
288
293
  accountRequired: '您当前使用的是非统一账户登录,此服务需要您使用统一账户。请切换到统一账户登录后重试。',
@@ -405,14 +410,48 @@ export default flat({
405
410
  periodic: '每{interval}发放 {amount}。',
406
411
  withRefresh: '每{interval}发放 {amount},未使用额度将在下次发放时过期。',
407
412
  },
413
+ topupDescription: '购买 {creditName},单价 {unitPrice}/额度。',
408
414
  },
409
415
  emptyItems: {
410
416
  title: '没有任何购买项目',
411
417
  description: '可能这个付款链接没有正确配置',
412
418
  },
419
+ error: {
420
+ title: '出了点问题',
421
+ },
413
422
  orderSummary: '订单概览',
423
+ orderSummarySubtitle: '本次交易包含以下项目',
414
424
  paymentDetails: '支付信息',
415
425
  productListTotal: '包括 {total} 项',
426
+ headerTitle: {
427
+ subscribe: '订阅 {name}',
428
+ purchase: '购买 {name}',
429
+ },
430
+ planFeatures: '功能特性',
431
+ typeBadge: {
432
+ subscription: '订阅',
433
+ topup: '充值',
434
+ oneTime: '一次性购买',
435
+ },
436
+ subtitle: {
437
+ subscriptionInterval: '{interval}订阅',
438
+ creditsTopup: '余额充值',
439
+ addonFor: '{product} 的附加项',
440
+ oneTime: '一次性购买',
441
+ },
442
+ creditTopup: {
443
+ title: '购买 {name}',
444
+ question: '你需要多少 {symbol}?',
445
+ credits: '额度',
446
+ increment: '每次增减 {step} {symbol}',
447
+ validFor: '额度在购买后 {duration} {unit}内有效。',
448
+ willReceive: '你将获得',
449
+ packInfo: '包含 {packs} 个额度包(每包 {perPack})',
450
+ autoMatch: '我们将自动匹配最接近的额度包。',
451
+ autoMatchTooltip: '{symbol} 按每包 {step} 个出售',
452
+ pendingWarning: '您有 {pendingAmount} 的使用超额,至少需要购买 {minCredits} 才能恢复访问。',
453
+ pendingEnough: '您有 {pendingAmount} 的使用超额,扣除后您的可用余额将为 {availableAmount}。',
454
+ },
416
455
  connectModal: {
417
456
  title: '{action}',
418
457
  scan: '使用以下方式完成本次支付',
@@ -242,6 +242,9 @@ export default function PaymentForm({
242
242
  } = useFormContext();
243
243
  const errorRef = useRef<HTMLDivElement | null>(null);
244
244
  const processingRef = useRef(false);
245
+ // Stable idempotency key: reuse same Quote on retry (intent: "retry with same Quote")
246
+ const idempotencyKeyRef = useRef('');
247
+ const sessionFingerprintRef = useRef('');
245
248
  const quantityInventoryStatus = useMemo(() => {
246
249
  let status = true;
247
250
  for (const item of checkoutSession.line_items) {
@@ -975,10 +978,22 @@ export default function PaymentForm({
975
978
  checkoutSession.line_items?.find((item: TLineItemExpanded) => (item as any)?.exchange_rate)?.exchange_rate ||
976
979
  undefined;
977
980
 
981
+ // Stable idempotency key: only regenerate when payment context changes
982
+ // Same context retry → reuse Quote (intent: "Failed Payments don't invalidate Quote")
983
+ const items = (checkoutSession.line_items || []) as TLineItemExpanded[];
984
+ const itemsSig = items
985
+ .map((i: TLineItemExpanded) => `${(i as any).upsell_price_id || i.price_id}:${i.quantity}`)
986
+ .join('|');
987
+ const fingerprint = `${checkoutSession.id}-${paymentCurrency?.id}-${itemsSig}`;
988
+ if (fingerprint !== sessionFingerprintRef.current || !idempotencyKeyRef.current) {
989
+ sessionFingerprintRef.current = fingerprint;
990
+ idempotencyKeyRef.current = generateIdempotencyKey(checkoutSession.id, paymentCurrency?.id || '');
991
+ }
992
+
978
993
  const payload = {
979
994
  ...data,
980
995
  // Final Freeze: Include these for new quote creation at submit
981
- idempotency_key: generateIdempotencyKey(checkoutSession.id, paymentCurrency?.id || ''),
996
+ idempotency_key: idempotencyKeyRef.current,
982
997
  preview_rate: (previewRate as unknown as string) || undefined,
983
998
  price_confirmed: state.priceChangeConfirm?.formData ? true : undefined,
984
999
  };