Add payment method selector to exchange form with threshold enforcement
This commit is contained in:
parent
cb173a7442
commit
4481c6b71a
1 changed files with 130 additions and 1 deletions
|
|
@ -19,9 +19,14 @@ type BookableSlot = components["schemas"]["BookableSlot"];
|
||||||
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
|
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
|
||||||
|
|
||||||
// Constants from shared config
|
// 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 Direction = "buy" | "sell";
|
||||||
|
type BitcoinTransferMethod = "onchain" | "lightning";
|
||||||
type WizardStep = "details" | "booking";
|
type WizardStep = "details" | "booking";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,6 +54,8 @@ export default function ExchangePage() {
|
||||||
|
|
||||||
// Trade form state
|
// Trade form state
|
||||||
const [direction, setDirection] = useState<Direction>("buy");
|
const [direction, setDirection] = useState<Direction>("buy");
|
||||||
|
const [bitcoinTransferMethod, setBitcoinTransferMethod] =
|
||||||
|
useState<BitcoinTransferMethod>("onchain");
|
||||||
const [eurAmount, setEurAmount] = useState<number>(10000); // €100 in cents
|
const [eurAmount, setEurAmount] = useState<number>(10000); // €100 in cents
|
||||||
|
|
||||||
// Date/slot selection state
|
// Date/slot selection state
|
||||||
|
|
@ -95,6 +102,18 @@ export default function ExchangePage() {
|
||||||
return Math.floor(btcAmount * 100_000_000);
|
return Math.floor(btcAmount * 100_000_000);
|
||||||
}, [eurAmount, agreedPrice]);
|
}, [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
|
// Fetch price data
|
||||||
const fetchPrice = useCallback(async () => {
|
const fetchPrice = useCallback(async () => {
|
||||||
setIsPriceLoading(true);
|
setIsPriceLoading(true);
|
||||||
|
|
@ -240,6 +259,7 @@ export default function ExchangePage() {
|
||||||
await api.post<ExchangeResponse>("/api/exchange", {
|
await api.post<ExchangeResponse>("/api/exchange", {
|
||||||
slot_start: selectedSlot.start_time,
|
slot_start: selectedSlot.start_time,
|
||||||
direction,
|
direction,
|
||||||
|
bitcoin_transfer_method: bitcoinTransferMethod,
|
||||||
eur_amount: eurAmount,
|
eur_amount: eurAmount,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -356,6 +376,43 @@ export default function ExchangePage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Amount Section */}
|
||||||
<div style={styles.amountSection}>
|
<div style={styles.amountSection}>
|
||||||
<div style={styles.amountHeader}>
|
<div style={styles.amountHeader}>
|
||||||
|
|
@ -445,6 +502,11 @@ export default function ExchangePage() {
|
||||||
<span style={styles.satsValue}>
|
<span style={styles.satsValue}>
|
||||||
<SatsDisplay sats={satsAmount} />
|
<SatsDisplay sats={satsAmount} />
|
||||||
</span>
|
</span>
|
||||||
|
<span style={styles.summaryDivider}>•</span>
|
||||||
|
<span style={styles.summaryPaymentMethod}>
|
||||||
|
{direction === "buy" ? "Receive via " : "Send via "}
|
||||||
|
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -559,6 +621,13 @@ export default function ExchangePage() {
|
||||||
<span style={styles.confirmLabel}>Rate:</span>
|
<span style={styles.confirmLabel}>Rate:</span>
|
||||||
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div style={styles.buttonRow}>
|
<div style={styles.buttonRow}>
|
||||||
|
|
@ -869,6 +938,66 @@ const styles: Record<string, CSSProperties> = {
|
||||||
summaryDivider: {
|
summaryDivider: {
|
||||||
color: "rgba(255, 255, 255, 0.3)",
|
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: {
|
section: {
|
||||||
marginBottom: "2rem",
|
marginBottom: "2rem",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue