diff --git a/frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx b/frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx index d44b7745..f6c57851 100644 --- a/frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx +++ b/frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx @@ -9,7 +9,7 @@ import { Button, } from '@mui/material'; import { Order } from '../../../models'; -import currencyDict from '../../../../static/assets/currencies.json'; +import currencies from '../../../../static/assets/currencies.json'; import { pn } from '../../../utils'; import { LoadingButton } from '@mui/lab'; @@ -29,7 +29,7 @@ export const ConfirmFiatReceivedDialog = ({ onConfirmClick, }: ConfirmFiatReceivedDialogProps): JSX.Element => { const { t } = useTranslation(); - const currencyCode = currencyDict[order.currency.toString()]; + const currencyCode = currencies[order.currency.toString()]; const amount = pn(parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4))); return ( diff --git a/frontend/src/components/TradeBox/LockInvoiceBox.tsx b/frontend/src/components/TradeBox/Prompts/LockInvoice.tsx similarity index 68% rename from frontend/src/components/TradeBox/LockInvoiceBox.tsx rename to frontend/src/components/TradeBox/Prompts/LockInvoice.tsx index a7ff7ac0..aca7d667 100644 --- a/frontend/src/components/TradeBox/LockInvoiceBox.tsx +++ b/frontend/src/components/TradeBox/Prompts/LockInvoice.tsx @@ -1,36 +1,24 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { - Button, - Grid, - Link, - Typography, - TextField, - Tooltip, - useTheme, - Divider, -} from '@mui/material'; +import { Button, Grid, Link, Typography, TextField, Tooltip, useTheme } from '@mui/material'; import { AccountBalanceWallet, ContentCopy } from '@mui/icons-material'; -import { NewTabIcon } from '../Icons'; +import { NewTabIcon } from '../../Icons'; import QRCode from 'react-qr-code'; -import { Order } from '../../models'; -import { systemClient } from '../../services/System'; -import currencyDict from '../../../static/assets/currencies.json'; -import stepXofY from './stepXofY'; -import { pn } from '../../utils'; +import { Order } from '../../../models'; +import { systemClient } from '../../../services/System'; +import currencies from '../../../../static/assets/currencies.json'; -interface LockInvoiceBoxProps { +interface LockInvoicePromptProps { order: Order; concept: 'bond' | 'escrow'; } -export const LockInvoiceBox = ({ order, concept }: LockInvoiceBoxProps): JSX.Element => { +export const LockInvoicePrompt = ({ order, concept }: LockInvoicePromptProps): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); - const currencyCode = currencyDict[order.currency.toString()]; + const currencyCode: string = currencies[`${order.currency}`]; const invoice = concept === 'bond' ? order.bond_invoice : order.escrow_invoice; - const amountSats = concept === 'bond' ? order.bond_satoshis : order.escrow_satoshis; const helperText = concept === 'bond' ? t( @@ -41,22 +29,6 @@ export const LockInvoiceBox = ({ order, concept }: LockInvoiceBoxProps): JSX.Ele { currencyCode }, ); - const Title = function () { - let text = `Lock {{amountSats}} Sats to ${order.is_maker ? 'PUBLISH' : 'TAKE'} order`; - if (concept === 'escrow') { - text = 'Lock {{amountSats}} Sats as collateral'; - } - return ( - - - {t(text, { - amountSats: pn(amountSats), - })} - - {` ${stepXofY(order)}`} - - ); - }; const CompatibleWalletsButton = function () { return ( - - - - - - {concept === 'bond' ? : } @@ -145,4 +111,4 @@ export const LockInvoiceBox = ({ order, concept }: LockInvoiceBoxProps): JSX.Ele ); }; -export default LockInvoiceBox; +export default LockInvoicePrompt; diff --git a/frontend/src/components/TradeBox/Prompts/TakerFound.tsx b/frontend/src/components/TradeBox/Prompts/TakerFound.tsx new file mode 100644 index 00000000..cce21870 --- /dev/null +++ b/frontend/src/components/TradeBox/Prompts/TakerFound.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Grid, Typography } from '@mui/material'; +import { Order } from '../../../models'; +import stepXofY from '../stepXofY'; + +interface TakerFoundPrompProps { + order: Order; +} + +export const TakerFoundPrompt = ({ order }: TakerFoundPrompProps): JSX.Element => { + const { t } = useTranslation(); + + const Title = function () { + return ( + + {t('A taker has been found!')} + {` ${stepXofY(order)}`} + + ); + }; + + return ( + + + + {t( + 'Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.', + )} + + + + ); +}; + +export default TakerFoundPrompt; diff --git a/frontend/src/components/TradeBox/Prompts/index.ts b/frontend/src/components/TradeBox/Prompts/index.ts new file mode 100644 index 00000000..042e08e1 --- /dev/null +++ b/frontend/src/components/TradeBox/Prompts/index.ts @@ -0,0 +1,2 @@ +export { LockInvoicePrompt } from './LockInvoice'; +export { TakerFoundPrompt } from './TakerFound'; diff --git a/frontend/src/components/TradeBox/Title/index.tsx b/frontend/src/components/TradeBox/Title/index.tsx new file mode 100644 index 00000000..402bf64d --- /dev/null +++ b/frontend/src/components/TradeBox/Title/index.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Typography, useTheme } from '@mui/material'; +import { Order } from '../../../models'; +import stepXofY from '../stepXofY'; +import currencies from '../../../../static/assets/currencies.json'; +import { pn } from '../../../utils'; + +interface TakerFoundPrompProps { + order: Order; +} + +export const Title = ({ order }: TakerFoundPrompProps): JSX.Element => { + const { t } = useTranslation(); + const theme = useTheme(); + const currencyCode: string = currencies[`${order.currency}`]; + + let text = ''; + + if (order.is_maker && order.status === 0) { + text = t('Lock {{amountSats}} Sats to PUBLISH order', { amountSats: pn(order.bond_satoshis) }); + } else if (order.is_taker && order.status === 3) { + text = t('Lock {{amountSats}} Sats to TAKE order', { amountSats: pn(order.bond_satoshis) }); + } else if (order.is_seller && [6, 7].includes(order.status)) { + text = t('Lock {{amountSats}} Sats as collateral', { amountSats: pn(order.escrow_satoshis) }); + } + + { + /* Maker and taker Bond request */ + } + // {this.props.data.is_maker & (this.props.data.status == 0) ? this.showQRInvoice() : ''} + // {this.props.data.is_taker & (this.props.data.status == 3) ? this.showQRInvoice() : ''} + + // {/* Waiting for taker and taker bond request */} + // {this.props.data.is_maker & (this.props.data.status == 2) ? this.showPausedOrder() : ''} + // {this.props.data.is_maker & (this.props.data.status == 1) ? this.showMakerWait() : ''} + // {this.props.data.is_maker & (this.props.data.status == 3) ? this.showTakerFound() : ''} + + // {/* Send Invoice (buyer) and deposit collateral (seller) */} + // {this.props.data.is_seller & + // (this.props.data.status == 6 || this.props.data.status == 7) + // ? this.showEscrowQRInvoice() + // : ''} + // {this.props.data.is_buyer & (this.props.data.status == 6 || this.props.data.status == 8) + // ? this.showInputInvoice() + // : ''} + // {this.props.data.is_buyer & (this.props.data.status == 7) + // ? this.showWaitingForEscrow() + // : ''} + // {this.props.data.is_seller & (this.props.data.status == 8) + // ? this.showWaitingForBuyerInvoice() + // : ''} + + // {/* In Chatroom */} + // {this.props.data.status == 9 || this.props.data.status == 10 ? this.showChat() : ''} + + // {/* Trade Finished */} + // {this.props.data.is_seller & [13, 14, 15].includes(this.props.data.status) + // ? this.showRateSelect() + // : ''} + // {this.props.data.is_buyer & (this.props.data.status == 14) ? this.showRateSelect() : ''} + + // {/* Trade Finished - Payment Routing Failed */} + // {this.props.data.is_buyer & (this.props.data.status == 13) + // ? this.showSendingPayment() + // : ''} + + // {/* Trade Finished - Payment Routing Failed */} + // {this.props.data.is_buyer & (this.props.data.status == 15) + // ? this.showRoutingFailed() + // : ''} + + // {/* Trade Finished - TODO Needs more planning */} + // {this.props.data.status == 11 ? this.showInDisputeStatement() : ''} + // {this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ''} + // {(this.props.data.status == 17) & this.props.data.is_taker || + // (this.props.data.status == 18) & this.props.data.is_maker + // ? this.showDisputeWinner() + // : ''} + // {(this.props.data.status == 18) & this.props.data.is_taker || + // (this.props.data.status == 17) & this.props.data.is_maker + // ? this.showDisputeLoser() + // : ''} + + // {/* Order has expired */} + // {this.props.data.status == 5 ? this.showOrderExpired() : ''} + + return ( + + {text} + {stepXofY(order)} + + ); +}; + +export default Title; diff --git a/frontend/src/components/TradeBox/index.tsx b/frontend/src/components/TradeBox/index.tsx index 8ea9e8cc..f1409d93 100644 --- a/frontend/src/components/TradeBox/index.tsx +++ b/frontend/src/components/TradeBox/index.tsx @@ -1,63 +1,15 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { - Box, - Alert, - AlertTitle, - ToggleButtonGroup, - ToggleButton, - IconButton, - Link, - Paper, - Rating, - Button, - Tooltip, - CircularProgress, - Grid, - Typography, - TextField, - List, - ListItem, - ListItemText, - Divider, - ListItemIcon, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - dividerClasses, -} from '@mui/material'; -import { LoadingButton } from '@mui/lab'; -import QRCode from 'react-qr-code'; -import Countdown, { zeroPad } from 'react-countdown'; -import EncryptedChat from './EncryptedChat'; -import TradeSummary from './TradeSummary'; +import { Box, Divider } from '@mui/material'; + import { systemClient } from '../../services/System'; import { apiClient } from '../../services/api'; import { ConfirmDisputeDialog, ConfirmFiatReceivedDialog } from './Dialogs'; + +import Title from './Title'; +import { LockInvoicePrompt, TakerFoundPrompt } from './Prompts'; import BondStatus from './BondStatus'; -// Icons -import { - Percent, - Book, - Lock, - LockOpen, - Balance, - ContentCopy, - PauseCircle, - PlayCircle, - Bolt, - Link, - AccountBalanceWallet, - Favorite, - RocketLaunch, - Refresh, -} from '@mui/icons-material'; -import { NewTabIcon } from '../Icons'; - -import { pn } from '../../utils'; import { Order } from '../../models'; const audio = { @@ -130,13 +82,13 @@ interface TradeBoxProps { const TradeBox = ({ order, setOrder }: TradeBoxProps): JSX.Element => { const { t } = useTranslation(); + // Buttons and Dialogs const [loadingButtons, setLoadingButtons] = useState(noLoadingButtons); const [open, setOpen] = useState(closeAll); - // Payout forms + // Forms const [onchain, setOnchain] = useState(defaultOnchain); const [lightning, setLightning] = useState(defaultLightning); - const [statement, setStatement] = useState(''); // Sounds @@ -168,6 +120,38 @@ const TradeBox = ({ order, setOrder }: TradeBoxProps): JSX.Element => { submitAction('confirm'); }; + const Steps = [ + { + isMaker: { + title: '', + prompt: , + bondStatus: 'none', + }, + }, + ]; + + const StepConcent = Steps[order.status][isMaker]; + + // 0: 2000, // 'Waiting for maker bond' + // 1: 25000, // 'Public' + // 2: 90000, // 'Paused' + // 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' + return ( { onAgreeClick={onClickAgreeOpenDispute} /> - + - + + + { -// const { t } = this.props; -// return ( -// -// -// {this.props.data.is_maker ? ( -// -// -// {t('Lock {{amountSats}} Sats to PUBLISH order', { -// amountSats: pn(this.props.data.bond_satoshis), -// })} -// {' '} -// {' ' + this.stepXofY()} -// -// ) : ( -// -// -// {t('Lock {{amountSats}} Sats to TAKE order', { -// amountSats: pn(this.props.data.bond_satoshis), -// })} -// {' '} -// {' ' + this.stepXofY()} -// -// )} -// - -// -// {this.compatibleWalletsButton()} -// - -// -// -// -// { -// systemClient.copyToClipboard(this.props.data.bond_invoice); -// }} -// align='center' -// > -// {' '} -// -// {t('Copy to clipboard')} -// -// -// -// -// -// -// -// ); -// }; - -// showEscrowQRInvoice = () => { -// const { t } = this.props; -// return ( -// -// {/* Make sound for Taker found or HTLC received. */} -// {this.props.data.is_maker ? this.Sound('taker-found') : this.Sound('locked-invoice')} -// -// -// -// {t('Lock {{amountSats}} Sats as collateral', { -// amountSats: pn(this.props.data.escrow_satoshis), -// })} -// -// {' ' + this.stepXofY()} -// -// -// -// -// {t( -// 'You risk losing your bond if you do not lock the collateral. Total time available is {{deposit_timer_hours}}h {{deposit_timer_minutes}}m.', -// this.depositHoursMinutes(), -// )} -// -// -// -// -// -// { -// systemClient.copyToClipboard(this.props.data.escrow_invoice); -// }} -// align='center' -// > -// {' '} -// -// {t('Copy to clipboard')} -// -// -// -// -// -// -// -// -// ); -// }; - // showTakerFound = () => { // const { t } = this.props; // return ( diff --git a/frontend/src/components/TradeBox/old.js b/frontend/src/components/TradeBox/old.js deleted file mode 100644 index c962bd65..00000000 --- a/frontend/src/components/TradeBox/old.js +++ /dev/null @@ -1,1884 +0,0 @@ -import React, { Component } from 'react'; -import { withTranslation, Trans } from 'react-i18next'; -import { - Alert, - AlertTitle, - ToggleButtonGroup, - ToggleButton, - IconButton, - Link, - Paper, - Rating, - Button, - Tooltip, - CircularProgress, - Grid, - Typography, - TextField, - List, - ListItem, - ListItemText, - Divider, - ListItemIcon, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, -} from '@mui/material'; -import { LoadingButton } from '@mui/lab'; -import QRCode from 'react-qr-code'; -import Countdown, { zeroPad } from 'react-countdown'; -import EncryptedChat from './EncryptedChat'; -import TradeSummary from './TradeSummary'; -import MediaQuery from 'react-responsive'; -import { systemClient } from '../../services/System'; -import { apiClient } from '../../services/api'; - -// Icons -import PercentIcon from '@mui/icons-material/Percent'; -import BookIcon from '@mui/icons-material/Book'; -import LockIcon from '@mui/icons-material/Lock'; -import LockOpenIcon from '@mui/icons-material/LockOpen'; -import BalanceIcon from '@mui/icons-material/Balance'; -import ContentCopy from '@mui/icons-material/ContentCopy'; -import PauseCircleIcon from '@mui/icons-material/PauseCircle'; -import PlayCircleIcon from '@mui/icons-material/PlayCircle'; -import BoltIcon from '@mui/icons-material/Bolt'; -import LinkIcon from '@mui/icons-material/Link'; -import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; -import FavoriteIcon from '@mui/icons-material/Favorite'; -import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import { NewTabIcon } from '../Icons'; - -import { pn } from '../../utils/prettyNumbers'; - -class TradeBox extends Component { - invoice_escrow_duration = 3; - - constructor(props) { - super(props); - this.state = { - openConfirmFiatReceived: false, - loadingButtonFiatSent: false, - loadingButtonFiatReceived: false, - loadingSubmitInvoice: false, - loadingSubmitAddress: false, - openConfirmDispute: false, - receiveTab: 0, - address: '', - miningFee: 1.05, - badInvoice: false, - badAddress: false, - badStatement: false, - }; - } - - Sound = (soundFileName) => ( - // Four filenames: "locked-invoice", "taker-found", "open-chat", "successful" - - ); - - stepXofY = () => { - // set y value - let x = null; - let y = null; - const status = this.props.data.status; - - if (this.props.data.is_maker) { - y = 5; - } - if (this.props.data.is_taker) { - y = 4; - } - - // set x values - if (this.props.data.is_maker) { - if (status == 0) { - x = 1; - } else if ([1, 2, 3].includes(status)) { - x = 2; - } else if ([6, 7, 8].includes(status)) { - x = 3; - } else if (status == 9) { - x = 4; - } else if (status == 10) { - x = 5; - } - } - if (this.props.data.is_taker) { - if (status == 3) { - x = 1; - } else if ([6, 7, 8].includes(status)) { - x = 2; - } else if (status == 9) { - x = 3; - } else if (status == 10) { - x = 4; - } - } - - // Return "(x/y)" - if ((x != null) & (y != null)) { - return '(' + x + '/' + y + ')'; - } else { - return ''; - } - }; - - handleClickOpenConfirmDispute = () => { - this.setState({ openConfirmDispute: true }); - }; - - handleClickCloseConfirmDispute = () => { - this.setState({ openConfirmDispute: false }); - }; - - handleClickAgreeDisputeButton = () => { - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'dispute', - }) - .then((data) => this.props.completeSetState(data)); - this.handleClickCloseConfirmDispute(); - }; - - ConfirmDisputeDialog = () => { - const { t } = this.props; - return ( - - - {t('Do you want to open a dispute?')} - - - - {t( - 'The RoboSats staff will examine the statements and evidence provided. You need to build a complete case, as the staff cannot read the chat. It is best to provide a burner contact method with your statement. The satoshis in the trade escrow will be sent to the dispute winner, while the dispute loser will lose the bond.', - )} - - - - {t( - 'Make sure to EXPORT the chat log. The staff might request your exported chat log JSON in order to solve discrepancies. It is your responsibility to store it.', - )} - - - - - {t('Disagree')} - - - {t('Agree and open dispute')} - - - - ); - }; - - handleClickOpenConfirmFiatReceived = () => { - this.setState({ openConfirmFiatReceived: true }); - }; - - handleClickCloseConfirmFiatReceived = () => { - this.setState({ openConfirmFiatReceived: false }); - }; - - handleClickTotallyConfirmFiatReceived = () => { - this.handleClickConfirmButton(); - this.handleClickCloseConfirmFiatReceived(); - }; - - ConfirmFiatReceivedDialog = () => { - const { t } = this.props; - return ( - - - {t('Confirm you received {{amount}} {{currencyCode}}?', { - currencyCode: this.props.data.currencyCode, - amount: pn( - parseFloat( - parseFloat(this.props.data.amount).toFixed( - this.props.data.currency == 1000 ? 8 : 4, - ), - ), - ), - })} - - - - {t( - 'Confirming that you received the fiat will finalize the trade. The satoshis in the escrow will be released to the buyer. Only confirm after the {{amount}} {{currencyCode}} have arrived to your account. In addition, if you have received the payment and do not confirm it, you risk losing your bond.', - { - currencyCode: this.props.data.currencyCode, - amount: pn( - parseFloat( - parseFloat(this.props.data.amount).toFixed( - this.props.data.currency == 1000 ? 8 : 4, - ), - ), - ), - }, - )} - - - - - {t('Go back')} - - { - this.setState({ loadingButtonFiatReceived: true }); - this.handleClickTotallyConfirmFiatReceived(); - }} - > - {t('Confirm')} - - - - ); - }; - - showQRInvoice = () => { - const { t } = this.props; - return ( - - {/* - - {t("Robots show commitment to their peers")} - - */} - - {this.props.data.is_maker ? ( - - - {t('Lock {{amountSats}} Sats to PUBLISH order', { - amountSats: pn(this.props.data.bond_satoshis), - })} - {' '} - {' ' + this.stepXofY()} - - ) : ( - - - {t('Lock {{amountSats}} Sats to TAKE order', { - amountSats: pn(this.props.data.bond_satoshis), - })} - {' '} - {' ' + this.stepXofY()} - - )} - - - - {this.compatibleWalletsButton()} - - - - - - { - systemClient.copyToClipboard(this.props.data.bond_invoice); - }} - align='center' - > - {' '} - - {t('Copy to clipboard')} - - - - - - - - ); - }; - - showBondIsLocked = () => { - const { t } = this.props; - return ( - - - - - {this.props.data.is_maker - ? t('Your maker bond is locked') - : t('Your taker bond is locked')} - - - - ); - }; - - showBondIsSettled = () => { - const { t } = this.props; - return ( - - - - - {this.props.data.is_maker - ? t('Your maker bond was settled') - : t('Your taker bond was settled')} - - - - ); - }; - - showBondIsReturned = () => { - const { t } = this.props; - return ( - - - - - {this.props.data.is_maker - ? t('Your maker bond was unlocked') - : t('Your taker bond was unlocked')} - - - - ); - }; - - showEscrowQRInvoice = () => { - const { t } = this.props; - return ( - - {/* Make sound for Taker found or HTLC received. */} - {this.props.data.is_maker ? this.Sound('taker-found') : this.Sound('locked-invoice')} - - - - {t('Lock {{amountSats}} Sats as collateral', { - amountSats: pn(this.props.data.escrow_satoshis), - })} - - {' ' + this.stepXofY()} - - - - - {t( - 'You risk losing your bond if you do not lock the collateral. Total time available is {{deposit_timer_hours}}h {{deposit_timer_minutes}}m.', - this.depositHoursMinutes(), - )} - - - - - - { - systemClient.copyToClipboard(this.props.data.escrow_invoice); - }} - align='center' - > - {' '} - - {t('Copy to clipboard')} - - - - - - - {this.showBondIsLocked()} - - ); - }; - - showTakerFound = () => { - const { t } = this.props; - return ( - - {/* Make bell sound when taker is found. SUPRESSED: It's annoying, not the right moment! Play only after taker locks bon */} - {/* {this.Sound("taker-found")} */} - - - {t('A taker has been found!')} {' ' + this.stepXofY()} - - - - - - {t( - 'Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.', - )} - - - {this.showBondIsLocked()} - - ); - }; - - depositHoursMinutes = () => { - const hours = parseInt(this.props.data.escrow_duration / 3600); - const minutes = parseInt((this.props.data.escrow_duration - hours * 3600) / 60); - const dict = { deposit_timer_hours: hours, deposit_timer_minutes: minutes }; - return dict; - }; - - handleClickPauseOrder = () => { - this.props.completeSetState({ pauseLoading: true }); - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'pause', - }) - .then((data) => this.props.getOrderDetails(data.id)); - }; - - showMakerWait = () => { - const { t } = this.props; - return ( - - {/* Make confirmation sound for HTLC received. */} - {this.Sound('locked-invoice')} - - - {t('Your order is public')} {' ' + this.stepXofY()} - - - - - - - - {t( - 'Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{deposit_timer_hours}}h {{deposit_timer_minutes}}m to reply. If you do not reply, you risk losing your bond.', - this.depositHoursMinutes(), - )} - - - - - - {t( - 'If the order expires untaken, your bond will return to you (no action needed).', - )} - - - - - - - - - - - - - - - - - - {this.props.pauseLoading ? ( - - ) : ( - - - - - - )} - - - - - - - - - - - - - - - {this.showBondIsLocked()} - - ); - }; - - showPausedOrder = () => { - const { t } = this.props; - return ( - - - - {t('Your order is paused')} {' ' + this.stepXofY()} - - - - - - - - - {t( - 'Your public order has been paused. At the moment it cannot be seen or taken by other robots. You can choose to unpause it at any time.', - )} - - - - - {this.props.pauseLoading ? ( - - ) : ( - - - {t('Unpause Order')} - - )} - - - - - - {this.showBondIsLocked()} - - ); - }; - - handleInputInvoiceChanged = (e) => { - this.setState({ - invoice: e.target.value, - badInvoice: false, - }); - }; - - handleClickSubmitInvoiceButton = () => { - this.setState({ badInvoice: false, loadingSubmitInvoice: true }); - - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'update_invoice', - invoice: this.state.invoice, - }) - .then( - (data) => - this.setState({ badInvoice: data.bad_invoice, loadingSubmitInvoice: false }) & - this.props.completeSetState(data), - ); - }; - - handleInputAddressChanged = (e) => { - this.setState({ - address: e.target.value, - badAddress: false, - }); - }; - - handleMiningFeeChanged = (e) => { - let fee = e.target.value; - if (fee > 50) { - fee = 50; - } - - this.setState({ - miningFee: fee, - }); - }; - - handleClickSubmitAddressButton = () => { - this.setState({ badInvoice: false, loadingSubmitAddress: true }); - - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'update_address', - address: this.state.address, - mining_fee_rate: Math.max(1, this.state.miningFee), - }) - .then( - (data) => - this.setState({ badAddress: data.bad_address, loadingSubmitAddress: false }) & - this.props.completeSetState(data), - ); - }; - - handleInputDisputeChanged = (e) => { - this.setState({ - statement: e.target.value, - badStatement: false, - }); - }; - - handleClickSubmitStatementButton = () => { - this.setState({ badInvoice: false }); - - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'submit_statement', - statement: this.state.statement, - }) - .then( - (data) => - this.setState({ badStatement: data.bad_statement }) & this.props.completeSetState(data), - ); - }; - - handleScan = (data) => { - if (data) { - this.setState({ - invoice: data, - }); - } - }; - - handleError = (err) => { - console.error(err); - }; - - compatibleWalletsButton = () => { - const { t } = this.props; - - return ( - - - {t('See Compatible Wallets')} - - - ); - }; - - showInputInvoice() { - const { t } = this.props; - return ( - - - {/* Make sound for Taker found or HTLC received. */} - {this.props.data.is_maker ? this.Sound('taker-found') : this.Sound('locked-invoice')} - - - {' '} - {t('Submit payout info for {{amountSats}} Sats', { - amountSats: pn(this.props.data.invoice_amount), - })} - {' '} - {' ' + this.stepXofY()} - - - - - - - {t( - 'Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC.', - { - amountFiat: pn( - parseFloat( - parseFloat(this.props.data.amount).toFixed( - this.props.data.currency == 1000 ? 8 : 4, - ), - ), - ), - currencyCode: this.props.data.currencyCode, - }, - )} - - - - - - - this.setState({ receiveTab: 0 })} - > - - {t('Lightning')} - - - - this.setState({ - receiveTab: 1, - miningFee: parseFloat(this.props.data.suggested_mining_fee_rate), - }) - } - > - - {t('Onchain')} - - - - - - {/* LIGHTNING PAYOUT TAB */} - - - - - - {t('Submit a valid invoice for {{amountSats}} Satoshis.', { - amountSats: pn(this.props.data.invoice_amount), - })} - - - - - {this.compatibleWalletsButton()} - - - - - - - - {t('Submit')} - - - - - - {/* ONCHAIN PAYOUT TAB */} - - - - - {t('EXPERIMENTAL: ')} - {t('RoboSats will do a swap and send the Sats to your onchain address.')} - - - - - - - - - - - - - - - - - - - - {pn( - parseInt( - this.props.data.invoice_amount - - Math.max(1, this.state.miningFee) * 141 - - (this.props.data.invoice_amount * this.props.data.swap_fee_rate) / 100, - ), - ) + ' Sats'} - - } - secondary={t('Final amount you will receive')} - /> - - - - 50} - helperText={this.state.miningFee < 1 || this.state.miningFee > 50 ? 'Invalid' : ''} - label={t('Mining Fee')} - required - sx={{ width: 110 }} - value={this.state.miningFee} - type='number' - inputProps={{ - max: 50, - min: 1, - style: { textAlign: 'center' }, - }} - onChange={this.handleMiningFeeChanged} - /> - - - - - {t('Submit')} - - - - - - - - {this.showBondIsLocked()} - - ); - } - - // Asks the user for a dispute statement. - showInDisputeStatement = () => { - const { t } = this.props; - if (this.props.data.statement_submitted) { - return ( - - - - {t('We have received your statement')} - - - - - - - - {t( - 'We are waiting for your trade counterpart statement. If you are hesitant about the state of the dispute or want to add more information, contact robosats@protonmail.com.', - )} - - - - - {t( - 'Please, save the information needed to identify your order and your payments: order ID; payment hashes of the bonds or escrow (check on your lightning wallet); exact amount of satoshis; and robot nickname. You will have to identify yourself as the user involved in this trade via email (or other contact methods).', - )} - - - - - - {this.showBondIsSettled()} - - ); - } else { - return ( - // TODO Option to upload files - - - - - {t('A dispute has been opened')} - - - - - - - {t( - 'Please, submit your statement. Be clear and specific about what happened and provide the necessary evidence. You MUST provide a contact method: burner email, XMPP or telegram username to follow up with the staff. Disputes are solved at the discretion of real robots (aka humans), so be as helpful as possible to ensure a fair outcome. Max 5000 chars.', - )} - - - - - - - - - Submit - - - - {this.showBondIsSettled()} - - ); - } - }; - - showWaitForDisputeResolution = () => { - const { t } = this.props; - return ( - - - - {t('We have the statements')} - - - - - - - - {t( - 'Both statements have been received, wait for the staff to resolve the dispute. If you are hesitant about the state of the dispute or want to add more information, contact robosats@protonmail.com. If you did not provide a contact method, or are unsure whether you wrote it right, write us immediately.', - )} - - - - - {t( - 'Please, save the information needed to identify your order and your payments: order ID; payment hashes of the bonds or escrow (check on your lightning wallet); exact amount of satoshis; and robot nickname. You will have to identify yourself as the user involved in this trade via email (or other contact methods).', - )} - - - - - - {this.showBondIsSettled()} - - ); - }; - - showDisputeWinner = () => { - const { t } = this.props; - return ( - - - - {t('You have won the dispute')} - - - - - {t( - 'You can claim the dispute resolution amount (escrow and fidelity bond) from your profile rewards. If there is anything the staff can help with, do not hesitate to contact to robosats@protonmail.com (or via your provided burner contact method).', - )} - - - {this.showBondIsSettled()} - - ); - }; - - showDisputeLoser = () => { - const { t } = this.props; - return ( - - - - {t('You have lost the dispute')} - - - - - {t( - 'Unfortunately you have lost the dispute. If you think this is a mistake you can ask to re-open the case via email to robosats@protonmail.com. However, chances of it being investigated again are low.', - )} - - - {this.showBondIsSettled()} - - ); - }; - - showWaitingForEscrow() { - const { t } = this.props; - return ( - - - - {t('Your info looks good!')} {' ' + this.stepXofY()} - - - - - - - - {t('We are waiting for the seller to lock the trade amount.')} - - - - - {t( - 'Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).', - )} - - - - - - {this.showBondIsLocked()} - - ); - } - - showWaitingForBuyerInvoice() { - const { t } = this.props; - return ( - - {/* Make confirmation sound for HTLC received. */} - {this.Sound('locked-invoice')} - - - {t('The trade collateral is locked!')} {' ' + this.stepXofY()} - - - - - - - - {t( - 'We are waiting for the buyer to post a lightning invoice. Once he does, you will be able to directly communicate the fiat payment details.', - )} - - - - - - {t( - 'Just hang on for a moment. If the buyer does not cooperate, you will get back the trade collateral and your bond automatically. In addition, you will receive a compensation (check the rewards in your profile).', - )} - - - - - - {this.showBondIsLocked()} - - ); - } - - handleClickConfirmButton = () => { - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'confirm', - }) - .then((data) => { - this.props.completeSetState(data), - this.setState({ loadingButtonFiatSent: false, loadingButtonFiatReceived: false }); - }); - }; - - handleRatingUserChange = (e) => { - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'rate_user', - rating: e.target.value, - }) - .then((data) => this.props.completeSetState(data)); - }; - - handleRatingRobosatsChange = (e) => { - if (this.state.rating_platform != null) { - return null; - } - this.setState({ rating_platform: e.target.value }); - - apiClient - .post('/api/order/?order_id=' + this.props.data.id, { - action: 'rate_platform', - rating: e.target.value, - }) - .then((data) => this.props.completeSetState(data)); - }; - - showFiatSentButton() { - const { t } = this.props; - return ( - - - { - this.setState({ loadingButtonFiatSent: true }); - this.handleClickConfirmButton(); - }} - > - {t('Confirm {{amount}} {{currencyCode}} sent', { - currencyCode: this.props.data.currencyCode, - amount: pn( - parseFloat( - parseFloat(this.props.data.amount).toFixed( - this.props.data.currency == 1000 ? 8 : 4, - ), - ), - ), - })} - - - - ); - } - - showFiatReceivedButton() { - const { t } = this.props; - return ( - - - {t('Confirm {{amount}} {{currencyCode}} received', { - currencyCode: this.props.data.currencyCode, - amount: pn( - parseFloat( - parseFloat(this.props.data.amount).toFixed( - this.props.data.currency == 1000 ? 8 : 4, - ), - ), - ), - })} - - - ); - } - - disputeCountdownRenderer = ({ hours, minutes }) => { - return ( - - {hours}h {zeroPad(minutes)}m{' '} - - ); - }; - - showOpenDisputeButton() { - const { t } = this.props; - const now = Date.now(); - const expires_at = new Date(this.props.data.expires_at); - // open dispute button enables 12h before expiry - expires_at.setHours(expires_at.getHours() - 12); - return ( - expires_at} - disableTouchListener={now > expires_at} - enterTouchDelay={0} - title={ - - To open a dispute you need to wait - - - } - > - - - {t('Open Dispute')} - - - - ); - } - - handleRenewOrderButtonPressed = () => { - this.setState({ renewLoading: true }); - const body = { - type: this.props.data.type, - currency: this.props.data.currency, - amount: this.props.data.has_range ? null : this.props.data.amount, - has_range: this.props.data.has_range, - min_amount: this.props.data.min_amount, - max_amount: this.props.data.max_amount, - payment_method: this.props.data.payment_method, - is_explicit: this.props.data.is_explicit, - premium: this.props.data.is_explicit ? null : this.props.data.premium, - satoshis: this.props.data.is_explicit ? this.props.data.satoshis : null, - public_duration: this.props.data.public_duration, - escrow_duration: this.props.data.escrow_duration, - bond_size: this.props.data.bond_size, - bondless_taker: this.props.data.bondless_taker, - }; - apiClient - .post('/api/make/', body) - .then( - (data) => - this.setState({ badRequest: data.bad_request }) & - (data.id - ? this.props.push('/order/' + data.id) & this.props.getOrderDetails(data.id) - : ''), - ); - }; - - showOrderExpired = () => { - const { t } = this.props; - const show_renew = this.props.data.is_maker; - - return ( - - - - {t('The order has expired')} - - - - - {t(this.props.data.expiry_message)} - - {show_renew ? ( - - {this.state.renewLoading ? ( - - ) : ( - - {t('Renew Order')} - - )} - - ) : null} - - ); - }; - - showChat = () => { - const { t } = this.props; - // In Chatroom - No fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) - if (this.props.data.is_buyer & (this.props.data.status == 9)) { - var showSendButton = true; - var showReveiceButton = false; - var showDisputeButton = true; - } - if (this.props.data.is_seller & (this.props.data.status == 9)) { - var showSendButton = false; - var showReveiceButton = false; - var showDisputeButton = true; - } - - // In Chatroom - Fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) - if (this.props.data.is_buyer & (this.props.data.status == 10)) { - var showSendButton = false; - var showReveiceButton = false; - var showDisputeButton = true; - } - if (this.props.data.is_seller & (this.props.data.status == 10)) { - var showSendButton = false; - var showReveiceButton = true; - var showDisputeButton = true; - } - - return ( - - {/* Make confirmation sound for Chat Open. */} - {this.Sound('locked-invoice')} - - - - {' '} - {this.props.data.is_seller ? t('Chat with the buyer') : t('Chat with the seller')} - {' '} - {' ' + this.stepXofY()} - - - - {this.props.data.is_seller ? ( - - {this.props.data.status == 9 - ? t( - 'Say hi! Be helpful and concise. Let them know how to send you {{amount}} {{currencyCode}}.', - { - currencyCode: this.props.data.currencyCode, - amount: pn( - parseFloat( - parseFloat(this.props.data.amount).toFixed( - this.props.data.currency == 1000 ? 8 : 4, - ), - ), - ), - }, - ) - : t("The buyer has sent the fiat. Click 'Confirm Received' once you receive it.")} - - ) : ( - - {this.props.data.status == 9 - ? t( - "Say hi! Ask for payment details and click 'Confirm Sent' as soon as the payment is sent.", - ) - : t('Wait for the seller to confirm he has received the payment.')} - - )} - - - - - {showDisputeButton ? this.showOpenDisputeButton() : ''} - {showSendButton ? this.showFiatSentButton() : ''} - {showReveiceButton ? this.showFiatReceivedButton() : ''} - - {this.showBondIsLocked()} - - ); - }; - - showRateSelect() { - const { t } = this.props; - const show_renew = this.props.data.is_maker; - - return ( - - {/* Make confirmation sound for Chat Open. */} - {this.Sound('successful')} - - - - - {t('Trade finished!')} - - - - - - - - What do you think of RoboSats? - - - - - - - {this.state.rating_platform == 5 ? ( - - - - {t('Thank you! RoboSats loves you too')}{' '} - - - - - {t( - 'RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!', - )} - - - ) : null} - {(this.state.rating_platform != 5) & (this.state.rating_platform != null) ? ( - - - {t('Thank you for using Robosats!')} - - - - Let us know how the platform could improve ( - - Telegram - {' '} - /{' '} - - Github - - ) - - - - ) : null} - - {/* SHOW TXID IF USER RECEIVES ONCHAIN */} - {this.props.data.txid ? ( - - - - {t('Your TXID')} - - { - systemClient.copyToClipboard(this.props.data.txid); - }} - > - - - - - - - {this.props.data.txid} - - - - - ) : null} - - - - { - this.props.push('/'); - }} - > - - {t('Start Again')} - - - - {show_renew ? ( - - {this.state.renewLoading ? ( - - ) : ( - - - {t('Renew Order')} - - )} - - ) : null} - - - - - ); - } - - showSendingPayment() { - const { t } = this.props; - return ( - - - - {t('Attempting Lightning Payment')} - - - - - {t( - 'RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must be online in order to receive payments.', - )} - - - - - - - - ); - } - - // Countdown Renderer callback with condition - countdownRenderer = ({ minutes, seconds, completed }) => { - const { t } = this.props; - if (completed) { - // Render a completed state - return ( - - {t('Retrying!')} - - - - ); - } else { - return ( - - {zeroPad(minutes)}m {zeroPad(seconds)}s{' '} - - ); - } - }; - - failureReason = () => { - const { t } = this.props; - return ( - - - {t('Failure reason:')} - - - {t(this.props.data.failure_reason)} - - - ); - }; - - showRoutingFailed = () => { - const { t } = this.props; - if (this.props.data.invoice_expired) { - return ( - - - - {t('Lightning Routing Failed')} - - - - {this.props.data.failure_reason ? this.failureReason() : null} - - - - {t( - 'Your invoice has expired or more than 3 payment attempts have been made. Submit a new invoice.', - )} - - - - - {this.compatibleWalletsButton()} - - - - - - {' '} - {t('Submit an invoice for {{amountSats}} Sats', { - amountSats: pn(this.props.data.invoice_amount), - })} - - - - - - - - - {t('Submit')} - - - {this.showBondIsReturned()} - - ); - } else { - return ( - - - - {t('Lightning Routing Failed')} - - - - {this.props.data.failure_reason ? this.failureReason() : null} - - - - {t( - 'RoboSats will try to pay your invoice 3 times with a one minute pause in between. If it keeps failing, you will be able to submit a new invoice. Check whether you have enough inbound liquidity. Remember that lightning nodes must be online in order to receive payments.', - )} - - - - - - - - - {this.showBondIsReturned()} - - ); - } - }; - - render() { - const { t } = this.props; - return ( - - {this.ConfirmDisputeDialog()} - {this.ConfirmFiatReceivedDialog()} - - - - {t('Contract Box')} - - - - {/* Maker and taker Bond request */} - {this.props.data.is_maker & (this.props.data.status == 0) ? this.showQRInvoice() : ''} - {this.props.data.is_taker & (this.props.data.status == 3) ? this.showQRInvoice() : ''} - - {/* Waiting for taker and taker bond request */} - {this.props.data.is_maker & (this.props.data.status == 2) ? this.showPausedOrder() : ''} - {this.props.data.is_maker & (this.props.data.status == 1) ? this.showMakerWait() : ''} - {this.props.data.is_maker & (this.props.data.status == 3) ? this.showTakerFound() : ''} - - {/* Send Invoice (buyer) and deposit collateral (seller) */} - {this.props.data.is_seller & - (this.props.data.status == 6 || this.props.data.status == 7) - ? this.showEscrowQRInvoice() - : ''} - {this.props.data.is_buyer & (this.props.data.status == 6 || this.props.data.status == 8) - ? this.showInputInvoice() - : ''} - {this.props.data.is_buyer & (this.props.data.status == 7) - ? this.showWaitingForEscrow() - : ''} - {this.props.data.is_seller & (this.props.data.status == 8) - ? this.showWaitingForBuyerInvoice() - : ''} - - {/* In Chatroom */} - {this.props.data.status == 9 || this.props.data.status == 10 ? this.showChat() : ''} - - {/* Trade Finished */} - {this.props.data.is_seller & [13, 14, 15].includes(this.props.data.status) - ? this.showRateSelect() - : ''} - {this.props.data.is_buyer & (this.props.data.status == 14) ? this.showRateSelect() : ''} - - {/* Trade Finished - Payment Routing Failed */} - {this.props.data.is_buyer & (this.props.data.status == 13) - ? this.showSendingPayment() - : ''} - - {/* Trade Finished - Payment Routing Failed */} - {this.props.data.is_buyer & (this.props.data.status == 15) - ? this.showRoutingFailed() - : ''} - - {/* Trade Finished - TODO Needs more planning */} - {this.props.data.status == 11 ? this.showInDisputeStatement() : ''} - {this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ''} - {(this.props.data.status == 17) & this.props.data.is_taker || - (this.props.data.status == 18) & this.props.data.is_maker - ? this.showDisputeWinner() - : ''} - {(this.props.data.status == 18) & this.props.data.is_taker || - (this.props.data.status == 17) & this.props.data.is_maker - ? this.showDisputeLoser() - : ''} - - {/* Order has expired */} - {this.props.data.status == 5 ? this.showOrderExpired() : ''} - {/* TODO */} - {/* */} - {/* */} - - - - ); - } -} - -export default withTranslation()(TradeBox);