import React, { useState, useMemo, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { List, ListItem, Alert, Chip, ListItemAvatar, ListItemText, ListItemIcon, Divider, Grid, Collapse, useTheme, Typography, IconButton, Tooltip, ListItemButton, } from '@mui/material'; import Countdown, { type CountdownRenderProps, zeroPad } from 'react-countdown'; import RobotAvatar from '../../components/RobotAvatar'; import currencies from '../../../static/assets/currencies.json'; import { AccessTime, Numbers, PriceChange, Payments, Article, HourglassTop, ExpandLess, ExpandMore, Map, } from '@mui/icons-material'; import { PaymentStringAsIcons } from '../../components/PaymentMethods'; import { FlagWithProps, SendReceiveIcon } from '../Icons'; import LinearDeterminate from './LinearDeterminate'; import type Coordinator from '../../models'; import { statusBadgeColor, pn, amountToString, computeSats } from '../../utils'; import TakeButton from './TakeButton'; import { F2fMapDialog } from '../Dialogs'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext'; import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext'; import { type Order } from '../../models'; interface OrderDetailsProps { shortAlias: string; currentOrder: Order; onClickCoordinator?: () => void; baseUrl: string; onClickGenerateRobot?: () => void; } const OrderDetails = ({ shortAlias, currentOrder, onClickCoordinator = () => null, baseUrl, onClickGenerateRobot = () => null, }: OrderDetailsProps): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); const { hostUrl } = useContext(AppContext); const { federation } = useContext(FederationContext); const { orderUpdatedAt } = useContext(GarageContext); const [coordinator] = useState(federation.getCoordinator(shortAlias)); const currencyCode: string = currencies[(currentOrder?.currency ?? 1).toString()]; const [showSatsDetails, setShowSatsDetails] = useState(false); const [openWorldmap, setOpenWorldmap] = useState(false); const amountString = useMemo(() => { if (currentOrder === null || currentOrder.amount === null) return; if (currentOrder.currency === 1000) { return ( amountToString( (currentOrder.amount * 100000000).toString(), currentOrder.amount > 0 ? false : currentOrder.has_range, currentOrder.min_amount * 100000000, currentOrder.max_amount * 100000000, ) + ' Sats' ); } else { return ( amountToString( currentOrder.amount.toString(), currentOrder.amount > 0 ? false : currentOrder.has_range, currentOrder.min_amount, currentOrder.max_amount, ) + ` ${currencyCode}` ); } }, [orderUpdatedAt]); // Countdown Renderer callback with condition const countdownRenderer = function ({ total, hours, minutes, seconds, completed, }: CountdownRenderProps): JSX.Element { if (completed) { // Render a completed state return {t('The order has expired')}; } else { let color = 'inherit'; const fraction_left = total / 1000 / (currentOrder?.total_secs_exp ?? 1); // Make orange at 25% of time left if (fraction_left < 0.25) { color = theme.palette.warning.main; } // Make red at 10% of time left if (fraction_left < 0.1) { color = theme.palette.error.main; } // Render a countdown, bold when less than 25% return fraction_left < 0.25 ? ( {`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `} ) : ( {`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `} ); } }; const timerRenderer = function (seconds: number): JSX.Element { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds - hours * 3600) / 60); return ( {hours > 0 ? `${hours}h` : ''} {minutes > 0 ? `${zeroPad(minutes)}m` : ''}{' '} ); }; // Countdown Renderer callback with condition const countdownPenaltyRenderer = ({ minutes, seconds, completed, }: { minutes: number; seconds: number; completed: boolean; }): JSX.Element => { if (completed) { // Render a completed state return {t('Penalty lifted, good to go!')}; } else { return ( {' '} {t('You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s', { timeMin: zeroPad(minutes), timeSec: zeroPad(seconds), })}{' '} ); } }; const satsSummary = useMemo(() => { let send: string = ''; let receive: string = ''; let sats: string = ''; const order = currentOrder; if (order === null) return {}; const isBuyer = (order.type === 0 && order.is_maker) || (order.type === 1 && !order.is_maker); const tradeFee = order.is_maker ? coordinator.info?.maker_fee ?? 0 : coordinator.info?.taker_fee ?? 0; const defaultRoutingBudget = 0.001; const btc_now = order.satoshis_now / 100000000; const rate = order.amount > 0 ? order.amount / btc_now : Number(order.max_amount) / btc_now; if (isBuyer) { if (order.amount > 0) { sats = computeSats({ amount: order.amount, fee: -tradeFee, routingBudget: defaultRoutingBudget, rate, }); } else { const min = computeSats({ amount: Number(order.min_amount), fee: -tradeFee, routingBudget: defaultRoutingBudget, rate, }); const max = computeSats({ amount: Number(order.max_amount), fee: -tradeFee, routingBudget: defaultRoutingBudget, rate, }); sats = `${String(min)}-${String(max)}`; } send = t('You send via {{method}} {{amount}}', { amount: amountString, method: order.payment_method, currencyCode, }); receive = t('You receive via Lightning {{amount}} Sats (Approx)', { amount: sats, }); } else { if (order.amount > 0) { sats = computeSats({ amount: order.amount, fee: tradeFee, rate, }); } else { const min = computeSats({ amount: order.min_amount, fee: tradeFee, rate, }); const max = computeSats({ amount: order.max_amount, fee: tradeFee, rate, }); sats = `${String(min)}-${String(max)}`; } send = t('You send via Lightning {{amount}} Sats (Approx)', { amount: sats }); receive = t('You receive via {{method}} {{amount}}', { amount: amountString, method: order.payment_method, }); } return { send, receive }; }, [orderUpdatedAt]); return ( { setOpenWorldmap(false); }} /> { onClickCoordinator(); }} > {' '}
0 ? 'Amount' : 'Amount Range'} /> { setShowSatsDetails(!showSatsDetails); }} > {showSatsDetails ? : }
{satsSummary.send} {satsSummary.receive} } secondary={ currentOrder?.currency === 1000 ? t('Swap destination') : t('Accepted payment methods') } /> {currentOrder?.payment_method.includes('Cash F2F') && (
{ setOpenWorldmap(true); }} >
)}
{/* If there is live Price and Premium data, show it. Otherwise show the order maker settings */} {currentOrder?.price_now !== undefined ? ( ) : null} {currentOrder?.price_now === undefined && currentOrder?.is_explicit ? ( ) : null} {currentOrder?.price_now === undefined && !currentOrder?.is_explicit ? ( ) : null} {/* if order is in a status that does not expire, do not show countdown */} {/* If the user has a penalty/limit */} {currentOrder?.penalty !== undefined ? ( ) : ( <> )} {!currentOrder?.is_participant ? ( ) : ( <> )} ); }; export default OrderDetails;