@blocklet/payment-react 1.24.4 → 1.25.1

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 (98) hide show
  1. package/es/components/auto-topup/modal.d.ts +2 -0
  2. package/es/components/auto-topup/modal.js +48 -6
  3. package/es/components/auto-topup/product-card.d.ts +16 -1
  4. package/es/components/auto-topup/product-card.js +97 -15
  5. package/es/components/dynamic-pricing-unavailable.d.ts +9 -0
  6. package/es/components/dynamic-pricing-unavailable.js +58 -0
  7. package/es/components/loading-amount.d.ts +17 -0
  8. package/es/components/loading-amount.js +46 -0
  9. package/es/components/price-change-confirm.d.ts +18 -0
  10. package/es/components/price-change-confirm.js +107 -0
  11. package/es/components/quote-details-panel.d.ts +21 -0
  12. package/es/components/quote-details-panel.js +170 -0
  13. package/es/components/quote-lock-banner.d.ts +7 -0
  14. package/es/components/quote-lock-banner.js +79 -0
  15. package/es/components/slippage-config.d.ts +20 -0
  16. package/es/components/slippage-config.js +261 -0
  17. package/es/history/invoice/list.js +125 -15
  18. package/es/hooks/dynamic-pricing.d.ts +102 -0
  19. package/es/hooks/dynamic-pricing.js +393 -0
  20. package/es/index.d.ts +6 -1
  21. package/es/index.js +9 -1
  22. package/es/libs/util.d.ts +42 -5
  23. package/es/libs/util.js +345 -57
  24. package/es/locales/en.js +114 -3
  25. package/es/locales/zh.js +114 -3
  26. package/es/payment/form/index.d.ts +4 -1
  27. package/es/payment/form/index.js +454 -22
  28. package/es/payment/index.d.ts +1 -1
  29. package/es/payment/index.js +279 -16
  30. package/es/payment/product-item.d.ts +26 -1
  31. package/es/payment/product-item.js +330 -51
  32. package/es/payment/summary-section/promotion-section.d.ts +32 -0
  33. package/es/payment/summary-section/promotion-section.js +143 -0
  34. package/es/payment/summary-section/total-section.d.ts +39 -0
  35. package/es/payment/summary-section/total-section.js +83 -0
  36. package/es/payment/summary.d.ts +17 -2
  37. package/es/payment/summary.js +300 -253
  38. package/es/types/index.d.ts +11 -0
  39. package/lib/components/auto-topup/modal.d.ts +2 -0
  40. package/lib/components/auto-topup/modal.js +54 -6
  41. package/lib/components/auto-topup/product-card.d.ts +16 -1
  42. package/lib/components/auto-topup/product-card.js +75 -7
  43. package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
  44. package/lib/components/dynamic-pricing-unavailable.js +81 -0
  45. package/lib/components/loading-amount.d.ts +17 -0
  46. package/lib/components/loading-amount.js +53 -0
  47. package/lib/components/price-change-confirm.d.ts +18 -0
  48. package/lib/components/price-change-confirm.js +157 -0
  49. package/lib/components/quote-details-panel.d.ts +21 -0
  50. package/lib/components/quote-details-panel.js +226 -0
  51. package/lib/components/quote-lock-banner.d.ts +7 -0
  52. package/lib/components/quote-lock-banner.js +93 -0
  53. package/lib/components/slippage-config.d.ts +20 -0
  54. package/lib/components/slippage-config.js +316 -0
  55. package/lib/history/invoice/list.js +167 -27
  56. package/lib/hooks/dynamic-pricing.d.ts +102 -0
  57. package/lib/hooks/dynamic-pricing.js +390 -0
  58. package/lib/index.d.ts +6 -1
  59. package/lib/index.js +32 -0
  60. package/lib/libs/util.d.ts +42 -5
  61. package/lib/libs/util.js +367 -49
  62. package/lib/locales/en.js +114 -3
  63. package/lib/locales/zh.js +114 -3
  64. package/lib/payment/form/index.d.ts +4 -1
  65. package/lib/payment/form/index.js +476 -20
  66. package/lib/payment/index.d.ts +1 -1
  67. package/lib/payment/index.js +308 -14
  68. package/lib/payment/product-item.d.ts +26 -1
  69. package/lib/payment/product-item.js +270 -35
  70. package/lib/payment/summary-section/promotion-section.d.ts +32 -0
  71. package/lib/payment/summary-section/promotion-section.js +133 -0
  72. package/lib/payment/summary-section/total-section.d.ts +39 -0
  73. package/lib/payment/summary-section/total-section.js +117 -0
  74. package/lib/payment/summary.d.ts +17 -2
  75. package/lib/payment/summary.js +205 -127
  76. package/lib/types/index.d.ts +11 -0
  77. package/package.json +3 -3
  78. package/src/components/auto-topup/modal.tsx +59 -6
  79. package/src/components/auto-topup/product-card.tsx +118 -11
  80. package/src/components/dynamic-pricing-unavailable.tsx +69 -0
  81. package/src/components/loading-amount.tsx +66 -0
  82. package/src/components/price-change-confirm.tsx +136 -0
  83. package/src/components/quote-details-panel.tsx +218 -0
  84. package/src/components/quote-lock-banner.tsx +99 -0
  85. package/src/components/slippage-config.tsx +336 -0
  86. package/src/history/invoice/list.tsx +143 -9
  87. package/src/hooks/dynamic-pricing.ts +617 -0
  88. package/src/index.ts +9 -0
  89. package/src/libs/util.ts +473 -58
  90. package/src/locales/en.tsx +117 -0
  91. package/src/locales/zh.tsx +111 -0
  92. package/src/payment/form/index.tsx +561 -19
  93. package/src/payment/index.tsx +349 -10
  94. package/src/payment/product-item.tsx +451 -37
  95. package/src/payment/summary-section/promotion-section.tsx +172 -0
  96. package/src/payment/summary-section/total-section.tsx +141 -0
  97. package/src/payment/summary.tsx +334 -192
  98. package/src/types/index.ts +15 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * PromotionSection Component
3
+ *
4
+ * Handles promotion code input, applied discounts display, and removal.
5
+ */
6
+
7
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
8
+ import type { TPaymentCurrency } from '@blocklet/payment-types';
9
+ import { Close, LocalOffer } from '@mui/icons-material';
10
+ import { Box, Button, Stack, Typography } from '@mui/material';
11
+ import PromotionCode from '../../components/promotion-code';
12
+ import { formatAmount, formatCouponTerms } from '../../libs/util';
13
+
14
+ export interface DiscountInfo {
15
+ promotion_code?: string;
16
+ coupon?: string;
17
+ discount_amount?: string;
18
+ promotion_code_details?: {
19
+ code?: string;
20
+ };
21
+ coupon_details?: any;
22
+ verification_data?: {
23
+ code?: string;
24
+ };
25
+ }
26
+
27
+ export interface PromotionSectionProps {
28
+ checkoutSessionId: string;
29
+ currency: TPaymentCurrency;
30
+ currencyId: string;
31
+ discounts: DiscountInfo[];
32
+ allowPromotionCodes: boolean;
33
+ completed?: boolean;
34
+ disabled?: boolean;
35
+ onPromotionUpdate: () => void;
36
+ onRemovePromotion: (sessionId: string) => void;
37
+ // For dynamic pricing: frontend-calculated discount amount
38
+ calculatedDiscountAmount?: string | null;
39
+ isRateLoading?: boolean;
40
+ }
41
+
42
+ export default function PromotionSection({
43
+ checkoutSessionId,
44
+ currency,
45
+ currencyId,
46
+ discounts,
47
+ allowPromotionCodes,
48
+ completed = false,
49
+ disabled = false,
50
+ onPromotionUpdate,
51
+ onRemovePromotion,
52
+ calculatedDiscountAmount = null,
53
+ isRateLoading = false,
54
+ }: PromotionSectionProps) {
55
+ const { t, locale } = useLocaleContext();
56
+
57
+ const hasDiscounts = discounts?.length > 0;
58
+
59
+ const getAppliedPromotionCodes = () => {
60
+ if (!discounts?.length) return [];
61
+
62
+ return discounts
63
+ .filter((discount) => discount.promotion_code || discount.coupon)
64
+ .map((discount) => ({
65
+ id: (discount.promotion_code || discount.coupon) as string,
66
+ code: discount.verification_data?.code || 'APPLIED',
67
+ discount_amount: discount.discount_amount,
68
+ }));
69
+ };
70
+
71
+ // Only show add button if no discounts applied
72
+ if (allowPromotionCodes && !hasDiscounts) {
73
+ return (
74
+ <Box sx={{ mt: 1 }}>
75
+ <PromotionCode
76
+ checkoutSessionId={checkoutSessionId}
77
+ initialAppliedCodes={getAppliedPromotionCodes()}
78
+ disabled={completed}
79
+ onUpdate={onPromotionUpdate}
80
+ currencyId={currencyId}
81
+ />
82
+ </Box>
83
+ );
84
+ }
85
+
86
+ // Show applied discounts
87
+ if (!hasDiscounts) {
88
+ return null;
89
+ }
90
+
91
+ return (
92
+ <Box>
93
+ {discounts.map((discount) => {
94
+ const promotionCodeInfo = discount.promotion_code_details;
95
+ const couponInfo = discount.coupon_details;
96
+ const discountDescription = couponInfo ? formatCouponTerms(couponInfo, currency, locale) : '';
97
+ const notSupported = discountDescription === t('payment.checkout.coupon.noDiscount');
98
+
99
+ return (
100
+ <Stack key={discount.promotion_code || discount.coupon || `discount-${discount.discount_amount}`}>
101
+ <Stack
102
+ direction="row"
103
+ spacing={1}
104
+ sx={{
105
+ justifyContent: 'space-between',
106
+ alignItems: 'center',
107
+ }}>
108
+ <Stack
109
+ direction="row"
110
+ spacing={1}
111
+ sx={{
112
+ alignItems: 'center',
113
+ backgroundColor: 'grey.100',
114
+ width: 'fit-content',
115
+ px: 1,
116
+ py: 1,
117
+ borderRadius: 1,
118
+ }}>
119
+ <Typography
120
+ sx={{
121
+ fontWeight: 'medium',
122
+ display: 'flex',
123
+ alignItems: 'center',
124
+ gap: 0.5,
125
+ }}>
126
+ <LocalOffer sx={{ color: 'warning.main', fontSize: 'small' }} />
127
+ {promotionCodeInfo?.code || discount.verification_data?.code || t('payment.checkout.discount')}
128
+ </Typography>
129
+ {!completed && (
130
+ <Button
131
+ size="small"
132
+ disabled={disabled}
133
+ onClick={() => onRemovePromotion(checkoutSessionId)}
134
+ sx={{
135
+ minWidth: 'auto',
136
+ width: 16,
137
+ height: 16,
138
+ color: 'text.secondary',
139
+ '&.Mui-disabled': {
140
+ color: 'text.disabled',
141
+ },
142
+ }}>
143
+ <Close sx={{ fontSize: 14 }} />
144
+ </Button>
145
+ )}
146
+ </Stack>
147
+ <Typography
148
+ sx={{
149
+ color: 'text.secondary',
150
+ opacity: isRateLoading ? 0 : 1,
151
+ transition: 'opacity 300ms ease-in-out',
152
+ }}>
153
+ -{formatAmount(calculatedDiscountAmount || '0', currency.decimal)} {currency.symbol}
154
+ </Typography>
155
+ </Stack>
156
+ {/* Show discount description */}
157
+ {discountDescription && (
158
+ <Typography
159
+ sx={{
160
+ fontSize: 'small',
161
+ color: notSupported ? 'error.main' : 'text.secondary',
162
+ mt: 0.5,
163
+ }}>
164
+ {discountDescription}
165
+ </Typography>
166
+ )}
167
+ </Stack>
168
+ );
169
+ })}
170
+ </Box>
171
+ );
172
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * TotalSection Component
3
+ *
4
+ * Displays total amount, USD equivalent, exchange rate details,
5
+ * and quote information for dynamic pricing.
6
+ */
7
+
8
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
9
+ import type { TPaymentCurrency } from '@blocklet/payment-types';
10
+ import { Box, Skeleton, Stack, Tooltip, Typography } from '@mui/material';
11
+ import PaymentAmount from '../amount';
12
+ import QuoteDetailsPanel from '../../components/quote-details-panel';
13
+ import type { SlippageConfigValue } from '../../components/slippage-config';
14
+
15
+ export interface RateInfo {
16
+ exchangeRate: string | null;
17
+ baseCurrency: string;
18
+ providerName?: string | null;
19
+ providerId?: string | null;
20
+ timestampMs?: number | null;
21
+ }
22
+
23
+ export interface QuoteDetailRow {
24
+ label: string;
25
+ value: string | React.ReactNode;
26
+ isSlippage?: boolean;
27
+ }
28
+
29
+ export interface TotalSectionProps {
30
+ totalAmountText: string;
31
+ totalUsdDisplay: string | null;
32
+ currency: TPaymentCurrency;
33
+ hasDynamicPricing: boolean;
34
+ rateDisplay: string | null;
35
+ rateInfo: RateInfo;
36
+ quoteDetailRows: QuoteDetailRow[];
37
+ currentSlippagePercent: number;
38
+ slippageConfig?: SlippageConfigValue;
39
+ isPriceLocked: boolean;
40
+ isSubscription: boolean;
41
+ completed?: boolean;
42
+ onSlippageChange?: (slippageConfig: SlippageConfigValue) => void;
43
+ isStripePayment?: boolean;
44
+ thenInfo?: string;
45
+ isRateLoading?: boolean; // Loading state for skeleton display during currency switch
46
+ }
47
+
48
+ export default function TotalSection({
49
+ totalAmountText,
50
+ totalUsdDisplay,
51
+ currency,
52
+ hasDynamicPricing,
53
+ rateDisplay,
54
+ rateInfo,
55
+ quoteDetailRows,
56
+ currentSlippagePercent,
57
+ slippageConfig = undefined,
58
+ isPriceLocked,
59
+ isSubscription,
60
+ completed = false,
61
+ onSlippageChange = undefined,
62
+ isStripePayment = false,
63
+ thenInfo = '',
64
+ isRateLoading = false,
65
+ }: TotalSectionProps) {
66
+ const { t } = useLocaleContext();
67
+
68
+ return (
69
+ <>
70
+ <Stack
71
+ sx={{
72
+ display: 'flex',
73
+ justifyContent: 'space-between',
74
+ flexDirection: 'row',
75
+ alignItems: 'flex-start',
76
+ width: '100%',
77
+ }}>
78
+ <Box className="base-label">{t('common.total')}</Box>
79
+ <Stack sx={{ alignItems: 'flex-end' }}>
80
+ {isRateLoading ? (
81
+ <Skeleton variant="text" width={100} height={24} />
82
+ ) : (
83
+ <PaymentAmount amount={totalAmountText} sx={{ fontSize: '16px' }} />
84
+ )}
85
+
86
+ {/* USD equivalent display - hide for Stripe payments since base_amount is already USD */}
87
+ {/* No skeleton for USD - it's based on base_amount which doesn't change */}
88
+ {hasDynamicPricing &&
89
+ !isStripePayment &&
90
+ !isRateLoading &&
91
+ (totalUsdDisplay ? (
92
+ <Typography sx={{ fontSize: '0.7875rem', color: 'text.lighter' }}>≈ ${totalUsdDisplay}</Typography>
93
+ ) : (
94
+ <Tooltip title={t('payment.checkout.quote.referenceUnavailable')} placement="top">
95
+ <Box component="span">
96
+ <Typography sx={{ fontSize: '0.7875rem', color: 'text.lighter' }}>≈ —</Typography>
97
+ </Box>
98
+ </Tooltip>
99
+ ))}
100
+
101
+ {/* Quote details panel for dynamic pricing - hide for Stripe payments since no exchange rate conversion needed */}
102
+ {/* Show panel if hasDynamicPricing is true, even without rateDisplay (for metered subscriptions waiting for first usage) */}
103
+ {hasDynamicPricing && !isStripePayment && (
104
+ <QuoteDetailsPanel
105
+ rateLine={
106
+ rateDisplay ? t('payment.checkout.quote.rateLine', { symbol: currency.symbol, rate: rateDisplay }) : ''
107
+ }
108
+ rows={quoteDetailRows}
109
+ isSubscription={isSubscription}
110
+ slippageValue={currentSlippagePercent}
111
+ slippageConfig={slippageConfig}
112
+ onSlippageChange={!completed && onSlippageChange ? onSlippageChange : undefined}
113
+ exchangeRate={rateInfo.exchangeRate}
114
+ baseCurrency={rateInfo.baseCurrency}
115
+ disabled={isPriceLocked}
116
+ />
117
+ )}
118
+
119
+ {/* Lock expired warning removed - per Final Freeze design, Quote doesn't expire by time */}
120
+ </Stack>
121
+ </Stack>
122
+ {thenInfo && (
123
+ <Stack
124
+ sx={{
125
+ display: 'flex',
126
+ justifyContent: 'space-between',
127
+ flexDirection: 'row',
128
+ alignItems: 'flex-start',
129
+ width: '100%',
130
+ borderTop: '1px solid',
131
+ borderColor: 'divider',
132
+ pt: 1,
133
+ mt: 1,
134
+ }}>
135
+ <Box className="base-label">{t('common.nextCharge')}</Box>
136
+ <Typography sx={{ fontSize: '16px', color: 'text.secondary' }}>{thenInfo}</Typography>
137
+ </Stack>
138
+ )}
139
+ </>
140
+ );
141
+ }