import React, { Component } from "react"; import { withTranslation, Trans} from "react-i18next"; import {TextField,Chip, Tooltip, Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material" import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown'; import MediaQuery from 'react-responsive' import currencyDict from '../../static/assets/currencies.json'; import PaymentText from './PaymentText' import TradeBox from "./TradeBox"; import getFlags from './getFlags' // icons import AccessTimeIcon from '@mui/icons-material/AccessTime'; import NumbersIcon from '@mui/icons-material/Numbers'; import PriceChangeIcon from '@mui/icons-material/PriceChange'; import PaymentsIcon from '@mui/icons-material/Payments'; import ArticleIcon from '@mui/icons-material/Article'; import { t } from "i18next"; import { getCookie } from "../utils/cookies"; // pretty numbers function pn(x) { var parts = x.toString().split("."); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); return parts.join("."); } class OrderPage extends Component { constructor(props) { super(props); this.state = { is_explicit: false, delay: 60000, // Refresh every 60 seconds by default total_secs_exp: 300, loading: true, openCancel: false, openCollaborativeCancel: false, openInactiveMaker: false, showContractBox: 1, }; this.orderId = this.props.match.params.orderId; this.getOrderDetails(); // Refresh delays according to Order status this.statusToDelay = { "0": 2000, //'Waiting for maker bond' "1": 25000, //'Public' "2": 999999, //'Deleted' "3": 2000, //'Waiting for taker bond' "4": 999999, //'Cancelled' "5": 999999, //'Expired' "6": 6000, //'Waiting for trade collateral and buyer invoice' "7": 8000, //'Waiting only for seller trade collateral' "8": 8000, //'Waiting only for buyer invoice' "9": 10000, //'Sending fiat - In chatroom' "10": 10000, //'Fiat sent - In chatroom' "11": 30000, //'In dispute' "12": 999999, //'Collaboratively cancelled' "13": 3000, //'Sending satoshis to buyer' "14": 999999, //'Sucessful trade' "15": 10000, //'Failed lightning network routing' "16": 180000, //'Wait for dispute resolution' "17": 180000, //'Maker lost dispute' "18": 180000, //'Taker lost dispute' } } completeSetState=(newStateVars)=>{ // In case the reply only has "bad_request" // Do not substitute these two for "undefined" as // otherStateVars will fail to assign values if (newStateVars.currency == null){ newStateVars.currency = this.state.currency newStateVars.status = this.state.status } var otherStateVars = { amount: newStateVars.amount ? newStateVars.amount : null, loading: false, delay: this.setDelay(newStateVars.status), currencyCode: this.getCurrencyCode(newStateVars.currency), penalty: newStateVars.penalty, // in case penalty time has finished, it goes back to null invoice_expired: newStateVars.invoice_expired // in case invoice had expired, it goes back to null when it is valid again }; var completeStateVars = Object.assign({}, newStateVars, otherStateVars); this.setState(completeStateVars); } getOrderDetails() { this.setState(null) fetch('/api/order' + '?order_id=' + this.orderId) .then((response) => response.json()) .then((data) => this.completeSetState(data)); } // These are used to refresh the data componentDidMount() { this.interval = setInterval(this.tick, this.state.delay); } componentDidUpdate() { clearInterval(this.interval); this.interval = setInterval(this.tick, this.state.delay); } componentWillUnmount() { clearInterval(this.interval); } tick = () => { this.getOrderDetails(); } // Countdown Renderer callback with condition countdownRenderer = ({ total, hours, minutes, seconds, completed }) => { const { t } = this.props; if (completed) { // Render a completed state return ( {t("The order has expired")}); } else { var col = 'inherit' var fraction_left = (total/1000) / this.state.total_secs_exp // Make orange at 25% of time left if (fraction_left < 0.25){col = 'orange'} // Make red at 10% of time left if (fraction_left < 0.1){col = 'red'} // 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 ); } }; // Countdown Renderer callback with condition countdownPenaltyRenderer = ({ minutes, seconds, completed }) => { const { t } = this.props; 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) })} ); } }; handleTakeAmountChange = (e) => { if (e.target.value != "" & e.target.value != null){ this.setState({takeAmount: parseFloat(e.target.value)}) }else{ this.setState({takeAmount: e.target.value}) } } amountHelperText=()=>{ const { t } = this.props; if(this.state.takeAmount < this.state.min_amount & this.state.takeAmount != ""){ return t("Too low") }else if (this.state.takeAmount > this.state.max_amount & this.state.takeAmount != ""){ return t("Too high") }else{ return null } } takeOrderButton = () => { const { t } = this.props; if(this.state.has_range){ return(
this.state.max_amount) & this.state.takeAmount != "" } helperText={this.amountHelperText()} label={t("Amount {{currencyCode}}", {currencyCode: this.state.currencyCode})} size="small" type="number" required="true" value={this.state.takeAmount} inputProps={{ min:this.state.min_amount , max:this.state.max_amount , style: {textAlign:"center"} }} onChange={this.handleTakeAmountChange} />
this.state.max_amount || this.state.takeAmount == "" || this.state.takeAmount == null) ? '':'none'}}>
this.state.max_amount || this.state.takeAmount == "" || this.state.takeAmount == null) ? 'none':''}}>
) }else{ return( <> ) } } countdownTakeOrderRenderer = ({ seconds, completed }) => { if(isNaN(seconds)){return ()} if (completed) { // Render a completed state return ( ); } else{ return(
) } }; LinearDeterminate =()=> { const [progress, setProgress] = React.useState(0); React.useEffect(() => { const timer = setInterval(() => { setProgress((oldProgress) => { var left = calcTimeDelta( new Date(this.state.expires_at)).total /1000; return (left / this.state.total_secs_exp) * 100; }); }, 1000); return () => { clearInterval(timer); }; }, []); return ( ); } takeOrder=()=>{ this.setState({loading:true}) const requestOptions = { method: 'POST', headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, body: JSON.stringify({ 'action':'take', 'amount':this.state.takeAmount, }), }; fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) .then((response) => response.json()) .then((data) => this.completeSetState(data)); } // set delay to the one matching the order status. If null order status, delay goes to 9999999. setDelay = (status)=>{ return status >= 0 ? this.statusToDelay[status.toString()] : 99999999; } getCurrencyCode(val){ let code = val ? currencyDict[val.toString()] : "" return code } handleClickConfirmCancelButton=()=>{ this.setState({loading:true}) const requestOptions = { method: 'POST', headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, body: JSON.stringify({ 'action':'cancel', }), }; fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) .then((response) => response.json()) .then((data) => this.getOrderDetails(data.id)); this.handleClickCloseConfirmCancelDialog(); } handleClickOpenConfirmCancelDialog = () => { this.setState({openCancel: true}); }; handleClickCloseConfirmCancelDialog = () => { this.setState({openCancel: false}); }; CancelDialog =() =>{ const { t } = this.props; return( {t("Cancel the order?")} {t("If the order is cancelled now you will lose your bond.")} ) } handleClickOpenInactiveMakerDialog = () => { this.setState({openInactiveMaker: true}); }; handleClickCloseInactiveMakerDialog = () => { this.setState({openInactiveMaker: false}); }; InactiveMakerDialog =() =>{ const { t } = this.props; return( {t("The maker is away")} {t("By taking this order you risk wasting your time. If the maker does not proceed in time, you will be compensated in satoshis for 50% of the maker bond.")} ) } handleClickConfirmCollaborativeCancelButton=()=>{ const requestOptions = { method: 'POST', headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, body: JSON.stringify({ 'action':'cancel', }), }; fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) .then((response) => response.json()) .then((data) => this.getOrderDetails(data.id)); this.handleClickCloseCollaborativeCancelDialog(); } handleClickOpenCollaborativeCancelDialog = () => { this.setState({openCollaborativeCancel: true}); }; handleClickCloseCollaborativeCancelDialog = () => { this.setState({openCollaborativeCancel: false}); }; CollaborativeCancelDialog =() =>{ const { t } = this.props; return( {t("Collaborative cancel the order?")} {t("The trade escrow has been posted. The order can be cancelled only if both, maker and taker, agree to cancel.")} ) } BackButton = () => { const { t } = this.props; // If order has expired, show back button. if (this.state.status == 5){ return( )} return(null) } CancelButton = () => { const { t } = this.props; // If maker and Waiting for Bond. Or if taker and Waiting for bond. // Simply allow to cancel without showing the cancel dialog. if ((this.state.is_maker & [0,1].includes(this.state.status)) || this.state.is_taker & this.state.status == 3){ return( )} // If the order does not yet have an escrow deposited. Show dialog // to confirm forfeiting the bond if ([3,6,7].includes(this.state.status)){ return(
)} // If the escrow is Locked, show the collaborative cancel button. if ([8,9].includes(this.state.status)){ return( )} // If none of the above do not return a cancel button. return(null) } // Colors for the status badges statusBadgeColor(status){ if(status=='Active'){return("success")} if(status=='Seen recently'){return("warning")} if(status=='Inactive'){return('error')} } orderBox=()=>{ const { t } = this.props; return( {t("Order Box")} {this.state.is_participant ? <> {this.state.taker_nick!='None' ? <> : "" } : }
{getFlags(this.state.currencyCode)}
{this.state.has_range & this.state.amount == null ? : }
} secondary={this.state.currency==1000 ? t("Swap destination"):t("Accepted payment methods")}/> {/* If there is live Price and Premium data, show it. Otherwise show the order maker settings */} {this.state.price_now? : (this.state.is_explicit ? : ) }
{/* If the user has a penalty/limit */} {this.state.penalty ? <> : null} {/* If the counterparty asked for collaborative cancel */} {this.state.pending_cancel ? <> {t("{{nickname}} is asking for a collaborative cancel", {nickname: this.state.is_maker ? this.state.taker_nick : this.state.maker_nick})} : null} {/* If the user has asked for a collaborative cancel */} {this.state.asked_for_cancel ? <> {t("You asked for a collaborative cancellation")} : null}
{/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */} {this.state.is_participant ? <> : }
) } doubleOrderPageDesktop=()=>{ return( {this.orderBox()} ) } a11yProps(index) { return { id: `simple-tab-${index}`, 'aria-controls': `simple-tabpanel-${index}`, }; } doubleOrderPagePhone=()=>{ const { t } = this.props; const [value, setValue] = React.useState(this.state.showContractBox); const handleChange = (event, newValue) => { this.setState({showContractBox:newValue}) setValue(newValue); }; return(
{this.orderBox()}
); } orderDetailsPage (){ const { t } = this.props; return( this.state.bad_request ?
{/* IMPLEMENT I18N for bad_request */} {t(this.state.bad_request)}
: (this.state.is_participant ? <> {/* Desktop View */} {/* SmarPhone View */} : {this.orderBox()} ) ) } render (){ return ( // Only so nothing shows while requesting the first batch of data this.state.loading ? : this.orderDetailsPage() ); } } export default withTranslation()(OrderPage);