From 4481c6b71a8b9041eafdf6c0070f6656064546e8 Mon Sep 17 00:00:00 2001 From: counterweight Date: Tue, 23 Dec 2025 14:50:22 +0100 Subject: [PATCH] Add payment method selector to exchange form with threshold enforcement --- frontend/app/exchange/page.tsx | 131 ++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/frontend/app/exchange/page.tsx b/frontend/app/exchange/page.tsx index 1b1fe6e..8dd3eea 100644 --- a/frontend/app/exchange/page.tsx +++ b/frontend/app/exchange/page.tsx @@ -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("buy"); + const [bitcoinTransferMethod, setBitcoinTransferMethod] = + useState("onchain"); const [eurAmount, setEurAmount] = useState(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("/api/exchange", { slot_start: selectedSlot.start_time, direction, + bitcoin_transfer_method: bitcoinTransferMethod, eur_amount: eurAmount, }); @@ -356,6 +376,43 @@ export default function ExchangePage() { + {/* Payment Method Selector */} +
+
+ Payment Method * +
+
+ + +
+ {isLightningDisabled && ( +
+ Lightning payments are only available for amounts up to €{LIGHTNING_MAX_EUR} +
+ )} +
+ {/* Amount Section */}
@@ -445,6 +502,11 @@ export default function ExchangePage() { + + + {direction === "buy" ? "Receive via " : "Send via "} + {bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"} +
@@ -559,6 +621,13 @@ export default function ExchangePage() { Rate: {formatPrice(agreedPrice)}/BTC +
+ Payment: + + {direction === "buy" ? "Receive via " : "Send via "} + {bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"} + +
@@ -869,6 +938,66 @@ const styles: Record = { 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", },