@blocklet/payment-react 1.25.10 → 1.26.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.
- package/es/checkout-v2/checkout-v2.d.ts +2 -0
- package/es/checkout-v2/checkout-v2.js +121 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.js +106 -0
- package/es/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/es/checkout-v2/components/left/billing-toggle.js +118 -0
- package/es/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/es/checkout-v2/components/left/cross-sell-card.js +167 -0
- package/es/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/es/checkout-v2/components/left/product-item-card.js +571 -0
- package/es/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/es/checkout-v2/components/left/promotion-input.js +178 -0
- package/es/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/es/checkout-v2/components/left/staking-breakdown.js +48 -0
- package/es/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/es/checkout-v2/components/left/trial-info.js +48 -0
- package/es/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/es/checkout-v2/components/right/currency-grid.js +48 -0
- package/es/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/es/checkout-v2/components/right/customer-info-card.js +156 -0
- package/es/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/es/checkout-v2/components/right/status-feedback.js +17 -0
- package/es/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/es/checkout-v2/components/right/submit-button.js +29 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.js +8 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.js +182 -0
- package/es/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/es/checkout-v2/components/shared/scenario-badge.js +47 -0
- package/es/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/es/checkout-v2/components/shared/total-display.js +84 -0
- package/es/checkout-v2/index.d.ts +2 -0
- package/es/checkout-v2/index.js +1 -0
- package/es/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/es/checkout-v2/layouts/checkout-layout.js +226 -0
- package/es/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/composite-panel.js +423 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.js +611 -0
- package/es/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/es/checkout-v2/panels/left/scenario-router.js +19 -0
- package/es/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/es/checkout-v2/panels/right/payment-panel.js +644 -0
- package/es/checkout-v2/types.d.ts +15 -0
- package/es/checkout-v2/types.js +0 -0
- package/es/checkout-v2/utils/format.d.ts +59 -0
- package/es/checkout-v2/utils/format.js +125 -0
- package/es/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/es/checkout-v2/utils/scenario-detector.js +17 -0
- package/es/checkout-v2/views/error-view.d.ts +7 -0
- package/es/checkout-v2/views/error-view.js +269 -0
- package/es/checkout-v2/views/loading-view.d.ts +5 -0
- package/es/checkout-v2/views/loading-view.js +158 -0
- package/es/checkout-v2/views/success-view.d.ts +29 -0
- package/es/checkout-v2/views/success-view.js +614 -0
- package/es/components/phone-field.d.ts +14 -0
- package/es/components/phone-field.js +96 -0
- package/es/index.d.ts +3 -1
- package/es/index.js +3 -1
- package/es/locales/en.js +45 -6
- package/es/locales/zh.js +45 -6
- package/es/payment/form/index.js +10 -1
- package/lib/checkout-v2/checkout-v2.d.ts +2 -0
- package/lib/checkout-v2/checkout-v2.js +151 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +131 -0
- package/lib/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/lib/checkout-v2/components/left/billing-toggle.js +126 -0
- package/lib/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/lib/checkout-v2/components/left/cross-sell-card.js +257 -0
- package/lib/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/lib/checkout-v2/components/left/product-item-card.js +738 -0
- package/lib/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/lib/checkout-v2/components/left/promotion-input.js +220 -0
- package/lib/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/lib/checkout-v2/components/left/staking-breakdown.js +96 -0
- package/lib/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/lib/checkout-v2/components/left/trial-info.js +82 -0
- package/lib/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/lib/checkout-v2/components/right/currency-grid.js +96 -0
- package/lib/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/lib/checkout-v2/components/right/customer-info-card.js +246 -0
- package/lib/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/lib/checkout-v2/components/right/status-feedback.js +30 -0
- package/lib/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/lib/checkout-v2/components/right/submit-button.js +35 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.js +33 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.js +282 -0
- package/lib/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/lib/checkout-v2/components/shared/scenario-badge.js +57 -0
- package/lib/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/lib/checkout-v2/components/shared/total-display.js +154 -0
- package/lib/checkout-v2/index.d.ts +2 -0
- package/lib/checkout-v2/index.js +13 -0
- package/lib/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/lib/checkout-v2/layouts/checkout-layout.js +308 -0
- package/lib/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/composite-panel.js +515 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.js +795 -0
- package/lib/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/lib/checkout-v2/panels/left/scenario-router.js +29 -0
- package/lib/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/right/payment-panel.js +906 -0
- package/lib/checkout-v2/types.d.ts +15 -0
- package/lib/checkout-v2/types.js +1 -0
- package/lib/checkout-v2/utils/format.d.ts +59 -0
- package/lib/checkout-v2/utils/format.js +158 -0
- package/lib/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/lib/checkout-v2/utils/scenario-detector.js +23 -0
- package/lib/checkout-v2/views/error-view.d.ts +7 -0
- package/lib/checkout-v2/views/error-view.js +321 -0
- package/lib/checkout-v2/views/loading-view.d.ts +5 -0
- package/lib/checkout-v2/views/loading-view.js +168 -0
- package/lib/checkout-v2/views/success-view.d.ts +29 -0
- package/lib/checkout-v2/views/success-view.js +735 -0
- package/lib/components/phone-field.d.ts +14 -0
- package/lib/components/phone-field.js +130 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.js +8 -0
- package/lib/locales/en.js +45 -6
- package/lib/locales/zh.js +45 -6
- package/lib/payment/form/index.js +10 -1
- package/package.json +4 -3
- package/src/checkout-v2/checkout-v2.tsx +155 -0
- package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +134 -0
- package/src/checkout-v2/components/left/billing-toggle.tsx +122 -0
- package/src/checkout-v2/components/left/cross-sell-card.tsx +170 -0
- package/src/checkout-v2/components/left/product-item-card.tsx +642 -0
- package/src/checkout-v2/components/left/promotion-input.tsx +207 -0
- package/src/checkout-v2/components/left/staking-breakdown.tsx +57 -0
- package/src/checkout-v2/components/left/trial-info.tsx +63 -0
- package/src/checkout-v2/components/right/currency-grid.tsx +59 -0
- package/src/checkout-v2/components/right/customer-info-card.tsx +214 -0
- package/src/checkout-v2/components/right/status-feedback.tsx +35 -0
- package/src/checkout-v2/components/right/submit-button.tsx +37 -0
- package/src/checkout-v2/components/right/subscription-disclaimer.tsx +27 -0
- package/src/checkout-v2/components/shared/exchange-rate-footer.tsx +221 -0
- package/src/checkout-v2/components/shared/scenario-badge.tsx +51 -0
- package/src/checkout-v2/components/shared/total-display.tsx +112 -0
- package/src/checkout-v2/index.ts +2 -0
- package/src/checkout-v2/layouts/checkout-layout.tsx +232 -0
- package/src/checkout-v2/panels/left/composite-panel.tsx +465 -0
- package/src/checkout-v2/panels/left/credit-topup-panel.tsx +677 -0
- package/src/checkout-v2/panels/left/scenario-router.tsx +22 -0
- package/src/checkout-v2/panels/right/payment-panel.tsx +703 -0
- package/src/checkout-v2/types.ts +18 -0
- package/src/checkout-v2/utils/format.ts +205 -0
- package/src/checkout-v2/utils/scenario-detector.ts +30 -0
- package/src/checkout-v2/views/error-view.tsx +293 -0
- package/src/checkout-v2/views/loading-view.tsx +162 -0
- package/src/checkout-v2/views/success-view.tsx +770 -0
- package/src/components/phone-field.tsx +119 -0
- package/src/index.ts +3 -0
- package/src/locales/en.tsx +45 -4
- package/src/locales/zh.tsx +43 -4
- package/src/payment/form/index.tsx +16 -1
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { Box, IconButton, Typography, InputBase, Stack } from '@mui/material';
|
|
3
|
+
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
|
4
|
+
import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined';
|
|
5
|
+
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
|
6
|
+
import { useRequest } from 'ahooks';
|
|
7
|
+
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
8
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
9
|
+
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
|
10
|
+
import {
|
|
11
|
+
useCheckoutStatus,
|
|
12
|
+
useLineItems,
|
|
13
|
+
useSessionContext,
|
|
14
|
+
useProduct,
|
|
15
|
+
useExchangeRate,
|
|
16
|
+
useSlippage,
|
|
17
|
+
usePaymentMethodContext,
|
|
18
|
+
useSubmitFeature,
|
|
19
|
+
} from '@blocklet/payment-react-headless';
|
|
20
|
+
|
|
21
|
+
import { usePaymentContext } from '../../../contexts/payment';
|
|
22
|
+
import api from '../../../libs/api';
|
|
23
|
+
import { formatNumber, formatBNStr, formatCreditForCheckout, formatCreditAmount } from '../../../libs/util';
|
|
24
|
+
import { tSafe } from '../../utils/format';
|
|
25
|
+
import ScenarioBadge from '../../components/shared/scenario-badge';
|
|
26
|
+
import ExchangeRateFooter from '../../components/shared/exchange-rate-footer';
|
|
27
|
+
|
|
28
|
+
export default function CreditTopupPanel() {
|
|
29
|
+
const { t, locale } = useLocaleContext();
|
|
30
|
+
const { session, sessionData } = useSessionContext();
|
|
31
|
+
const { livemode } = useCheckoutStatus();
|
|
32
|
+
const { product } = useProduct();
|
|
33
|
+
const { currency, isStripe } = usePaymentMethodContext();
|
|
34
|
+
const lineItems = useLineItems();
|
|
35
|
+
const rate = useExchangeRate();
|
|
36
|
+
const slippage = useSlippage();
|
|
37
|
+
const { locked: configLocked } = useSubmitFeature();
|
|
38
|
+
|
|
39
|
+
const item = lineItems.items[0];
|
|
40
|
+
const activePrice: any = item ? (item as any).upsell_price || item.price : null;
|
|
41
|
+
|
|
42
|
+
// Credit metadata
|
|
43
|
+
const creditConfig = activePrice?.metadata?.credit_config;
|
|
44
|
+
const creditAmount = creditConfig?.credit_amount ? Number(creditConfig.credit_amount) : 0;
|
|
45
|
+
const creditCurrencyId = creditConfig?.currency_id;
|
|
46
|
+
|
|
47
|
+
// DID session for login state and userDid; getCurrency from /api/settings (same as V1)
|
|
48
|
+
const { session: didSession, getCurrency } = usePaymentContext();
|
|
49
|
+
|
|
50
|
+
// Credit currency: use getCurrency from /api/settings — same data source as V1
|
|
51
|
+
const creditCurrency = creditCurrencyId ? getCurrency(creditCurrencyId) : null;
|
|
52
|
+
const userDid = didSession?.user?.did || (sessionData?.customer as any)?.did;
|
|
53
|
+
|
|
54
|
+
const creditCurrencyDecimal = creditCurrency?.decimal;
|
|
55
|
+
const creditCurrencySymbol = creditCurrency?.symbol || 'Credits';
|
|
56
|
+
|
|
57
|
+
const currencySymbol = 'Credits';
|
|
58
|
+
|
|
59
|
+
// Fetch meter data by meter_id from price metadata
|
|
60
|
+
const meterId = activePrice?.metadata?.meter_id;
|
|
61
|
+
const { data: meterData } = useRequest(
|
|
62
|
+
async () => {
|
|
63
|
+
if (!meterId) return null;
|
|
64
|
+
try {
|
|
65
|
+
const { data } = await api.get(`/api/meters/public/${meterId}`);
|
|
66
|
+
return data;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{ refreshDeps: [meterId] }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const creditName = meterData?.name || product?.name || 'Credits';
|
|
75
|
+
|
|
76
|
+
// Credit config details
|
|
77
|
+
const validDuration = creditConfig?.valid_duration_value;
|
|
78
|
+
const validDurationUnit = creditConfig?.valid_duration_unit || 'days';
|
|
79
|
+
const scheduleConfig = creditConfig?.schedule;
|
|
80
|
+
const hasSchedule =
|
|
81
|
+
scheduleConfig?.enabled && scheduleConfig?.delivery_mode && scheduleConfig.delivery_mode !== 'invoice';
|
|
82
|
+
const hasExpiry = validDuration && validDuration > 0;
|
|
83
|
+
|
|
84
|
+
// Step size: each +/- changes by one pack's worth of credits
|
|
85
|
+
const step = Math.max(creditAmount, 1);
|
|
86
|
+
|
|
87
|
+
// Adjustable quantity config
|
|
88
|
+
const adjustableQty = item?.adjustable_quantity;
|
|
89
|
+
const canAdjust = adjustableQty?.enabled !== false;
|
|
90
|
+
const minQuantity = Math.max(adjustableQty?.minimum || 1, 1);
|
|
91
|
+
const quantityAvailable = Math.min(
|
|
92
|
+
activePrice?.quantity_limit_per_checkout ?? Infinity,
|
|
93
|
+
activePrice?.quantity_available ?? Infinity
|
|
94
|
+
);
|
|
95
|
+
const maxQuantity = quantityAvailable
|
|
96
|
+
? Math.min(adjustableQty?.maximum || Infinity, quantityAvailable)
|
|
97
|
+
: adjustableQty?.maximum || Infinity;
|
|
98
|
+
const { data: pendingAmount } = useRequest(
|
|
99
|
+
async () => {
|
|
100
|
+
// Wait for credit currency decimal before fetching — calculations depend on correct decimal
|
|
101
|
+
if (!creditConfig || !userDid || !creditCurrencyId || creditCurrencyDecimal == null) return null;
|
|
102
|
+
try {
|
|
103
|
+
const { data } = await api.get('/api/meter-events/pending-amount', {
|
|
104
|
+
params: { customer_id: userDid, currency_id: creditCurrencyId },
|
|
105
|
+
});
|
|
106
|
+
return data?.[creditCurrencyId];
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{ refreshDeps: [creditConfig, userDid, creditCurrencyId, creditCurrencyDecimal] }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Min quantity needed to cover pending
|
|
115
|
+
const minQtyForPending = useMemo(() => {
|
|
116
|
+
if (!pendingAmount || !creditAmount || creditAmount <= 0) return null;
|
|
117
|
+
const pendingBN = new BN(pendingAmount || '0');
|
|
118
|
+
if (!pendingBN.gt(new BN(0))) return null;
|
|
119
|
+
// creditCurrencyDecimal is guaranteed non-null here (pending fetch depends on it)
|
|
120
|
+
const creditBN = fromTokenToUnit(creditAmount, creditCurrencyDecimal!);
|
|
121
|
+
if (!creditBN || creditBN.isZero()) return null;
|
|
122
|
+
return Math.ceil(pendingBN.mul(new BN(100)).div(creditBN).toNumber() / 100);
|
|
123
|
+
}, [pendingAmount, creditAmount, creditCurrencyDecimal]);
|
|
124
|
+
|
|
125
|
+
// Credit info description (referencing V1 formatCreditInfo)
|
|
126
|
+
const hasPendingAmount = pendingAmount && new BN(pendingAmount || '0').gt(new BN(0));
|
|
127
|
+
const pendingDisplayAmount = useMemo(() => {
|
|
128
|
+
if (!hasPendingAmount) return '';
|
|
129
|
+
// creditCurrencyDecimal is guaranteed non-null here (pending fetch depends on it)
|
|
130
|
+
return formatCreditForCheckout(formatBNStr(pendingAmount!, creditCurrencyDecimal!), creditCurrencySymbol, locale);
|
|
131
|
+
}, [hasPendingAmount, pendingAmount, creditCurrencyDecimal, creditCurrencySymbol, locale]);
|
|
132
|
+
|
|
133
|
+
// Min credits needed to cover pending (for user-facing display)
|
|
134
|
+
const minCreditsForPending = minQtyForPending ? minQtyForPending * step : 0;
|
|
135
|
+
const minCreditsForPendingFormatted = minCreditsForPending
|
|
136
|
+
? formatCreditForCheckout(formatNumber(minCreditsForPending), creditCurrencySymbol, locale)
|
|
137
|
+
: '';
|
|
138
|
+
|
|
139
|
+
// Credit info text: schedule or product description (no fallback "Purchase X")
|
|
140
|
+
const creditInfoText = useMemo(() => {
|
|
141
|
+
if (hasSchedule && scheduleConfig) {
|
|
142
|
+
const intervalUnit = scheduleConfig.interval_unit;
|
|
143
|
+
const intervalValue = scheduleConfig.interval_value;
|
|
144
|
+
let amountPerGrant: number;
|
|
145
|
+
if (scheduleConfig.amount_per_grant) {
|
|
146
|
+
amountPerGrant = Number(scheduleConfig.amount_per_grant);
|
|
147
|
+
} else {
|
|
148
|
+
amountPerGrant = creditAmount;
|
|
149
|
+
}
|
|
150
|
+
const formattedAmount = formatCreditAmount(formatNumber(amountPerGrant), currencySymbol);
|
|
151
|
+
const intervalDisplay =
|
|
152
|
+
intervalValue === 1 ? t(`common.${intervalUnit}`) : `${intervalValue} ${t(`common.${intervalUnit}s` as any)}`;
|
|
153
|
+
return scheduleConfig.expire_with_next_grant
|
|
154
|
+
? t('payment.checkout.credit.schedule.withRefresh', { amount: formattedAmount, interval: intervalDisplay })
|
|
155
|
+
: t('payment.checkout.credit.schedule.periodic', { amount: formattedAmount, interval: intervalDisplay });
|
|
156
|
+
}
|
|
157
|
+
return '';
|
|
158
|
+
}, [creditAmount, currencySymbol, hasSchedule, scheduleConfig, t]);
|
|
159
|
+
|
|
160
|
+
// Validity text: "Credits are valid for X days after purchase."
|
|
161
|
+
const validityText = useMemo(() => {
|
|
162
|
+
if (!hasExpiry) return '';
|
|
163
|
+
return t('payment.checkout.creditTopup.validFor', {
|
|
164
|
+
duration: validDuration,
|
|
165
|
+
unit: t(`common.${validDurationUnit}`),
|
|
166
|
+
});
|
|
167
|
+
}, [hasExpiry, validDuration, validDurationUnit, t]);
|
|
168
|
+
|
|
169
|
+
const hasSubtitle = !!creditInfoText;
|
|
170
|
+
|
|
171
|
+
// ── State: user inputs desired credits, we compute packs ──
|
|
172
|
+
const currentQty = item?.quantity || 1;
|
|
173
|
+
const [localQty, setLocalQty] = useState(currentQty); // pack quantity (source of truth for API)
|
|
174
|
+
const [desiredCredits, setDesiredCredits] = useState(currentQty * step); // what user sees/types
|
|
175
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
176
|
+
const [editValue, setEditValue] = useState('');
|
|
177
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
178
|
+
|
|
179
|
+
// Sync when item quantity changes from backend (skip if locked or already completed)
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (configLocked || session?.status === 'complete') return;
|
|
182
|
+
if (item?.quantity && item.quantity !== localQty) {
|
|
183
|
+
setLocalQty(item.quantity);
|
|
184
|
+
setDesiredCredits(item.quantity * step);
|
|
185
|
+
}
|
|
186
|
+
}, [item?.quantity]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
187
|
+
|
|
188
|
+
// Enforce min quantity for pending — only once on initial load.
|
|
189
|
+
// After user confirms quantity (e.g. clicks "Connect and Pay"), don't override their choice.
|
|
190
|
+
const pendingEnforcedRef = useRef(false);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (configLocked || session?.status === 'complete') return;
|
|
193
|
+
if (pendingEnforcedRef.current) return;
|
|
194
|
+
if (minQtyForPending && minQtyForPending > localQty) {
|
|
195
|
+
pendingEnforcedRef.current = true;
|
|
196
|
+
const newQty = Math.min(Math.max(minQtyForPending, minQuantity), maxQuantity);
|
|
197
|
+
setLocalQty(newQty);
|
|
198
|
+
setDesiredCredits(newQty * step);
|
|
199
|
+
if (item) lineItems.updateQuantity(item.price_id, newQty);
|
|
200
|
+
}
|
|
201
|
+
}, [minQtyForPending]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
202
|
+
|
|
203
|
+
// Max credits = maxQuantity packs * credits per pack
|
|
204
|
+
const maxCredits = maxQuantity * step;
|
|
205
|
+
const minCredits = minQuantity * step;
|
|
206
|
+
|
|
207
|
+
// Commit desired credits → compute packs → update API
|
|
208
|
+
const commitCredits = useCallback(
|
|
209
|
+
(credits: number) => {
|
|
210
|
+
if (credits <= 0) return;
|
|
211
|
+
// Clamp credits to valid range
|
|
212
|
+
const clamped = Math.max(minCredits, Math.min(maxCredits, credits));
|
|
213
|
+
const packs = Math.ceil(clamped / step);
|
|
214
|
+
const clampedPacks = Math.max(minQuantity, Math.min(maxQuantity, packs));
|
|
215
|
+
setLocalQty(clampedPacks);
|
|
216
|
+
setDesiredCredits(clamped);
|
|
217
|
+
if (item) lineItems.updateQuantity(item.price_id, clampedPacks);
|
|
218
|
+
},
|
|
219
|
+
[step, minQuantity, maxQuantity, minCredits, maxCredits, item, lineItems]
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// +/- buttons: step by one pack's worth of credits
|
|
223
|
+
const handleStep = useCallback(
|
|
224
|
+
(delta: number) => {
|
|
225
|
+
const newCredits = desiredCredits + delta * step;
|
|
226
|
+
if (newCredits < step * minQuantity || Math.ceil(newCredits / step) > maxQuantity) return;
|
|
227
|
+
commitCredits(newCredits);
|
|
228
|
+
},
|
|
229
|
+
[desiredCredits, step, minQuantity, maxQuantity, commitCredits]
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Actual credits user will receive (always >= desired, rounded up to whole packs)
|
|
233
|
+
const actualPacks = Math.max(minQuantity, Math.min(maxQuantity, Math.ceil(desiredCredits / step)));
|
|
234
|
+
const actualCredits = actualPacks * step;
|
|
235
|
+
const actualCreditsFormatted = formatNumber(actualCredits, 6, true, true);
|
|
236
|
+
const desiredCreditsFormatted = formatNumber(desiredCredits, 6, true, true);
|
|
237
|
+
const showReceiveSection = canAdjust && step > 1;
|
|
238
|
+
|
|
239
|
+
// Pre-compute pending message to avoid deep ternary in JSX
|
|
240
|
+
const pendingMessage = useMemo(() => {
|
|
241
|
+
if (!hasPendingAmount) return '';
|
|
242
|
+
if (actualCredits >= minCreditsForPending) {
|
|
243
|
+
return t('payment.checkout.creditTopup.pendingEnough', {
|
|
244
|
+
pendingAmount: pendingDisplayAmount,
|
|
245
|
+
availableAmount: formatCreditForCheckout(
|
|
246
|
+
formatNumber(actualCredits - minCreditsForPending),
|
|
247
|
+
creditCurrencySymbol,
|
|
248
|
+
locale
|
|
249
|
+
),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return t('payment.checkout.creditTopup.pendingWarning', {
|
|
253
|
+
pendingAmount: pendingDisplayAmount,
|
|
254
|
+
minCredits: minCreditsForPendingFormatted,
|
|
255
|
+
});
|
|
256
|
+
}, [
|
|
257
|
+
hasPendingAmount,
|
|
258
|
+
actualCredits,
|
|
259
|
+
minCreditsForPending,
|
|
260
|
+
pendingDisplayAmount,
|
|
261
|
+
creditCurrencySymbol,
|
|
262
|
+
locale,
|
|
263
|
+
minCreditsForPendingFormatted,
|
|
264
|
+
t,
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
// Editing handlers — user types desired credit amount
|
|
268
|
+
const startEditing = useCallback(() => {
|
|
269
|
+
if (!canAdjust) return;
|
|
270
|
+
setEditValue(String(desiredCredits));
|
|
271
|
+
setIsEditing(true);
|
|
272
|
+
setTimeout(() => inputRef.current?.select(), 0);
|
|
273
|
+
}, [canAdjust, desiredCredits]);
|
|
274
|
+
|
|
275
|
+
const commitEdit = useCallback(() => {
|
|
276
|
+
setIsEditing(false);
|
|
277
|
+
const parsed = parseInt(editValue.replace(/,/g, ''), 10);
|
|
278
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return;
|
|
279
|
+
commitCredits(parsed);
|
|
280
|
+
}, [editValue, commitCredits]);
|
|
281
|
+
|
|
282
|
+
const handleEditKeyDown = useCallback(
|
|
283
|
+
(e: React.KeyboardEvent) => {
|
|
284
|
+
if (e.key === 'Enter') commitEdit();
|
|
285
|
+
else if (e.key === 'Escape') setIsEditing(false);
|
|
286
|
+
},
|
|
287
|
+
[commitEdit]
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ── Shared styles ──
|
|
291
|
+
const numberHeight = { xs: 48, md: 72 };
|
|
292
|
+
const numberFontSx = {
|
|
293
|
+
fontSize: numberHeight,
|
|
294
|
+
fontWeight: 800,
|
|
295
|
+
lineHeight: 1,
|
|
296
|
+
letterSpacing: '-0.03em',
|
|
297
|
+
};
|
|
298
|
+
const circleBtnSx = {
|
|
299
|
+
width: { xs: 40, md: 56 },
|
|
300
|
+
height: { xs: 40, md: 56 },
|
|
301
|
+
borderRadius: '50%',
|
|
302
|
+
bgcolor: 'background.paper',
|
|
303
|
+
border: '1px solid',
|
|
304
|
+
borderColor: (theme: any) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.12)' : 'divider'),
|
|
305
|
+
color: 'text.secondary',
|
|
306
|
+
transition: 'all 0.2s ease',
|
|
307
|
+
'&:hover': {
|
|
308
|
+
borderColor: 'primary.main',
|
|
309
|
+
color: 'primary.main',
|
|
310
|
+
bgcolor: 'background.paper',
|
|
311
|
+
},
|
|
312
|
+
'&.Mui-disabled': {
|
|
313
|
+
borderColor: (theme: any) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'),
|
|
314
|
+
color: (theme: any) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.12)'),
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<Box
|
|
320
|
+
sx={{
|
|
321
|
+
display: 'flex',
|
|
322
|
+
flexDirection: 'column',
|
|
323
|
+
height: '100%',
|
|
324
|
+
}}>
|
|
325
|
+
{/* Main content — vertically centered */}
|
|
326
|
+
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
|
327
|
+
{/* Badge */}
|
|
328
|
+
<ScenarioBadge livemode={livemode} label={tSafe(t, 'payment.checkout.typeBadge.topup', 'TOP-UP')} />
|
|
329
|
+
|
|
330
|
+
{/* Title: Get Credits */}
|
|
331
|
+
<Typography
|
|
332
|
+
sx={{
|
|
333
|
+
fontWeight: 800,
|
|
334
|
+
fontSize: { xs: 28, md: 48 },
|
|
335
|
+
lineHeight: 1.1,
|
|
336
|
+
letterSpacing: '-0.03em',
|
|
337
|
+
color: 'text.primary',
|
|
338
|
+
mb: { xs: 0.5, md: 1 },
|
|
339
|
+
}}>
|
|
340
|
+
{t('payment.checkout.creditTopup.title', { name: creditName })}
|
|
341
|
+
</Typography>
|
|
342
|
+
|
|
343
|
+
{/* Credit info (schedule/description) */}
|
|
344
|
+
{hasSubtitle && (
|
|
345
|
+
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: { xs: 1.5, md: 2.5 } }}>
|
|
346
|
+
<Typography
|
|
347
|
+
sx={{ fontSize: { xs: 13, md: 15 }, fontWeight: 500, color: 'text.secondary', lineHeight: 1.5 }}>
|
|
348
|
+
{creditInfoText}
|
|
349
|
+
</Typography>
|
|
350
|
+
</Box>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{/* Pending/overdue warning */}
|
|
354
|
+
{hasPendingAmount && (
|
|
355
|
+
<Stack
|
|
356
|
+
direction="row"
|
|
357
|
+
alignItems="flex-start"
|
|
358
|
+
spacing={1.5}
|
|
359
|
+
sx={{
|
|
360
|
+
mb: { xs: 1.5, md: 2.5 },
|
|
361
|
+
p: { xs: 1.5, md: 2 },
|
|
362
|
+
borderRadius: '12px',
|
|
363
|
+
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,152,0,0.12)' : 'rgba(255,152,0,0.08)'),
|
|
364
|
+
border: '1px solid',
|
|
365
|
+
borderColor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,152,0,0.25)' : 'rgba(255,152,0,0.2)'),
|
|
366
|
+
}}>
|
|
367
|
+
<WarningAmberIcon sx={{ fontSize: 18, color: 'warning.main', mt: 0.25, flexShrink: 0 }} />
|
|
368
|
+
<Typography sx={{ fontSize: { xs: 12, md: 13 }, fontWeight: 600, lineHeight: 1.5, color: 'text.primary' }}>
|
|
369
|
+
{pendingMessage}
|
|
370
|
+
</Typography>
|
|
371
|
+
</Stack>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{canAdjust ? (
|
|
375
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: 480 }}>
|
|
376
|
+
{/* Question label — spaced away from subtitle, close to input */}
|
|
377
|
+
<Typography
|
|
378
|
+
sx={{
|
|
379
|
+
fontSize: { xs: 14, md: 17 },
|
|
380
|
+
fontWeight: 700,
|
|
381
|
+
color: 'text.secondary',
|
|
382
|
+
mt: { xs: 3, md: 5 },
|
|
383
|
+
mb: { xs: 2, md: 3 },
|
|
384
|
+
}}>
|
|
385
|
+
{t('payment.checkout.creditTopup.question', { symbol: currencySymbol })}
|
|
386
|
+
</Typography>
|
|
387
|
+
|
|
388
|
+
{/* ── Credit input with +/- ── */}
|
|
389
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1.5, md: 2 } }}>
|
|
390
|
+
<IconButton onClick={() => handleStep(-1)} disabled={actualPacks <= minQuantity} sx={circleBtnSx}>
|
|
391
|
+
<Box component="span" sx={{ fontSize: { xs: 22, md: 28 }, fontWeight: 300, lineHeight: 1, mt: '-1px' }}>
|
|
392
|
+
−
|
|
393
|
+
</Box>
|
|
394
|
+
</IconButton>
|
|
395
|
+
|
|
396
|
+
<Box
|
|
397
|
+
sx={{ flex: 1, minWidth: 0, textAlign: 'center', cursor: canAdjust ? 'text' : 'default' }}
|
|
398
|
+
onClick={startEditing}>
|
|
399
|
+
<Box
|
|
400
|
+
sx={{
|
|
401
|
+
height: numberHeight,
|
|
402
|
+
display: 'flex',
|
|
403
|
+
alignItems: 'center',
|
|
404
|
+
justifyContent: 'center',
|
|
405
|
+
}}>
|
|
406
|
+
{isEditing ? (
|
|
407
|
+
<InputBase
|
|
408
|
+
inputRef={inputRef}
|
|
409
|
+
value={editValue}
|
|
410
|
+
onChange={(e) => setEditValue(e.target.value.replace(/\D/g, ''))}
|
|
411
|
+
onBlur={commitEdit}
|
|
412
|
+
onKeyDown={handleEditKeyDown}
|
|
413
|
+
autoFocus
|
|
414
|
+
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
|
|
415
|
+
sx={{
|
|
416
|
+
width: '100%',
|
|
417
|
+
bgcolor: 'transparent',
|
|
418
|
+
'&.Mui-focused': { bgcolor: 'transparent' },
|
|
419
|
+
'& input': {
|
|
420
|
+
textAlign: 'center',
|
|
421
|
+
...numberFontSx,
|
|
422
|
+
p: 0,
|
|
423
|
+
fontFamily: 'inherit',
|
|
424
|
+
bgcolor: 'transparent',
|
|
425
|
+
'&:focus': { bgcolor: 'transparent' },
|
|
426
|
+
},
|
|
427
|
+
}}
|
|
428
|
+
/>
|
|
429
|
+
) : (
|
|
430
|
+
<Typography sx={{ ...numberFontSx, color: 'text.primary', userSelect: 'none' }}>
|
|
431
|
+
{desiredCreditsFormatted}
|
|
432
|
+
</Typography>
|
|
433
|
+
)}
|
|
434
|
+
</Box>
|
|
435
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.75, mt: 1 }}>
|
|
436
|
+
<Typography
|
|
437
|
+
sx={{
|
|
438
|
+
fontSize: { xs: 12, md: 14 },
|
|
439
|
+
fontWeight: 700,
|
|
440
|
+
color: 'text.secondary',
|
|
441
|
+
textTransform: 'uppercase',
|
|
442
|
+
letterSpacing: '0.05em',
|
|
443
|
+
}}>
|
|
444
|
+
{currencySymbol}
|
|
445
|
+
</Typography>
|
|
446
|
+
</Box>
|
|
447
|
+
</Box>
|
|
448
|
+
|
|
449
|
+
<IconButton onClick={() => handleStep(1)} disabled={actualPacks >= maxQuantity} sx={circleBtnSx}>
|
|
450
|
+
<Box component="span" sx={{ fontSize: { xs: 22, md: 28 }, fontWeight: 300, lineHeight: 1 }}>
|
|
451
|
+
+
|
|
452
|
+
</Box>
|
|
453
|
+
</IconButton>
|
|
454
|
+
</Box>
|
|
455
|
+
|
|
456
|
+
{/* Increment hint */}
|
|
457
|
+
{step > 1 && (
|
|
458
|
+
<Typography
|
|
459
|
+
sx={{
|
|
460
|
+
fontSize: 11,
|
|
461
|
+
fontWeight: 600,
|
|
462
|
+
color: 'grey.500',
|
|
463
|
+
textTransform: 'uppercase',
|
|
464
|
+
letterSpacing: '0.08em',
|
|
465
|
+
textAlign: 'center',
|
|
466
|
+
mt: { xs: 1.5, md: 2 },
|
|
467
|
+
}}>
|
|
468
|
+
{t('payment.checkout.creditTopup.increment', {
|
|
469
|
+
step: formatNumber(step, 6, true, true),
|
|
470
|
+
symbol: currencySymbol,
|
|
471
|
+
})}
|
|
472
|
+
</Typography>
|
|
473
|
+
)}
|
|
474
|
+
|
|
475
|
+
{/* ── "You'll receive" result card — frosted glass ── */}
|
|
476
|
+
{showReceiveSection && (
|
|
477
|
+
<Box
|
|
478
|
+
sx={{
|
|
479
|
+
display: 'flex',
|
|
480
|
+
flexDirection: 'column',
|
|
481
|
+
mt: { xs: 3, md: 4 },
|
|
482
|
+
borderRadius: '20px',
|
|
483
|
+
backdropFilter: 'blur(12px)',
|
|
484
|
+
bgcolor: (theme) =>
|
|
485
|
+
theme.palette.mode === 'dark' ? 'rgba(59,130,246,0.08)' : 'rgba(59,130,246,0.04)',
|
|
486
|
+
border: '1px solid',
|
|
487
|
+
borderColor: (theme) =>
|
|
488
|
+
theme.palette.mode === 'dark' ? 'rgba(59,130,246,0.18)' : 'rgba(59,130,246,0.10)',
|
|
489
|
+
boxShadow: (theme) =>
|
|
490
|
+
theme.palette.mode === 'dark'
|
|
491
|
+
? '0 8px 32px rgba(59,130,246,0.06)'
|
|
492
|
+
: '0 8px 32px rgba(59,130,246,0.04)',
|
|
493
|
+
}}>
|
|
494
|
+
{/* Main content */}
|
|
495
|
+
<Box sx={{ p: { xs: 2.5, md: 3.5 } }}>
|
|
496
|
+
<Typography
|
|
497
|
+
sx={{
|
|
498
|
+
fontSize: { xs: 16, md: 20 },
|
|
499
|
+
fontWeight: 700,
|
|
500
|
+
color: 'primary.main',
|
|
501
|
+
mb: 0.5,
|
|
502
|
+
}}>
|
|
503
|
+
{t('payment.checkout.creditTopup.willReceive')}
|
|
504
|
+
{': '}
|
|
505
|
+
<Box component="span" sx={{ fontWeight: 800 }}>
|
|
506
|
+
{formatCreditForCheckout(actualCreditsFormatted, currencySymbol, locale)}
|
|
507
|
+
</Box>
|
|
508
|
+
</Typography>
|
|
509
|
+
{/* Pack info line */}
|
|
510
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.5 }}>
|
|
511
|
+
<Inventory2OutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
512
|
+
<Typography sx={{ fontSize: { xs: 12, md: 14 }, fontWeight: 600, color: 'text.secondary' }}>
|
|
513
|
+
{t('payment.checkout.creditTopup.packInfo', {
|
|
514
|
+
packs: actualPacks,
|
|
515
|
+
perPack: formatNumber(step, 6, true, true),
|
|
516
|
+
})}
|
|
517
|
+
</Typography>
|
|
518
|
+
</Box>
|
|
519
|
+
{/* Auto match hint */}
|
|
520
|
+
<Typography
|
|
521
|
+
sx={{
|
|
522
|
+
fontSize: { xs: 12, md: 13 },
|
|
523
|
+
fontWeight: 500,
|
|
524
|
+
color: 'grey.500',
|
|
525
|
+
}}>
|
|
526
|
+
{t('payment.checkout.creditTopup.autoMatch')}
|
|
527
|
+
</Typography>
|
|
528
|
+
</Box>
|
|
529
|
+
{/* Validity footer */}
|
|
530
|
+
{validityText && (
|
|
531
|
+
<Box
|
|
532
|
+
sx={{
|
|
533
|
+
display: 'flex',
|
|
534
|
+
alignItems: 'center',
|
|
535
|
+
gap: 0.75,
|
|
536
|
+
px: { xs: 2.5, md: 3.5 },
|
|
537
|
+
py: { xs: 1.5, md: 2 },
|
|
538
|
+
borderTop: '1px solid',
|
|
539
|
+
borderColor: (theme) =>
|
|
540
|
+
theme.palette.mode === 'dark' ? 'rgba(59,130,246,0.15)' : 'rgba(59,130,246,0.08)',
|
|
541
|
+
}}>
|
|
542
|
+
<AccessTimeOutlinedIcon sx={{ fontSize: 14, color: 'grey.500' }} />
|
|
543
|
+
<Typography
|
|
544
|
+
sx={{
|
|
545
|
+
fontSize: { xs: 12, md: 13 },
|
|
546
|
+
fontWeight: 500,
|
|
547
|
+
color: 'grey.500',
|
|
548
|
+
}}>
|
|
549
|
+
{validityText}
|
|
550
|
+
</Typography>
|
|
551
|
+
</Box>
|
|
552
|
+
)}
|
|
553
|
+
</Box>
|
|
554
|
+
)}
|
|
555
|
+
|
|
556
|
+
{/* Validity hint — shown below input when no receive box */}
|
|
557
|
+
{!showReceiveSection && validityText && (
|
|
558
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: { xs: 2.5, md: 3.5 } }}>
|
|
559
|
+
<InfoOutlinedIcon sx={{ fontSize: 14, color: 'grey.500' }} />
|
|
560
|
+
<Typography
|
|
561
|
+
sx={{
|
|
562
|
+
fontSize: { xs: 12, md: 13 },
|
|
563
|
+
fontWeight: 500,
|
|
564
|
+
color: 'grey.500',
|
|
565
|
+
}}>
|
|
566
|
+
{validityText}
|
|
567
|
+
</Typography>
|
|
568
|
+
</Box>
|
|
569
|
+
)}
|
|
570
|
+
</Box>
|
|
571
|
+
) : (
|
|
572
|
+
// ── Non-adjustable: show fixed credit amount in a styled card ──
|
|
573
|
+
<Box
|
|
574
|
+
sx={{
|
|
575
|
+
display: 'flex',
|
|
576
|
+
flexDirection: 'column',
|
|
577
|
+
mt: { xs: 2, md: 3 },
|
|
578
|
+
borderRadius: '20px',
|
|
579
|
+
backdropFilter: 'blur(12px)',
|
|
580
|
+
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(59,130,246,0.08)' : 'rgba(59,130,246,0.04)'),
|
|
581
|
+
border: '1px solid',
|
|
582
|
+
borderColor: (theme) =>
|
|
583
|
+
theme.palette.mode === 'dark' ? 'rgba(59,130,246,0.18)' : 'rgba(59,130,246,0.10)',
|
|
584
|
+
boxShadow: (theme) =>
|
|
585
|
+
theme.palette.mode === 'dark' ? '0 8px 32px rgba(59,130,246,0.06)' : '0 8px 32px rgba(59,130,246,0.04)',
|
|
586
|
+
}}>
|
|
587
|
+
<Box sx={{ p: { xs: 2.5, md: 3.5 } }}>
|
|
588
|
+
<Typography
|
|
589
|
+
sx={{
|
|
590
|
+
fontSize: { xs: 13, md: 14 },
|
|
591
|
+
fontWeight: 600,
|
|
592
|
+
color: 'text.secondary',
|
|
593
|
+
mb: 1,
|
|
594
|
+
}}>
|
|
595
|
+
{t('payment.checkout.creditTopup.willReceive')}
|
|
596
|
+
</Typography>
|
|
597
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
598
|
+
<Typography
|
|
599
|
+
sx={{
|
|
600
|
+
fontSize: { xs: 32, md: 48 },
|
|
601
|
+
fontWeight: 800,
|
|
602
|
+
lineHeight: 1,
|
|
603
|
+
letterSpacing: '-0.03em',
|
|
604
|
+
color: 'primary.main',
|
|
605
|
+
}}>
|
|
606
|
+
{formatCreditForCheckout(actualCreditsFormatted, currencySymbol, locale)}
|
|
607
|
+
</Typography>
|
|
608
|
+
</Box>
|
|
609
|
+
</Box>
|
|
610
|
+
{/* Validity footer */}
|
|
611
|
+
{validityText && (
|
|
612
|
+
<Box
|
|
613
|
+
sx={{
|
|
614
|
+
display: 'flex',
|
|
615
|
+
alignItems: 'center',
|
|
616
|
+
gap: 0.75,
|
|
617
|
+
px: { xs: 2.5, md: 3.5 },
|
|
618
|
+
py: { xs: 1.5, md: 2 },
|
|
619
|
+
borderTop: '1px solid',
|
|
620
|
+
borderColor: (theme) =>
|
|
621
|
+
theme.palette.mode === 'dark' ? 'rgba(59,130,246,0.15)' : 'rgba(59,130,246,0.08)',
|
|
622
|
+
}}>
|
|
623
|
+
<AccessTimeOutlinedIcon sx={{ fontSize: 14, color: 'grey.500' }} />
|
|
624
|
+
<Typography
|
|
625
|
+
sx={{
|
|
626
|
+
fontSize: { xs: 12, md: 13 },
|
|
627
|
+
fontWeight: 500,
|
|
628
|
+
color: 'grey.500',
|
|
629
|
+
}}>
|
|
630
|
+
{validityText}
|
|
631
|
+
</Typography>
|
|
632
|
+
</Box>
|
|
633
|
+
)}
|
|
634
|
+
</Box>
|
|
635
|
+
)}
|
|
636
|
+
</Box>
|
|
637
|
+
{/* End main content */}
|
|
638
|
+
|
|
639
|
+
{/* Exchange rate — shown for all dynamic pricing scenarios */}
|
|
640
|
+
<Box sx={{ flexShrink: 0 }}>
|
|
641
|
+
{rate.hasDynamicPricing && rate.status === 'unavailable' && !isStripe && (
|
|
642
|
+
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mb: 2 }}>
|
|
643
|
+
<Typography sx={{ fontSize: 13, color: 'text.secondary', fontWeight: 500 }}>
|
|
644
|
+
{t('payment.dynamicPricing.unavailable.title')}
|
|
645
|
+
</Typography>
|
|
646
|
+
<Typography
|
|
647
|
+
component="span"
|
|
648
|
+
onClick={rate.refresh}
|
|
649
|
+
sx={{
|
|
650
|
+
fontSize: 13,
|
|
651
|
+
color: 'primary.main',
|
|
652
|
+
fontWeight: 600,
|
|
653
|
+
cursor: 'pointer',
|
|
654
|
+
'&:hover': { textDecoration: 'underline' },
|
|
655
|
+
}}>
|
|
656
|
+
{t('payment.dynamicPricing.unavailable.retry')}
|
|
657
|
+
</Typography>
|
|
658
|
+
</Stack>
|
|
659
|
+
)}
|
|
660
|
+
<ExchangeRateFooter
|
|
661
|
+
hasDynamicPricing={rate.hasDynamicPricing}
|
|
662
|
+
rate={{
|
|
663
|
+
value: rate.value,
|
|
664
|
+
display: rate.display,
|
|
665
|
+
provider: rate.provider,
|
|
666
|
+
providerDisplay: rate.providerDisplay,
|
|
667
|
+
fetchedAt: rate.fetchedAt,
|
|
668
|
+
status: rate.status,
|
|
669
|
+
}}
|
|
670
|
+
slippage={{ percent: slippage.percent, set: slippage.set }}
|
|
671
|
+
currencySymbol={currency?.symbol || ''}
|
|
672
|
+
isSubscription={['subscription', 'setup'].includes(session?.mode || 'payment')}
|
|
673
|
+
/>
|
|
674
|
+
</Box>
|
|
675
|
+
</Box>
|
|
676
|
+
);
|
|
677
|
+
}
|