@blocklet/payment-react 1.24.3 → 1.25.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.
- package/es/components/auto-topup/modal.d.ts +2 -0
- package/es/components/auto-topup/modal.js +48 -6
- package/es/components/auto-topup/product-card.d.ts +16 -1
- package/es/components/auto-topup/product-card.js +97 -15
- package/es/components/dynamic-pricing-unavailable.d.ts +9 -0
- package/es/components/dynamic-pricing-unavailable.js +58 -0
- package/es/components/loading-amount.d.ts +17 -0
- package/es/components/loading-amount.js +46 -0
- package/es/components/price-change-confirm.d.ts +18 -0
- package/es/components/price-change-confirm.js +107 -0
- package/es/components/quote-details-panel.d.ts +21 -0
- package/es/components/quote-details-panel.js +170 -0
- package/es/components/quote-lock-banner.d.ts +7 -0
- package/es/components/quote-lock-banner.js +79 -0
- package/es/components/slippage-config.d.ts +20 -0
- package/es/components/slippage-config.js +261 -0
- package/es/history/credit/transactions-list.js +11 -1
- package/es/history/invoice/list.js +125 -15
- package/es/hooks/dynamic-pricing.d.ts +102 -0
- package/es/hooks/dynamic-pricing.js +393 -0
- package/es/index.d.ts +6 -1
- package/es/index.js +9 -1
- package/es/libs/util.d.ts +42 -5
- package/es/libs/util.js +345 -57
- package/es/locales/en.js +114 -3
- package/es/locales/zh.js +114 -3
- package/es/payment/form/index.d.ts +4 -1
- package/es/payment/form/index.js +454 -22
- package/es/payment/index.d.ts +1 -1
- package/es/payment/index.js +279 -16
- package/es/payment/product-item.d.ts +26 -1
- package/es/payment/product-item.js +330 -51
- package/es/payment/summary-section/promotion-section.d.ts +32 -0
- package/es/payment/summary-section/promotion-section.js +143 -0
- package/es/payment/summary-section/total-section.d.ts +39 -0
- package/es/payment/summary-section/total-section.js +83 -0
- package/es/payment/summary.d.ts +17 -2
- package/es/payment/summary.js +300 -253
- package/es/types/index.d.ts +11 -0
- package/lib/components/auto-topup/modal.d.ts +2 -0
- package/lib/components/auto-topup/modal.js +54 -6
- package/lib/components/auto-topup/product-card.d.ts +16 -1
- package/lib/components/auto-topup/product-card.js +75 -7
- package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
- package/lib/components/dynamic-pricing-unavailable.js +81 -0
- package/lib/components/loading-amount.d.ts +17 -0
- package/lib/components/loading-amount.js +53 -0
- package/lib/components/price-change-confirm.d.ts +18 -0
- package/lib/components/price-change-confirm.js +157 -0
- package/lib/components/quote-details-panel.d.ts +21 -0
- package/lib/components/quote-details-panel.js +226 -0
- package/lib/components/quote-lock-banner.d.ts +7 -0
- package/lib/components/quote-lock-banner.js +93 -0
- package/lib/components/slippage-config.d.ts +20 -0
- package/lib/components/slippage-config.js +316 -0
- package/lib/history/credit/transactions-list.js +11 -1
- package/lib/history/invoice/list.js +167 -27
- package/lib/hooks/dynamic-pricing.d.ts +102 -0
- package/lib/hooks/dynamic-pricing.js +390 -0
- package/lib/index.d.ts +6 -1
- package/lib/index.js +32 -0
- package/lib/libs/util.d.ts +42 -5
- package/lib/libs/util.js +367 -49
- package/lib/locales/en.js +114 -3
- package/lib/locales/zh.js +114 -3
- package/lib/payment/form/index.d.ts +4 -1
- package/lib/payment/form/index.js +476 -20
- package/lib/payment/index.d.ts +1 -1
- package/lib/payment/index.js +308 -14
- package/lib/payment/product-item.d.ts +26 -1
- package/lib/payment/product-item.js +270 -35
- package/lib/payment/summary-section/promotion-section.d.ts +32 -0
- package/lib/payment/summary-section/promotion-section.js +133 -0
- package/lib/payment/summary-section/total-section.d.ts +39 -0
- package/lib/payment/summary-section/total-section.js +117 -0
- package/lib/payment/summary.d.ts +17 -2
- package/lib/payment/summary.js +205 -127
- package/lib/types/index.d.ts +11 -0
- package/package.json +3 -3
- package/src/components/auto-topup/modal.tsx +59 -6
- package/src/components/auto-topup/product-card.tsx +118 -11
- package/src/components/dynamic-pricing-unavailable.tsx +69 -0
- package/src/components/loading-amount.tsx +66 -0
- package/src/components/price-change-confirm.tsx +136 -0
- package/src/components/quote-details-panel.tsx +218 -0
- package/src/components/quote-lock-banner.tsx +99 -0
- package/src/components/slippage-config.tsx +336 -0
- package/src/history/credit/transactions-list.tsx +14 -1
- package/src/history/invoice/list.tsx +143 -9
- package/src/hooks/dynamic-pricing.ts +617 -0
- package/src/index.ts +9 -0
- package/src/libs/util.ts +473 -58
- package/src/locales/en.tsx +117 -0
- package/src/locales/zh.tsx +111 -0
- package/src/payment/form/index.tsx +561 -19
- package/src/payment/index.tsx +349 -10
- package/src/payment/product-item.tsx +451 -37
- package/src/payment/summary-section/promotion-section.tsx +172 -0
- package/src/payment/summary-section/total-section.tsx +141 -0
- package/src/payment/summary.tsx +334 -192
- 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
|
+
}
|