Add payment method selector to exchange form with threshold enforcement

This commit is contained in:
counterweight 2025-12-23 14:50:22 +01:00
parent cb173a7442
commit 4481c6b71a
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C

View file

@ -19,9 +19,14 @@ type BookableSlot = components["schemas"]["BookableSlot"];
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
// Constants from shared config
const { minAdvanceDays: MIN_ADVANCE_DAYS, maxAdvanceDays: MAX_ADVANCE_DAYS } = constants.exchange;
const {
minAdvanceDays: MIN_ADVANCE_DAYS,
maxAdvanceDays: MAX_ADVANCE_DAYS,
lightningMaxEur: LIGHTNING_MAX_EUR,
} = constants.exchange;
type Direction = "buy" | "sell";
type BitcoinTransferMethod = "onchain" | "lightning";
type WizardStep = "details" | "booking";
/**
@ -49,6 +54,8 @@ export default function ExchangePage() {
// Trade form state
const [direction, setDirection] = useState<Direction>("buy");
const [bitcoinTransferMethod, setBitcoinTransferMethod] =
useState<BitcoinTransferMethod>("onchain");
const [eurAmount, setEurAmount] = useState<number>(10000); // €100 in cents
// Date/slot selection state
@ -95,6 +102,18 @@ export default function ExchangePage() {
return Math.floor(btcAmount * 100_000_000);
}, [eurAmount, agreedPrice]);
// Check if Lightning is disabled due to threshold
const isLightningDisabled = useMemo(() => {
return eurAmount > LIGHTNING_MAX_EUR * 100;
}, [eurAmount]);
// Auto-switch to onchain if Lightning becomes disabled
useEffect(() => {
if (isLightningDisabled && bitcoinTransferMethod === "lightning") {
setBitcoinTransferMethod("onchain");
}
}, [isLightningDisabled, bitcoinTransferMethod]);
// Fetch price data
const fetchPrice = useCallback(async () => {
setIsPriceLoading(true);
@ -240,6 +259,7 @@ export default function ExchangePage() {
await api.post<ExchangeResponse>("/api/exchange", {
slot_start: selectedSlot.start_time,
direction,
bitcoin_transfer_method: bitcoinTransferMethod,
eur_amount: eurAmount,
});
@ -356,6 +376,43 @@ export default function ExchangePage() {
</button>
</div>
{/* Payment Method Selector */}
<div style={styles.paymentMethodSection}>
<div style={styles.paymentMethodLabel}>
Payment Method <span style={styles.required}>*</span>
</div>
<div style={styles.paymentMethodRow}>
<button
onClick={() => setBitcoinTransferMethod("onchain")}
disabled={false}
style={{
...styles.paymentMethodBtn,
...(bitcoinTransferMethod === "onchain" ? styles.paymentMethodBtnActive : {}),
}}
>
<span style={styles.paymentMethodIcon}>🔗</span>
<span>Onchain</span>
</button>
<button
onClick={() => setBitcoinTransferMethod("lightning")}
disabled={isLightningDisabled}
style={{
...styles.paymentMethodBtn,
...(bitcoinTransferMethod === "lightning" ? styles.paymentMethodBtnActive : {}),
...(isLightningDisabled ? styles.paymentMethodBtnDisabled : {}),
}}
>
<span style={styles.paymentMethodIcon}></span>
<span>Lightning</span>
</button>
</div>
{isLightningDisabled && (
<div style={styles.thresholdMessage}>
Lightning payments are only available for amounts up to {LIGHTNING_MAX_EUR}
</div>
)}
</div>
{/* Amount Section */}
<div style={styles.amountSection}>
<div style={styles.amountHeader}>
@ -445,6 +502,11 @@ export default function ExchangePage() {
<span style={styles.satsValue}>
<SatsDisplay sats={satsAmount} />
</span>
<span style={styles.summaryDivider}></span>
<span style={styles.summaryPaymentMethod}>
{direction === "buy" ? "Receive via " : "Send via "}
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
</span>
</div>
</div>
@ -559,6 +621,13 @@ export default function ExchangePage() {
<span style={styles.confirmLabel}>Rate:</span>
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
</div>
<div style={styles.confirmRow}>
<span style={styles.confirmLabel}>Payment:</span>
<span style={styles.confirmValue}>
{direction === "buy" ? "Receive via " : "Send via "}
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
</span>
</div>
</div>
<div style={styles.buttonRow}>
@ -869,6 +938,66 @@ const styles: Record<string, CSSProperties> = {
summaryDivider: {
color: "rgba(255, 255, 255, 0.3)",
},
summaryPaymentMethod: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.875rem",
color: "rgba(255, 255, 255, 0.6)",
},
paymentMethodSection: {
marginBottom: "1.5rem",
},
paymentMethodLabel: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.7)",
fontSize: "0.9rem",
marginBottom: "0.75rem",
},
required: {
color: "#f87171",
},
paymentMethodRow: {
display: "flex",
gap: "0.5rem",
},
paymentMethodBtn: {
flex: 1,
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.95rem",
fontWeight: 600,
padding: "0.875rem",
background: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
color: "rgba(255, 255, 255, 0.6)",
cursor: "pointer",
transition: "all 0.2s",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "0.5rem",
},
paymentMethodBtnActive: {
background: "rgba(167, 139, 250, 0.15)",
border: "1px solid #a78bfa",
color: "#a78bfa",
},
paymentMethodBtnDisabled: {
opacity: 0.4,
cursor: "not-allowed",
},
paymentMethodIcon: {
fontSize: "1.2rem",
},
thresholdMessage: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.75rem",
color: "rgba(251, 146, 60, 0.9)",
marginTop: "0.5rem",
padding: "0.5rem",
background: "rgba(251, 146, 60, 0.1)",
borderRadius: "6px",
border: "1px solid rgba(251, 146, 60, 0.2)",
},
section: {
marginBottom: "2rem",
},