mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-24 21:02:09 +03:00
Add Tradebox, OrderDetails, Notifications, OrderPage functional components. (#315)
* Re-init new tradebox * Wip progress on OrderDetails * Wip 2 OrderDetails * Fix multiple requests on load * Add functional Order page * Fixes order page * Fix delete storage * Fix order page style * Add Public order prompt * Add paused order prompt * Add expired prompt * Create statusToContract logics * Move fetch order loop to Main * Add payout and wait prompts * Fix order fetch on badOrder * Fix styles * Add chat and dispute prompts * Add remaining prompts * Fix style * Add notifications component * Fix take order style, add more notifications * Add page title notification * Add more notifications and small tradebox fixes * Small fixes * Small fixes to routing failure prompt * Remove old trade box * Add bad take order
This commit is contained in:
parent
25074351f3
commit
6b2dedce13
@ -19,7 +19,9 @@ interface BookPageProps {
|
||||
fetchLimits: () => void;
|
||||
fav: Favorites;
|
||||
setFav: (state: Favorites) => void;
|
||||
onViewOrder: () => void;
|
||||
fetchBook: () => void;
|
||||
clearOrder: () => void;
|
||||
windowSize: { width: number; height: number };
|
||||
lastDayPremium: number;
|
||||
maker: Maker;
|
||||
@ -36,8 +38,10 @@ const BookPage = ({
|
||||
book = { orders: [], loading: true },
|
||||
fetchBook,
|
||||
fetchLimits,
|
||||
clearOrder,
|
||||
fav,
|
||||
setFav,
|
||||
onViewOrder,
|
||||
maker,
|
||||
setMaker,
|
||||
windowSize,
|
||||
@ -79,6 +83,7 @@ const BookPage = ({
|
||||
history.push('/order/' + id);
|
||||
setPage('order');
|
||||
setCurrentOrder(id);
|
||||
onViewOrder();
|
||||
} else {
|
||||
setOpenNoRobot(true);
|
||||
}
|
||||
@ -128,6 +133,7 @@ const BookPage = ({
|
||||
setPage={setPage}
|
||||
hasRobot={hasRobot}
|
||||
onOrderCreated={(id) => {
|
||||
clearOrder();
|
||||
setCurrentOrder(id);
|
||||
setPage('order');
|
||||
history.push('/order/' + id);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { HashRouter, BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
|
||||
import { HashRouter, BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||
import { useTheme, Box, Slide, Typography } from '@mui/material';
|
||||
|
||||
import UserGenPage from './UserGenPage';
|
||||
@ -22,6 +22,7 @@ import {
|
||||
defaultMaker,
|
||||
defaultInfo,
|
||||
Coordinator,
|
||||
Order,
|
||||
} from '../models';
|
||||
|
||||
import { apiClient } from '../services/api';
|
||||
@ -30,6 +31,7 @@ import { sha256 } from 'js-sha256';
|
||||
|
||||
import defaultCoordinators from '../../static/federation.json';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Notifications from '../components/Notifications';
|
||||
|
||||
const getWindowSize = function (fontSize: number) {
|
||||
// returns window size in EM units
|
||||
@ -39,6 +41,29 @@ const getWindowSize = function (fontSize: number) {
|
||||
};
|
||||
};
|
||||
|
||||
// Refresh delays (ms) according to Order status
|
||||
const statusToDelay = [
|
||||
3000, // 'Waiting for maker bond'
|
||||
35000, // 'Public'
|
||||
180000, // 'Paused'
|
||||
3000, // 'Waiting for taker bond'
|
||||
999999, // 'Cancelled'
|
||||
999999, // 'Expired'
|
||||
8000, // 'Waiting for trade collateral and buyer invoice'
|
||||
8000, // 'Waiting only for seller trade collateral'
|
||||
8000, // 'Waiting only for buyer invoice'
|
||||
10000, // 'Sending fiat - In chatroom'
|
||||
10000, // 'Fiat sent - In chatroom'
|
||||
100000, // 'In dispute'
|
||||
999999, // 'Collaboratively cancelled'
|
||||
10000, // 'Sending satoshis to buyer'
|
||||
999999, // 'Sucessful trade'
|
||||
30000, // 'Failed lightning network routing'
|
||||
300000, // 'Wait for dispute resolution'
|
||||
300000, // 'Maker lost dispute'
|
||||
300000, // 'Taker lost dispute'
|
||||
];
|
||||
|
||||
interface SlideDirection {
|
||||
in: 'left' | 'right' | undefined;
|
||||
out: 'left' | 'right' | undefined;
|
||||
@ -51,6 +76,7 @@ interface MainProps {
|
||||
|
||||
const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
// All app data structured
|
||||
const [book, setBook] = useState<Book>({ orders: [], loading: true });
|
||||
@ -65,8 +91,10 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||
const [fav, setFav] = useState<Favorites>({ type: null, currency: 0 });
|
||||
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
const [delay, setDelay] = useState<number>(60000);
|
||||
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(setInterval(() => null, delay));
|
||||
const [order, setOrder] = useState<Order | undefined>(undefined);
|
||||
const [badOrder, setBadOrder] = useState<string | undefined>(undefined);
|
||||
|
||||
const Router = window.NativeRobosats === undefined ? BrowserRouter : HashRouter;
|
||||
const basename = window.NativeRobosats === undefined ? '' : window.location.pathname;
|
||||
@ -77,7 +105,8 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
in: undefined,
|
||||
out: undefined,
|
||||
});
|
||||
const [currentOrder, setCurrentOrder] = useState<number | null>(null);
|
||||
|
||||
const [currentOrder, setCurrentOrder] = useState<number | undefined>(undefined);
|
||||
|
||||
const navbarHeight = 2.5;
|
||||
const closeAll = {
|
||||
@ -100,8 +129,11 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
if (typeof window !== undefined) {
|
||||
window.addEventListener('resize', onResize);
|
||||
}
|
||||
|
||||
if (baseUrl != '') {
|
||||
fetchBook();
|
||||
fetchLimits();
|
||||
}
|
||||
return () => {
|
||||
if (typeof window !== undefined) {
|
||||
window.removeEventListener('resize', onResize);
|
||||
@ -164,11 +196,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
open.stats ||
|
||||
open.coordinator ||
|
||||
info.version == { major: null, minor: null, patch: null }
|
||||
) {
|
||||
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
|
||||
fetchInfo();
|
||||
}
|
||||
}, [open.stats, open.coordinator]);
|
||||
@ -196,7 +224,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
nickname: data.nickname,
|
||||
token: robot.token,
|
||||
loading: false,
|
||||
avatarLoaded: robot.nickname === data.nickname ? true : false,
|
||||
avatarLoaded: robot.nickname === data.nickname,
|
||||
activeOrderId: data.active_order_id ? data.active_order_id : null,
|
||||
lastOrderId: data.last_order_id ? data.last_order_id : null,
|
||||
referralCode: data.referral_code,
|
||||
@ -215,7 +243,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (baseUrl != '') {
|
||||
if (baseUrl != '' && page != 'robot') {
|
||||
if (open.profile || (robot.token && robot.nickname === null)) {
|
||||
fetchRobot({ keys: false }); // fetch existing robot
|
||||
} else if (robot.token && robot.encPrivKey && robot.pubKey) {
|
||||
@ -224,6 +252,48 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
}
|
||||
}, [open.profile, baseUrl]);
|
||||
|
||||
// Fetch current order at load and in a loop
|
||||
useEffect(() => {
|
||||
if (currentOrder != undefined && (page == 'order' || (order == badOrder) == undefined)) {
|
||||
fetchOrder();
|
||||
}
|
||||
}, [currentOrder, page]);
|
||||
|
||||
useEffect(() => {
|
||||
clearInterval(timer);
|
||||
setTimer(setInterval(fetchOrder, delay));
|
||||
return () => clearInterval(timer);
|
||||
}, [delay, currentOrder, page, badOrder]);
|
||||
|
||||
const orderReceived = function (data: any) {
|
||||
if (data.bad_request != undefined) {
|
||||
setBadOrder(data.bad_request);
|
||||
setDelay(99999999);
|
||||
setOrder(undefined);
|
||||
} else {
|
||||
setDelay(
|
||||
data.status >= 0 && data.status <= 18
|
||||
? page == 'order'
|
||||
? statusToDelay[data.status]
|
||||
: statusToDelay[data.status] * 5
|
||||
: 99999999,
|
||||
);
|
||||
setOrder(data);
|
||||
setBadOrder(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOrder = function () {
|
||||
if (currentOrder != undefined) {
|
||||
apiClient.get(baseUrl, '/api/order/?order_id=' + currentOrder).then(orderReceived);
|
||||
}
|
||||
};
|
||||
|
||||
const clearOrder = function () {
|
||||
setOrder(undefined);
|
||||
setBadOrder(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<Router basename={basename}>
|
||||
{/* load robot avatar image, set avatarLoaded: true */}
|
||||
@ -233,6 +303,14 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
baseUrl={baseUrl}
|
||||
onLoad={() => setRobot({ ...robot, avatarLoaded: true })}
|
||||
/>
|
||||
<Notifications
|
||||
order={order}
|
||||
page={page}
|
||||
openProfile={() => setOpen({ ...closeAll, profile: true })}
|
||||
rewards={robot.earnedRewards}
|
||||
setPage={setPage}
|
||||
windowWidth={windowSize.width}
|
||||
/>
|
||||
{settings.network === 'testnet' ? (
|
||||
<div style={{ height: 0 }}>
|
||||
<Typography color='secondary' align='center'>
|
||||
@ -286,12 +364,17 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
<BookPage
|
||||
book={book}
|
||||
fetchBook={fetchBook}
|
||||
onViewOrder={() => {
|
||||
setOrder(undefined);
|
||||
setDelay(10000);
|
||||
}}
|
||||
limits={limits}
|
||||
fetchLimits={fetchLimits}
|
||||
fav={fav}
|
||||
setFav={setFav}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
clearOrder={clearOrder}
|
||||
lastDayPremium={info.last_day_nonkyc_btc_premium}
|
||||
windowSize={windowSize}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
@ -316,6 +399,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
fetchLimits={fetchLimits}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
clearOrder={clearOrder}
|
||||
setPage={setPage}
|
||||
setCurrentOrder={setCurrentOrder}
|
||||
fav={fav}
|
||||
@ -338,11 +422,16 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
>
|
||||
<div>
|
||||
<OrderPage
|
||||
theme={theme}
|
||||
history={history}
|
||||
{...props}
|
||||
setPage={setPage}
|
||||
baseUrl={baseUrl}
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
setCurrentOrder={setCurrentOrder}
|
||||
badOrder={badOrder}
|
||||
locationOrderId={props.match.params.orderId}
|
||||
setBadOrder={setBadOrder}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
|
||||
setPage={setPage}
|
||||
/>
|
||||
</div>
|
||||
</Slide>
|
||||
@ -366,6 +455,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
</Route>
|
||||
</Switch>
|
||||
</Box>
|
||||
<div style={{ alignContent: 'center', display: 'flex' }}>
|
||||
<NavBar
|
||||
nickname={robot.avatarLoaded ? robot.nickname : null}
|
||||
color={settings.network === 'mainnet' ? 'primary' : 'secondary'}
|
||||
@ -381,6 +471,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
hasRobot={robot.avatarLoaded}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</div>
|
||||
<MainDialogs
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Grid, Paper, Collapse, Typography } from '@mui/material';
|
||||
|
||||
import { LimitList, Maker, Book, Favorites } from '../../models';
|
||||
import { LimitList, Maker, Book, Favorites, Order } from '../../models';
|
||||
|
||||
import { filterOrders } from '../../utils';
|
||||
|
||||
@ -20,6 +20,7 @@ interface MakerPageProps {
|
||||
maker: Maker;
|
||||
setFav: (state: Favorites) => void;
|
||||
setMaker: (state: Maker) => void;
|
||||
clearOrder: () => void;
|
||||
windowSize: { width: number; height: number };
|
||||
hasRobot: boolean;
|
||||
setCurrentOrder: (state: number) => void;
|
||||
@ -35,6 +36,7 @@ const MakerPage = ({
|
||||
maker,
|
||||
setFav,
|
||||
setMaker,
|
||||
clearOrder,
|
||||
windowSize,
|
||||
setCurrentOrder,
|
||||
setPage,
|
||||
@ -76,6 +78,7 @@ const MakerPage = ({
|
||||
showControls={false}
|
||||
showFooter={false}
|
||||
showNoResults={false}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -99,6 +102,7 @@ const MakerPage = ({
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
onOrderCreated={(id) => {
|
||||
clearOrder();
|
||||
setCurrentOrder(id);
|
||||
setPage('order');
|
||||
history.push('/order/' + id);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Tabs, Tab, Paper, useTheme, Tooltip } from '@mui/material';
|
||||
import { Tabs, Tab, Paper, useTheme } from '@mui/material';
|
||||
import MoreTooltip from './MoreTooltip';
|
||||
|
||||
import { OpenDialogs } from '../MainDialogs';
|
||||
@ -30,7 +30,7 @@ interface NavBarProps {
|
||||
open: OpenDialogs;
|
||||
setOpen: (state: OpenDialogs) => void;
|
||||
closeAll: OpenDialogs;
|
||||
currentOrder: number | null;
|
||||
currentOrder: number | undefined;
|
||||
hasRobot: boolean;
|
||||
baseUrl: string;
|
||||
color: 'primary' | 'secondary';
|
||||
@ -151,7 +151,7 @@ const NavBar = ({
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('Order')}
|
||||
value='order'
|
||||
disabled={!hasRobot || currentOrder == null}
|
||||
disabled={!hasRobot || currentOrder == undefined}
|
||||
icon={<Assignment />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
|
File diff suppressed because it is too large
Load Diff
208
frontend/src/basic/OrderPage/index.tsx
Normal file
208
frontend/src/basic/OrderPage/index.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import TradeBox from '../../components/TradeBox';
|
||||
import OrderDetails from '../../components/OrderDetails';
|
||||
|
||||
import { Page } from '../NavBar';
|
||||
import { Order } from '../../models';
|
||||
import { apiClient } from '../../services/api';
|
||||
|
||||
interface OrderPageProps {
|
||||
windowSize: { width: number; height: number };
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
setCurrentOrder: (state: number) => void;
|
||||
fetchOrder: () => void;
|
||||
badOrder: string | undefined;
|
||||
setBadOrder: (state: string | undefined) => void;
|
||||
hasRobot: boolean;
|
||||
setPage: (state: Page) => void;
|
||||
baseUrl: string;
|
||||
locationOrderId: number;
|
||||
}
|
||||
|
||||
const OrderPage = ({
|
||||
windowSize,
|
||||
order,
|
||||
setOrder,
|
||||
setCurrentOrder,
|
||||
badOrder,
|
||||
setBadOrder,
|
||||
setPage,
|
||||
hasRobot = false,
|
||||
baseUrl,
|
||||
locationOrderId,
|
||||
}: OrderPageProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const doublePageWidth: number = 50;
|
||||
const maxHeight: number = windowSize.height * 0.85 - 3;
|
||||
|
||||
const [tab, setTab] = useState<'order' | 'contract'>('contract');
|
||||
|
||||
useEffect(() => setCurrentOrder(locationOrderId), []);
|
||||
|
||||
const renewOrder = function () {
|
||||
if (order != undefined) {
|
||||
const body = {
|
||||
type: order.type,
|
||||
currency: order.currency,
|
||||
amount: order.has_range ? null : order.amount,
|
||||
has_range: order.has_range,
|
||||
min_amount: order.min_amount,
|
||||
max_amount: order.max_amount,
|
||||
payment_method: order.payment_method,
|
||||
is_explicit: order.is_explicit,
|
||||
premium: order.is_explicit ? null : order.premium,
|
||||
satoshis: order.is_explicit ? order.satoshis : null,
|
||||
public_duration: order.public_duration,
|
||||
escrow_duration: order.escrow_duration,
|
||||
bond_size: order.bond_size,
|
||||
bondless_taker: order.bondless_taker,
|
||||
};
|
||||
apiClient.post(baseUrl, '/api/make/', body).then((data: any) => {
|
||||
if (data.bad_request) {
|
||||
setBadOrder(data.bad_request);
|
||||
} else if (data.id) {
|
||||
history.push('/order/' + data.id);
|
||||
setCurrentOrder(data.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const startAgain = function () {
|
||||
history.push('/robot');
|
||||
setPage('robot');
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
{order == undefined && badOrder == undefined ? <CircularProgress /> : null}
|
||||
{badOrder != undefined ? (
|
||||
<Typography align='center' variant='subtitle2' color='secondary'>
|
||||
{t(badOrder)}
|
||||
</Typography>
|
||||
) : null}
|
||||
{order != undefined && badOrder == undefined ? (
|
||||
order.is_participant ? (
|
||||
windowSize.width > doublePageWidth ? (
|
||||
// DOUBLE PAPER VIEW
|
||||
<Grid
|
||||
container
|
||||
direction='row'
|
||||
justifyContent='center'
|
||||
alignItems='flex-start'
|
||||
spacing={2}
|
||||
style={{ width: '43em' }}
|
||||
>
|
||||
<Grid item xs={6} style={{ width: '21em' }}>
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{
|
||||
width: '21em',
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
setPage={setPage}
|
||||
hasRobot={hasRobot}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{ width: '21em' }}>
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{
|
||||
width: '21em',
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<TradeBox
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
setBadOrder={setBadOrder}
|
||||
baseUrl={baseUrl}
|
||||
onRenewOrder={renewOrder}
|
||||
onStartAgain={startAgain}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
// SINGLE PAPER VIEW
|
||||
<Box>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', width: '21em' }}>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(mouseEvent, value) => setTab(value)}
|
||||
variant='fullWidth'
|
||||
>
|
||||
<Tab label={t('Order')} value='order' />
|
||||
<Tab label={t('Contract')} value='contract' />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{
|
||||
width: '21em',
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: tab == 'order' ? '' : 'none' }}>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
setPage={setPage}
|
||||
hasRobot={hasRobot}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: tab == 'contract' ? '' : 'none' }}>
|
||||
<TradeBox
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
setBadOrder={setBadOrder}
|
||||
baseUrl={baseUrl}
|
||||
onRenewOrder={renewOrder}
|
||||
onStartAgain={startAgain}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{
|
||||
width: '21em',
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
setPage={setPage}
|
||||
hasRobot={hasRobot}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderPage;
|
@ -89,7 +89,7 @@ class UserGenPage extends Component {
|
||||
nickname: data.nickname ?? this.props.robot.nickname,
|
||||
activeOrderId: data.active_order_id ?? null,
|
||||
referralCode: data.referral_code ?? this.props.referralCode,
|
||||
earnedRewards: data.earned_rewards ?? this.props.eartnedRewards,
|
||||
earnedRewards: data.earned_rewards ?? this.props.earnedRewards,
|
||||
lastOrderId: data.last_order_id ?? this.props.lastOrderId,
|
||||
stealthInvoices: data.wants_stealth ?? this.props.stealthInvoices,
|
||||
tgEnabled: data.tg_enabled,
|
||||
@ -99,7 +99,7 @@ class UserGenPage extends Component {
|
||||
: this.props.setRobot({
|
||||
...this.props.robot,
|
||||
nickname: data.nickname,
|
||||
token: token,
|
||||
token,
|
||||
loading: false,
|
||||
activeOrderId: data.active_order_id ? data.active_order_id : null,
|
||||
lastOrderId: data.last_order_id ? data.last_order_id : null,
|
||||
@ -126,9 +126,9 @@ class UserGenPage extends Component {
|
||||
apiClient.delete(this.props.baseUrl, '/api/user');
|
||||
|
||||
systemClient.deleteCookie('sessionid');
|
||||
systemClient.deleteCookie('robot_token');
|
||||
systemClient.deleteCookie('pub_key');
|
||||
systemClient.deleteCookie('enc_priv_key');
|
||||
systemClient.deleteItem('robot_token');
|
||||
systemClient.deleteItem('pub_key');
|
||||
systemClient.deleteItem('enc_priv_key');
|
||||
}
|
||||
|
||||
handleClickNewRandomToken = () => {
|
||||
|
@ -68,7 +68,6 @@ const ProfileDialog = ({
|
||||
const host = getHost();
|
||||
|
||||
const [rewardInvoice, setRewardInvoice] = useState<string>('');
|
||||
const [showRewards, setShowRewards] = useState<boolean>(false);
|
||||
const [showRewardsSpinner, setShowRewardsSpinner] = useState<boolean>(false);
|
||||
const [withdrawn, setWithdrawn] = useState<boolean>(false);
|
||||
const [badInvoice, setBadInvoice] = useState<string>('');
|
||||
@ -80,7 +79,7 @@ const ProfileDialog = ({
|
||||
getWebln().then((webln) => {
|
||||
setWeblnEnabled(webln !== undefined);
|
||||
});
|
||||
}, [showRewards]);
|
||||
}, []);
|
||||
|
||||
const copyTokenHandler = () => {
|
||||
const robotToken = systemClient.getItem('robot_token');
|
||||
@ -326,28 +325,6 @@ const ProfileDialog = ({
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<BitcoinIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText>
|
||||
<FormControlLabel
|
||||
labelPlacement='end'
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('Rewards and compensations')}
|
||||
</div>
|
||||
}
|
||||
control={
|
||||
<Switch checked={showRewards} onChange={() => setShowRewards(!showRewards)} />
|
||||
}
|
||||
/>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
{showRewards && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PersonAddAltIcon />
|
||||
@ -360,11 +337,7 @@ const ProfileDialog = ({
|
||||
size='small'
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Tooltip
|
||||
disableHoverListener
|
||||
enterTouchDelay={0}
|
||||
title={t('Copied!') || ''}
|
||||
>
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!') || ''}>
|
||||
<IconButton onClick={copyReferralCodeHandler}>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
@ -462,8 +435,6 @@ const ProfileDialog = ({
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -5,13 +5,12 @@ import { GoldIcon, EarthIcon } from '.';
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
}
|
||||
|
||||
const FlagWithProps = ({ code }: Props): JSX.Element => {
|
||||
const defaultProps = {
|
||||
width: '1.428em',
|
||||
height: '1.428em',
|
||||
};
|
||||
const FlagWithProps = ({ code, width = '1.428em', height = '1.428em' }: Props): JSX.Element => {
|
||||
const defaultProps = { width, height };
|
||||
|
||||
let flag: JSX.Element | null = null;
|
||||
|
||||
|
364
frontend/src/components/Notifications/index.tsx
Normal file
364
frontend/src/components/Notifications/index.tsx
Normal file
@ -0,0 +1,364 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { StringIfPlural, useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Tooltip,
|
||||
Alert,
|
||||
useTheme,
|
||||
IconButton,
|
||||
TooltipProps,
|
||||
styled,
|
||||
tooltipClasses,
|
||||
} from '@mui/material';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Order } from '../../models';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
import { Page } from '../../basic/NavBar';
|
||||
|
||||
interface NotificationsProps {
|
||||
order: Order | undefined;
|
||||
rewards: number | undefined;
|
||||
page: Page;
|
||||
setPage: (state: Page) => void;
|
||||
openProfile: () => void;
|
||||
windowWidth: number;
|
||||
}
|
||||
|
||||
interface NotificationMessage {
|
||||
title: string;
|
||||
severity: 'error' | 'warning' | 'info' | 'success';
|
||||
onClick: () => void;
|
||||
sound: HTMLAudioElement | undefined;
|
||||
timeout: number;
|
||||
pageTitle: String;
|
||||
}
|
||||
|
||||
const audio = {
|
||||
chat: new Audio(`/static/assets/sounds/chat-open.mp3`),
|
||||
takerFound: new Audio(`/static/assets/sounds/taker-found.mp3`),
|
||||
ding: new Audio(`/static/assets/sounds/locked-invoice.mp3`),
|
||||
successful: new Audio(`/static/assets/sounds/successful.mp3`),
|
||||
};
|
||||
|
||||
const emptyNotificationMessage: NotificationMessage = {
|
||||
title: '',
|
||||
severity: 'info',
|
||||
onClick: () => null,
|
||||
sound: undefined,
|
||||
timeout: 1000,
|
||||
pageTitle: 'RoboSats - Simple and Private Bitcoin Exchange',
|
||||
};
|
||||
|
||||
const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))(({ theme }) => ({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
backgroundColor: 'rgb(0,0,0,0)',
|
||||
boxShadow: theme.shadows[1],
|
||||
borderRadius: '0.3em',
|
||||
padding: '0',
|
||||
},
|
||||
}));
|
||||
|
||||
const Notifications = ({
|
||||
order,
|
||||
rewards,
|
||||
page,
|
||||
setPage,
|
||||
windowWidth,
|
||||
openProfile,
|
||||
}: NotificationsProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
|
||||
const [message, setMessage] = useState<NotificationMessage>(emptyNotificationMessage);
|
||||
const [inFocus, setInFocus] = useState<boolean>(true);
|
||||
const [titleAnimation, setTitleAnimation] = useState<NodeJS.Timer | undefined>(undefined);
|
||||
const [show, setShow] = useState<boolean>(false);
|
||||
|
||||
// Keep last values to trigger effects on change
|
||||
const [oldOrderStatus, setOldOrderStatus] = useState<number | undefined>(undefined);
|
||||
const [oldRewards, setOldRewards] = useState<number>(0);
|
||||
const [oldChatIndex, setOldChatIndex] = useState<number>(0);
|
||||
|
||||
const position = windowWidth > 60 ? { top: '4em', right: '0em' } : { top: '0.5em', left: '50%' };
|
||||
const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange');
|
||||
|
||||
const moveToOrderPage = function () {
|
||||
setPage('order');
|
||||
history.push(`/order/${order?.id}`);
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
interface MessagesProps {
|
||||
bondLocked: NotificationMessage;
|
||||
escrowLocked: NotificationMessage;
|
||||
taken: NotificationMessage;
|
||||
expired: NotificationMessage;
|
||||
chat: NotificationMessage;
|
||||
successful: NotificationMessage;
|
||||
routingFailed: NotificationMessage;
|
||||
dispute: NotificationMessage;
|
||||
disputeWinner: NotificationMessage;
|
||||
disputeLoser: NotificationMessage;
|
||||
rewards: NotificationMessage;
|
||||
chatMessage: NotificationMessage;
|
||||
}
|
||||
|
||||
const Messages: MessagesProps = {
|
||||
bondLocked: {
|
||||
title: t(`${order?.is_maker ? 'Maker' : 'Taker'} bond locked`),
|
||||
severity: 'info',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.ding,
|
||||
timeout: 10000,
|
||||
pageTitle: `${t('✅ Bond!')} - ${basePageTitle}`,
|
||||
},
|
||||
escrowLocked: {
|
||||
title: t(`Order collateral locked`),
|
||||
severity: 'info',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.ding,
|
||||
timeout: 10000,
|
||||
pageTitle: `${t('✅ Escrow!')} - ${basePageTitle}`,
|
||||
},
|
||||
taken: {
|
||||
title: t('Order has been taken!'),
|
||||
severity: 'success',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.takerFound,
|
||||
timeout: 30000,
|
||||
pageTitle: `${t('🥳 Taken!')} - ${basePageTitle}`,
|
||||
},
|
||||
expired: {
|
||||
title: t('Order has expired'),
|
||||
severity: 'warning',
|
||||
onClick: moveToOrderPage,
|
||||
sound: undefined,
|
||||
timeout: 30000,
|
||||
pageTitle: `${t('😪 Expired!')} - ${basePageTitle}`,
|
||||
},
|
||||
chat: {
|
||||
title: t('Order chat is open'),
|
||||
severity: 'info',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.chat,
|
||||
timeout: 30000,
|
||||
pageTitle: `${t('💬 Chat!')} - ${basePageTitle}`,
|
||||
},
|
||||
successful: {
|
||||
title: t('Trade finished successfully!'),
|
||||
severity: 'success',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.successful,
|
||||
timeout: 10000,
|
||||
pageTitle: `${t('🙌 Funished!')} - ${basePageTitle}`,
|
||||
},
|
||||
routingFailed: {
|
||||
title: t('Lightning routing failed'),
|
||||
severity: 'warning',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.ding,
|
||||
timeout: 20000,
|
||||
pageTitle: `${t('❗⚡ Routing Failed')} - ${basePageTitle}`,
|
||||
},
|
||||
dispute: {
|
||||
title: t('Order has been disputed'),
|
||||
severity: 'warning',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.ding,
|
||||
timeout: 40000,
|
||||
pageTitle: `${t('⚖️ Disputed!')} - ${basePageTitle}`,
|
||||
},
|
||||
disputeWinner: {
|
||||
title: t('You won the dispute'),
|
||||
severity: 'success',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.ding,
|
||||
timeout: 30000,
|
||||
pageTitle: `${t('👍 dispute')} - ${basePageTitle}`,
|
||||
},
|
||||
disputeLoser: {
|
||||
title: t('You lost the dispute'),
|
||||
severity: 'error',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.ding,
|
||||
timeout: 30000,
|
||||
pageTitle: `${t('👎 dispute')} - ${basePageTitle}`,
|
||||
},
|
||||
rewards: {
|
||||
title: t('You can claim Sats!'),
|
||||
severity: 'success',
|
||||
onClick: () => {
|
||||
openProfile();
|
||||
setShow(false);
|
||||
},
|
||||
sound: audio.ding,
|
||||
timeout: 300000,
|
||||
pageTitle: `${t('₿ Rewards!')} - ${basePageTitle}`,
|
||||
},
|
||||
chatMessage: {
|
||||
title: t('New chat message'),
|
||||
severity: 'info',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.chat,
|
||||
timeout: 3000,
|
||||
pageTitle: `${t('💬 message!')} - ${basePageTitle}`,
|
||||
},
|
||||
};
|
||||
|
||||
const notify = function (message: NotificationMessage) {
|
||||
if (message.title != '') {
|
||||
setMessage(message);
|
||||
setShow(true);
|
||||
setTimeout(() => setShow(false), message.timeout);
|
||||
if (message.sound) {
|
||||
message.sound.play();
|
||||
}
|
||||
if (!inFocus) {
|
||||
setTitleAnimation(
|
||||
setInterval(function () {
|
||||
var title = document.title;
|
||||
document.title = title == basePageTitle ? message.pageTitle : basePageTitle;
|
||||
}, 1000),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = function (oldStatus: number | undefined, status: number) {
|
||||
let message = emptyNotificationMessage;
|
||||
|
||||
// Order status descriptions:
|
||||
// 0: 'Waiting for maker bond'
|
||||
// 1: 'Public'
|
||||
// 2: 'Paused'
|
||||
// 3: 'Waiting for taker bond'
|
||||
// 5: 'Expired'
|
||||
// 6: 'Waiting for trade collateral and buyer invoice'
|
||||
// 7: 'Waiting only for seller trade collateral'
|
||||
// 8: 'Waiting only for buyer invoice'
|
||||
// 9: 'Sending fiat - In chatroom'
|
||||
// 10: 'Fiat sent - In chatroom'
|
||||
// 11: 'In dispute'
|
||||
// 12: 'Collaboratively cancelled'
|
||||
// 13: 'Sending satoshis to buyer'
|
||||
// 14: 'Sucessful trade'
|
||||
// 15: 'Failed lightning network routing'
|
||||
// 16: 'Wait for dispute resolution'
|
||||
// 17: 'Maker lost dispute'
|
||||
// 18: 'Taker lost dispute'
|
||||
|
||||
if (status == 5 && oldStatus != 5) {
|
||||
message = Messages.expired;
|
||||
} else if (oldStatus == undefined) {
|
||||
message = emptyNotificationMessage;
|
||||
} else if (order?.is_maker && status > 0 && oldStatus == 0) {
|
||||
message = Messages.bondLocked;
|
||||
} else if (order?.is_taker && status > 5 && oldStatus <= 5) {
|
||||
message = Messages.bondLocked;
|
||||
} else if (order?.is_maker && status > 5 && oldStatus <= 5) {
|
||||
message = Messages.taken;
|
||||
} else if (order?.is_seller && status > 7 && oldStatus < 7) {
|
||||
message = Messages.escrowLocked;
|
||||
} else if ([9, 10].includes(status) && oldStatus < 9) {
|
||||
console.log('yoooo');
|
||||
message = Messages.chat;
|
||||
} else if (order?.is_seller && [13, 14, 15].includes(status) && oldStatus < 13) {
|
||||
message = Messages.successful;
|
||||
} else if (order?.is_buyer && status == 14 && oldStatus != 14) {
|
||||
message = Messages.successful;
|
||||
} else if (order?.is_buyer && status == 15 && oldStatus < 14) {
|
||||
message = Messages.routingFailed;
|
||||
} else if (status == 11 && oldStatus < 11) {
|
||||
message = Messages.dispute;
|
||||
} else if (status == 11 && oldStatus < 11) {
|
||||
message = Messages.dispute;
|
||||
} else if (
|
||||
((order?.is_maker && status == 18) || (order?.is_taker && status == 17)) &&
|
||||
oldStatus < 17
|
||||
) {
|
||||
message = Messages.disputeWinner;
|
||||
} else if (
|
||||
((order?.is_maker && status == 17) || (order?.is_taker && status == 18)) &&
|
||||
oldStatus < 17
|
||||
) {
|
||||
message = Messages.disputeLoser;
|
||||
}
|
||||
|
||||
notify(message);
|
||||
};
|
||||
|
||||
// Notify on order status change
|
||||
useEffect(() => {
|
||||
if (order != undefined && order.status != oldOrderStatus) {
|
||||
handleStatusChange(oldOrderStatus, order.status);
|
||||
setOldOrderStatus(order.status);
|
||||
} else if (order != undefined && order.chat_last_index > oldChatIndex) {
|
||||
if (page != 'order') {
|
||||
notify(Messages.chatMessage);
|
||||
}
|
||||
setOldChatIndex(order.chat_last_index);
|
||||
}
|
||||
}, [order]);
|
||||
|
||||
// Notify on rewards change
|
||||
useEffect(() => {
|
||||
if (rewards != undefined) {
|
||||
if (rewards > oldRewards) {
|
||||
notify(Messages.rewards);
|
||||
}
|
||||
setOldRewards(rewards);
|
||||
}
|
||||
}, [rewards]);
|
||||
|
||||
// Set blinking page title and clear on visibility change > infocus
|
||||
useEffect(() => {
|
||||
if (titleAnimation != undefined && inFocus) {
|
||||
clearInterval(titleAnimation);
|
||||
}
|
||||
}, [inFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.hidden) {
|
||||
setInFocus(false);
|
||||
} else if (!document.hidden) {
|
||||
setInFocus(true);
|
||||
document.title = basePageTitle;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledTooltip
|
||||
open={show}
|
||||
style={{ padding: 0, backgroundColor: 'black' }}
|
||||
placement={windowWidth > 60 ? 'left' : 'bottom'}
|
||||
title={
|
||||
<Alert
|
||||
severity={message.severity}
|
||||
action={
|
||||
<IconButton
|
||||
color='inherit'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
>
|
||||
<Close fontSize='inherit' />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<div style={{ cursor: 'pointer' }} onClick={message.onClick}>
|
||||
{message.title}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
>
|
||||
<div style={{ ...position, visibility: 'hidden', position: 'absolute' }} />
|
||||
</StyledTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notifications;
|
@ -8,7 +8,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const LinearDeterminate = ({ expiresAt, totalSecsExp }: Props): JSX.Element => {
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [progress, setProgress] = useState<number>(100);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@ -25,7 +25,12 @@ const LinearDeterminate = ({ expiresAt, totalSecsExp }: Props): JSX.Element => {
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<LinearProgress variant='determinate' value={progress} />
|
||||
<LinearProgress
|
||||
sx={{ height: '0.4em' }}
|
||||
variant='determinate'
|
||||
value={progress}
|
||||
color={progress < 20 ? 'secondary' : 'primary'}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
294
frontend/src/components/OrderDetails/TakeButton.tsx
Normal file
294
frontend/src/components/OrderDetails/TakeButton.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Box,
|
||||
Button,
|
||||
Tooltip,
|
||||
Grid,
|
||||
TextField,
|
||||
useTheme,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
import Countdown from 'react-countdown';
|
||||
|
||||
import currencies from '../../../static/assets/currencies.json';
|
||||
import { apiClient } from '../../services/api';
|
||||
|
||||
import { Order } from '../../models';
|
||||
import { ConfirmationDialog } from '../Dialogs';
|
||||
import { Page } from '../../basic/NavBar';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
interface TakeButtonProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
baseUrl: string;
|
||||
hasRobot: boolean;
|
||||
setPage: (state: Page) => void;
|
||||
}
|
||||
|
||||
interface OpenDialogsProps {
|
||||
inactiveMaker: boolean;
|
||||
confirmation: boolean;
|
||||
}
|
||||
const closeAll = { inactiveMaker: false, confirmation: false };
|
||||
|
||||
const TakeButton = ({
|
||||
order,
|
||||
setOrder,
|
||||
baseUrl,
|
||||
setPage,
|
||||
hasRobot,
|
||||
}: TakeButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const [takeAmount, setTakeAmount] = useState<string>('');
|
||||
const [badRequest, setBadRequest] = useState<string>('');
|
||||
const [loadingTake, setLoadingTake] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState<OpenDialogsProps>(closeAll);
|
||||
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
|
||||
const InactiveMakerDialog = function () {
|
||||
return (
|
||||
<Dialog open={open.inactiveMaker} onClose={() => setOpen({ ...open, inactiveMaker: false })}>
|
||||
<DialogTitle>{t('The maker is away')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{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.',
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(closeAll)} autoFocus>
|
||||
{t('Go back')}
|
||||
</Button>
|
||||
<Button onClick={() => setOpen({ inactiveMaker: false, confirmation: true })}>
|
||||
{t('Sounds fine')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const countdownTakeOrderRenderer = function ({ seconds, completed }) {
|
||||
if (isNaN(seconds) || completed) {
|
||||
return takeOrderButton();
|
||||
} else {
|
||||
return (
|
||||
<Tooltip enterTouchDelay={0} title={t('Wait until you can take an order')}>
|
||||
<Grid container sx={{ width: '100%' }} padding={1} justifyContent='center'>
|
||||
<LoadingButton loading={loadingTake} disabled={true} variant='outlined' color='primary'>
|
||||
{t('Take Order')}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTakeAmountChange = function (e) {
|
||||
if (e.target.value != '' && e.target.value != null) {
|
||||
setTakeAmount(`${parseFloat(e.target.value)}`);
|
||||
} else {
|
||||
setTakeAmount(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const amountHelperText = function () {
|
||||
if (Number(takeAmount) < Number(order.min_amount) && takeAmount != '') {
|
||||
return t('Too low');
|
||||
} else if (Number(takeAmount) > Number(order.max_amount) && takeAmount != '') {
|
||||
return t('Too high');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onTakeOrderClicked = function () {
|
||||
if (order.maker_status == 'Inactive') {
|
||||
setOpen({ inactiveMaker: true, confirmation: false });
|
||||
} else {
|
||||
setOpen({ inactiveMaker: false, confirmation: true });
|
||||
}
|
||||
};
|
||||
|
||||
const invalidTakeAmount = function () {
|
||||
return (
|
||||
Number(takeAmount) < Number(order.min_amount) ||
|
||||
Number(takeAmount) > Number(order.max_amount) ||
|
||||
takeAmount == '' ||
|
||||
takeAmount == null
|
||||
);
|
||||
};
|
||||
|
||||
const takeOrderButton = function () {
|
||||
if (order.has_range) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
padding: '0.5em',
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderRadius: '4px',
|
||||
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#2f2f2f',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid container direction='row' alignItems='flex-start' justifyContent='space-evenly'>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={500}
|
||||
enterDelay={700}
|
||||
enterNextDelay={2000}
|
||||
title={t('Enter amount of fiat to exchange for bitcoin')}
|
||||
>
|
||||
<TextField
|
||||
error={
|
||||
(Number(takeAmount) < Number(order.min_amount) ||
|
||||
Number(takeAmount) > Number(order.max_amount)) &&
|
||||
takeAmount != ''
|
||||
}
|
||||
helperText={amountHelperText()}
|
||||
label={t('Amount {{currencyCode}}', { currencyCode })}
|
||||
size='small'
|
||||
type='number'
|
||||
required={true}
|
||||
value={takeAmount}
|
||||
inputProps={{
|
||||
min: order.min_amount,
|
||||
max: order.max_amount,
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={handleTakeAmountChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<div
|
||||
style={{
|
||||
display: invalidTakeAmount() ? '' : 'none',
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={1200}
|
||||
title={t('You must specify an amount first')}
|
||||
>
|
||||
<div>
|
||||
<LoadingButton
|
||||
loading={loadingTake}
|
||||
sx={{ height: '2.8em' }}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
disabled={true}
|
||||
>
|
||||
{t('Take Order')}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: invalidTakeAmount() ? 'none' : '',
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
loading={loadingTake}
|
||||
sx={{ height: '2.8em' }}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={onTakeOrderClicked}
|
||||
>
|
||||
{t('Take Order')}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
bottom: '0.25em',
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
loading={loadingTake}
|
||||
sx={{ height: '2.71em' }}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={onTakeOrderClicked}
|
||||
>
|
||||
{t('Take Order')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const takeOrder = function () {
|
||||
setLoadingTake(true);
|
||||
apiClient
|
||||
.post(baseUrl, '/api/order/?order_id=' + order.id, {
|
||||
action: 'take',
|
||||
amount: takeAmount,
|
||||
})
|
||||
.then((data) => {
|
||||
setLoadingTake(false);
|
||||
if (data.bad_request) {
|
||||
setBadRequest(data.bad_request);
|
||||
} else {
|
||||
setOrder(data);
|
||||
setBadRequest('');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Countdown date={new Date(order.penalty)} renderer={countdownTakeOrderRenderer} />
|
||||
{badRequest != '' ? (
|
||||
<Box style={{ padding: '0.5em' }}>
|
||||
<Typography align='center' color='secondary'>
|
||||
{t(badRequest)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<ConfirmationDialog
|
||||
open={open.confirmation}
|
||||
onClose={() => setOpen({ ...open, confirmation: false })}
|
||||
setPage={setPage}
|
||||
onClickDone={() => {
|
||||
takeOrder();
|
||||
setLoadingTake(true);
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
hasRobot={hasRobot}
|
||||
/>
|
||||
<InactiveMakerDialog />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TakeButton;
|
334
frontend/src/components/OrderDetails/index.tsx
Normal file
334
frontend/src/components/OrderDetails/index.tsx
Normal file
@ -0,0 +1,334 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Alert,
|
||||
Chip,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Divider,
|
||||
Grid,
|
||||
Collapse,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
|
||||
import Countdown, { CountdownRenderProps, zeroPad } from 'react-countdown';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
|
||||
import currencies from '../../../static/assets/currencies.json';
|
||||
import {
|
||||
AccessTime,
|
||||
Numbers,
|
||||
PriceChange,
|
||||
Payments,
|
||||
Article,
|
||||
HourglassTop,
|
||||
} from '@mui/icons-material';
|
||||
import { PaymentStringAsIcons } from '../../components/PaymentMethods';
|
||||
import { FlagWithProps } from '../Icons';
|
||||
import LinearDeterminate from './LinearDeterminate';
|
||||
|
||||
import { Order } from '../../models';
|
||||
import { statusBadgeColor, pn } from '../../utils';
|
||||
import { Page } from '../../basic/NavBar';
|
||||
import TakeButton from './TakeButton';
|
||||
|
||||
interface OrderDetailsProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
baseUrl: string;
|
||||
hasRobot: boolean;
|
||||
setPage: (state: Page) => void;
|
||||
}
|
||||
|
||||
const OrderDetails = ({
|
||||
order,
|
||||
setOrder,
|
||||
baseUrl,
|
||||
setPage,
|
||||
hasRobot,
|
||||
}: OrderDetailsProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
|
||||
const AmountString = function () {
|
||||
// precision to 8 decimal if currency is BTC otherwise 4 decimals
|
||||
const precision = order.currency == 1000 ? 8 : 4;
|
||||
|
||||
let primary = '';
|
||||
let secondary = '';
|
||||
if (order.has_range && order.amount == null) {
|
||||
const minAmount = pn(parseFloat(Number(order.min_amount).toPrecision(precision)));
|
||||
const maxAmount = pn(parseFloat(Number(order.max_amount).toPrecision(precision)));
|
||||
primary = `${minAmount}-${maxAmount} ${currencyCode}`;
|
||||
secondary = t('Amount range');
|
||||
} else {
|
||||
const amount = pn(parseFloat(Number(order.amount).toPrecision(precision)));
|
||||
primary = `${amount} ${currencyCode}`;
|
||||
secondary = t('Amount');
|
||||
}
|
||||
return { primary, secondary };
|
||||
};
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
const countdownRenderer = function ({
|
||||
total,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
completed,
|
||||
}: CountdownRenderProps) {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return <span> {t('The order has expired')}</span>;
|
||||
} else {
|
||||
let col = 'inherit';
|
||||
const fraction_left = total / 1000 / order.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 ? (
|
||||
<b>
|
||||
<span style={{ color: col }}>
|
||||
{`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `}
|
||||
</span>
|
||||
</b>
|
||||
) : (
|
||||
<span style={{ color: col }}>{`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `}</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const timerRenderer = function (seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds - hours * 3600) / 60);
|
||||
return (
|
||||
<span>
|
||||
{hours > 0 ? hours + 'h' : ''} {minutes > 0 ? zeroPad(minutes) + 'm' : ''}{' '}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
const countdownPenaltyRenderer = function ({ minutes, seconds, completed }) {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return <span> {t('Penalty lifted, good to go!')}</span>;
|
||||
} else {
|
||||
return (
|
||||
<span>
|
||||
{' '}
|
||||
{t('You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s', {
|
||||
timeMin: zeroPad(minutes),
|
||||
timeSec: zeroPad(seconds),
|
||||
})}{' '}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={0}>
|
||||
<Grid item xs={12}>
|
||||
<List dense={true}>
|
||||
<ListItem>
|
||||
<ListItemAvatar sx={{ width: '4em', height: '4em' }}>
|
||||
<RobotAvatar
|
||||
statusColor={statusBadgeColor(order.maker_status)}
|
||||
nickname={order.maker_nick}
|
||||
tooltip={t(order.maker_status)}
|
||||
orderType={order.type}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={order.maker_nick + (order.type ? ' ' + t('(Seller)') : ' ' + t('(Buyer)'))}
|
||||
secondary={t('Order maker')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Collapse in={order.is_participant && order.taker_nick !== 'None'}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={`${order.taker_nick} ${order.type ? t('(Buyer)') : t('(Seller)')}`}
|
||||
secondary={t('Order taker')}
|
||||
/>
|
||||
<ListItemAvatar>
|
||||
<RobotAvatar
|
||||
avatarClass='smallAvatar'
|
||||
statusColor={statusBadgeColor(order.taker_status)}
|
||||
nickname={order.taker_nick == 'None' ? undefined : order.taker_nick}
|
||||
tooltip={t(order.taker_status)}
|
||||
orderType={order.type === 0 ? 1 : 0}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</ListItem>
|
||||
</Collapse>
|
||||
|
||||
<Divider>
|
||||
<Chip label={t('Order Details')} />
|
||||
</Divider>
|
||||
|
||||
<Collapse in={order.is_participant}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t(order.status_message)} secondary={t('Order status')} />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Collapse>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<div
|
||||
style={{
|
||||
zoom: 1.25,
|
||||
opacity: 0.7,
|
||||
msZoom: 1.25,
|
||||
WebkitZoom: 1.25,
|
||||
MozTransform: 'scale(1.25,1.25)',
|
||||
MozTransformOrigin: 'left center',
|
||||
}}
|
||||
>
|
||||
<FlagWithProps code={currencyCode} width='1.2em' height='1.2em' />
|
||||
</div>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={AmountString().primary} secondary={AmountString().secondary} />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Payments />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<PaymentStringAsIcons
|
||||
size={1.42 * theme.typography.fontSize}
|
||||
othersText={t('Others')}
|
||||
verbose={true}
|
||||
text={order.payment_method}
|
||||
/>
|
||||
}
|
||||
secondary={
|
||||
order.currency == 1000 ? t('Swap destination') : t('Accepted payment methods')
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
{/* If there is live Price and Premium data, show it. Otherwise show the order maker settings */}
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PriceChange />
|
||||
</ListItemIcon>
|
||||
|
||||
{order.price_now !== undefined ? (
|
||||
<ListItemText
|
||||
primary={t('{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%', {
|
||||
price: pn(order.price_now),
|
||||
currencyCode,
|
||||
premium: order.premium_now,
|
||||
})}
|
||||
secondary={t('Price and Premium')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!order.price_now && order.is_explicit ? (
|
||||
<ListItemText primary={pn(order.satoshis)} secondary={t('Amount of Satoshis')} />
|
||||
) : null}
|
||||
|
||||
{!order.price_now && !order.is_explicit ? (
|
||||
<ListItemText
|
||||
primary={parseFloat(Number(order.premium).toFixed(2)) + '%'}
|
||||
secondary={t('Premium over market price')}
|
||||
/>
|
||||
) : null}
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Numbers />
|
||||
</ListItemIcon>
|
||||
<Grid container>
|
||||
<Grid item xs={4.5}>
|
||||
<ListItemText primary={order.id} secondary={t('Order ID')} />
|
||||
</Grid>
|
||||
<Grid item xs={7.5}>
|
||||
<Grid container>
|
||||
<Grid item xs={2}>
|
||||
<ListItemIcon sx={{ position: 'relative', top: '12px', left: '-5px' }}>
|
||||
<HourglassTop />
|
||||
</ListItemIcon>
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<ListItemText
|
||||
primary={timerRenderer(order.escrow_duration)}
|
||||
secondary={t('Deposit timer')}
|
||||
></ListItemText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
|
||||
{/* if order is in a status that does not expire, do not show countdown */}
|
||||
<Collapse in={![4, 5, 12, 13, 14, 15, 16, 17, 18].includes(order.status)}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AccessTime />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Expires in')}>
|
||||
<Countdown date={new Date(order.expires_at)} renderer={countdownRenderer} />
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
<LinearDeterminate totalSecsExp={order.total_secs_exp} expiresAt={order.expires_at} />
|
||||
</Collapse>
|
||||
</List>
|
||||
|
||||
{/* If the user has a penalty/limit */}
|
||||
{order.penalty !== undefined ? (
|
||||
<Grid item xs={12}>
|
||||
<Alert severity='warning' sx={{ borderRadius: '0' }}>
|
||||
<Countdown date={new Date(order.penalty)} renderer={countdownPenaltyRenderer} />
|
||||
</Alert>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{!order.is_participant ? (
|
||||
<Grid item xs={12}>
|
||||
<TakeButton
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
setPage={setPage}
|
||||
hasRobot={hasRobot}
|
||||
/>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderDetails;
|
@ -7,7 +7,7 @@ import { apiClient } from '../../services/api';
|
||||
import placeholder from './placeholder.json';
|
||||
|
||||
interface Props {
|
||||
nickname: string | null;
|
||||
nickname: string | undefined;
|
||||
smooth?: boolean;
|
||||
flipHorizontally?: boolean;
|
||||
style?: object;
|
||||
@ -40,7 +40,7 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
const [avatarSrc, setAvatarSrc] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (nickname != null) {
|
||||
if (nickname != undefined) {
|
||||
if (window.NativeRobosats === undefined) {
|
||||
setAvatarSrc(baseUrl + '/static/assets/avatars/' + nickname + '.png');
|
||||
} else {
|
||||
|
@ -7,19 +7,19 @@ import Flags from 'country-flag-icons/react/3x2';
|
||||
import { CataloniaFlag, BasqueCountryFlag } from '../Icons';
|
||||
|
||||
const menuLanuguages = [
|
||||
{ name: 'English', i18nCode: 'en', flag: Flags['US'] },
|
||||
{ name: 'Español', i18nCode: 'es', flag: Flags['ES'] },
|
||||
{ name: 'Deutsch', i18nCode: 'de', flag: Flags['DE'] },
|
||||
{ name: 'Polski', i18nCode: 'pl', flag: Flags['PL'] },
|
||||
{ name: 'Français', i18nCode: 'fr', flag: Flags['FR'] },
|
||||
{ name: 'Русский', i18nCode: 'ru', flag: Flags['RU'] },
|
||||
{ name: 'Italiano', i18nCode: 'it', flag: Flags['IT'] },
|
||||
{ name: 'Português', i18nCode: 'pt', flag: Flags['BR'] },
|
||||
{ name: '简体', i18nCode: 'zh-si', flag: Flags['CN'] },
|
||||
{ name: '繁體', i18nCode: 'zh-tr', flag: Flags['CN'] },
|
||||
{ name: 'Svenska', i18nCode: 'sv', flag: Flags['SE'] },
|
||||
{ name: 'Čeština', i18nCode: 'cs', flag: Flags['CZ'] },
|
||||
{ name: 'ภาษาไทย', i18nCode: 'th', flag: Flags['TH'] },
|
||||
{ name: 'English', i18nCode: 'en', flag: Flags.US },
|
||||
{ name: 'Español', i18nCode: 'es', flag: Flags.ES },
|
||||
{ name: 'Deutsch', i18nCode: 'de', flag: Flags.DE },
|
||||
{ name: 'Polski', i18nCode: 'pl', flag: Flags.PL },
|
||||
{ name: 'Français', i18nCode: 'fr', flag: Flags.FR },
|
||||
{ name: 'Русский', i18nCode: 'ru', flag: Flags.RU },
|
||||
{ name: 'Italiano', i18nCode: 'it', flag: Flags.IT },
|
||||
{ name: 'Português', i18nCode: 'pt', flag: Flags.BR },
|
||||
{ name: '简体', i18nCode: 'zh-si', flag: Flags.CN },
|
||||
{ name: '繁體', i18nCode: 'zh-tr', flag: Flags.CN },
|
||||
{ name: 'Svenska', i18nCode: 'sv', flag: Flags.SE },
|
||||
{ name: 'Čeština', i18nCode: 'cs', flag: Flags.CZ },
|
||||
{ name: 'ภาษาไทย', i18nCode: 'th', flag: Flags.TH },
|
||||
{ name: 'Català', i18nCode: 'ca', flag: CataloniaFlag },
|
||||
{ name: 'Euskara', i18nCode: 'eu', flag: BasqueCountryFlag },
|
||||
];
|
||||
|
@ -1,29 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Typography, useTheme } from '@mui/material';
|
||||
import { Lock, LockOpen, Balance } from '@mui/icons-material';
|
||||
|
||||
interface BondStatusProps {
|
||||
status: 'locked' | 'settled' | 'returned' | 'hide';
|
||||
status: 'locked' | 'settled' | 'unlocked' | 'hide';
|
||||
isMaker: boolean;
|
||||
}
|
||||
|
||||
const BondStatus = ({ status, isMaker }: BondStatusProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
let Icon = Lock;
|
||||
if (status === 'returned') {
|
||||
let color = 'primary';
|
||||
if (status === 'unlocked') {
|
||||
Icon = LockOpen;
|
||||
color = theme.palette.mode == 'dark' ? 'lightgreen' : 'green';
|
||||
} else if (status === 'settled') {
|
||||
Icon = Balance;
|
||||
color = theme.palette.mode == 'dark' ? 'lightred' : 'red';
|
||||
}
|
||||
|
||||
if (status === 'hide') {
|
||||
return <></>;
|
||||
} else {
|
||||
return (
|
||||
<Box>
|
||||
<Typography color='primary' variant='subtitle1' align='center'>
|
||||
<Typography color={color} variant='subtitle1' align='center'>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@ -32,11 +35,10 @@ const BondStatus = ({ status, isMaker }: BondStatusProps): JSX.Element => {
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
<Icon sx={{ height: '0.9em', width: '0.9em' }} />
|
||||
{t(`Your ${isMaker ? 'maker' : 'taker'} bond is ${status}`)}
|
||||
</div>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
76
frontend/src/components/TradeBox/CancelButton.tsx
Normal file
76
frontend/src/components/TradeBox/CancelButton.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Tooltip } from '@mui/material';
|
||||
import { Order } from '../../models';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
interface CancelButtonProps {
|
||||
order: Order;
|
||||
onClickCancel: () => void;
|
||||
openCancelDialog: () => void;
|
||||
openCollabCancelDialog: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const CancelButton = ({
|
||||
order,
|
||||
onClickCancel,
|
||||
openCancelDialog,
|
||||
openCollabCancelDialog,
|
||||
loading = false,
|
||||
}: CancelButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showCancelButton =
|
||||
(order.is_maker && [0, 1, 2].includes(order.status)) || [3, 6, 7].includes(order.status);
|
||||
const showCollabCancelButton = [8, 9].includes(order.status) && !order.asked_for_cancel;
|
||||
const noConfirmation =
|
||||
(order.is_maker && [0, 1, 2].includes(order.status)) || (order.is_taker && order.status === 3);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{showCancelButton ? (
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={500}
|
||||
enterDelay={700}
|
||||
enterNextDelay={2000}
|
||||
title={
|
||||
noConfirmation
|
||||
? t('Cancel order and unlock bond instantly')
|
||||
: t('Unilateral cancelation (bond at risk!)')
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<LoadingButton
|
||||
size='small'
|
||||
loading={loading}
|
||||
variant='outlined'
|
||||
color='secondary'
|
||||
onClick={noConfirmation ? onClickCancel : openCancelDialog}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{showCollabCancelButton ? (
|
||||
<LoadingButton
|
||||
size='small'
|
||||
loading={loading}
|
||||
variant='outlined'
|
||||
color='secondary'
|
||||
onClick={openCollabCancelDialog}
|
||||
>
|
||||
{t('Collaborative Cancel')}
|
||||
</LoadingButton>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CancelButton;
|
30
frontend/src/components/TradeBox/CollabCancelAlert.tsx
Normal file
30
frontend/src/components/TradeBox/CollabCancelAlert.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert } from '@mui/material';
|
||||
import { Order } from '../../models';
|
||||
|
||||
interface CollabCancelAlertProps {
|
||||
order: Order;
|
||||
}
|
||||
|
||||
const CollabCancelAlert = ({ order }: CollabCancelAlertProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
let text = '';
|
||||
if (order.pending_cancel) {
|
||||
text = t('{{nickname}} is asking for a collaborative cancel', {
|
||||
nickname: order.is_maker ? order.taker_nick : order.maker_nick,
|
||||
});
|
||||
} else if (order.asked_for_cancel) {
|
||||
text = t('You asked for a collaborative cancellation');
|
||||
}
|
||||
|
||||
return text != '' ? (
|
||||
<Alert severity='warning' style={{ width: '100%' }}>
|
||||
{text}
|
||||
</Alert>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollabCancelAlert;
|
43
frontend/src/components/TradeBox/Dialogs/ConfirmCancel.tsx
Normal file
43
frontend/src/components/TradeBox/Dialogs/ConfirmCancel.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
|
||||
interface ConfirmCancelDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCancelClick: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmCancelDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onCancelClick,
|
||||
}: ConfirmCancelDialogProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('Cancel the order?')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('If the order is cancelled now you will lose your bond.')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} autoFocus>
|
||||
{t('Go back')}
|
||||
</Button>
|
||||
<Button onClick={onCancelClick}>{t('Confirm Cancel')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmCancelDialog;
|
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
interface ConfirmCollabCancelDialogProps {
|
||||
open: boolean;
|
||||
loading: Boolean;
|
||||
onClose: () => void;
|
||||
onCollabCancelClick: () => void;
|
||||
peerAskedCancel: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmCollabCancelDialog = ({
|
||||
open,
|
||||
loading,
|
||||
onClose,
|
||||
onCollabCancelClick,
|
||||
peerAskedCancel,
|
||||
}: ConfirmCollabCancelDialogProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle id='cancel-dialog-title'>{t('Collaborative cancel the order?')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id='cancel-dialog-description'>
|
||||
{t(
|
||||
'The trade escrow has been posted. The order can be cancelled only if both, maker and taker, agree to cancel.',
|
||||
)}
|
||||
{peerAskedCancel ? ` ${t('Your peer has asked for cancellation')}` : ''}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} autoFocus>
|
||||
{t('Go back')}
|
||||
</Button>
|
||||
<LoadingButton loading={loading} onClick={onCollabCancelClick}>
|
||||
{peerAskedCancel ? t('Accept Cancelation') : t('Ask for Cancel')}
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmCollabCancelDialog;
|
65
frontend/src/components/TradeBox/Dialogs/WebLN.tsx
Normal file
65
frontend/src/components/TradeBox/Dialogs/WebLN.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
Button,
|
||||
useTheme,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
|
||||
import { Check } from '@mui/icons-material';
|
||||
|
||||
interface WebLNDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
waitingWebln: boolean;
|
||||
isBuyer: boolean;
|
||||
}
|
||||
|
||||
export const WebLNDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
waitingWebln,
|
||||
isBuyer,
|
||||
}: WebLNDialogProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('WebLN')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{waitingWebln ? (
|
||||
<>
|
||||
<CircularProgress
|
||||
size={1.1 * theme.typography.fontSize}
|
||||
thickness={5}
|
||||
style={{ marginRight: '0.8em' }}
|
||||
/>
|
||||
{isBuyer
|
||||
? t('Invoice not received, please check your WebLN wallet.')
|
||||
: t('Payment not received, please check your WebLN wallet.')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check color='success' />
|
||||
{t('You can close now your WebLN wallet popup.')}
|
||||
</>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} autoFocus>
|
||||
{t('Done')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebLNDialog;
|
@ -1,2 +1,5 @@
|
||||
export { ConfirmDisputeDialog } from './ConfirmDispute';
|
||||
export { ConfirmFiatReceivedDialog } from './ConfirmFiatReceived';
|
||||
export { ConfirmCancelDialog } from './ConfirmCancel';
|
||||
export { ConfirmCollabCancelDialog } from './ConfirmCollabCancel';
|
||||
export { WebLNDialog } from './WebLN';
|
||||
|
@ -18,8 +18,15 @@ const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid item xs={6}>
|
||||
<Grid
|
||||
container
|
||||
sx={{ width: '18em' }}
|
||||
direction='row'
|
||||
justifyContent='space-evenly'
|
||||
alignItems='center'
|
||||
padding={0.3}
|
||||
>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
@ -28,13 +35,13 @@ const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile
|
||||
title={t('Verify your privacy')}
|
||||
>
|
||||
<Button size='small' color='primary' variant='outlined' onClick={() => setAudit(!audit)}>
|
||||
<KeyIcon />
|
||||
<KeyIcon sx={{ width: '0.8em', height: '0.8em' }} />
|
||||
{t('Audit PGP')}{' '}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Grid item>
|
||||
{window.ReactNativeWebView === undefined ? (
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
@ -71,7 +78,7 @@ const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile
|
||||
</Tooltip>
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,39 +1,86 @@
|
||||
import React from 'react';
|
||||
import { Grid, Paper, Typography, useTheme } from '@mui/material';
|
||||
import { Grid, Paper, Tooltip, IconButton, Typography, useTheme } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { WifiTetheringError } from '@mui/icons-material';
|
||||
|
||||
interface Props {
|
||||
connected: boolean;
|
||||
peerConnected: boolean;
|
||||
turtleMode: boolean;
|
||||
setTurtleMode: (state: boolean) => void;
|
||||
}
|
||||
|
||||
const ChatHeader: React.FC<Props> = ({ connected, peerConnected }) => {
|
||||
const ChatHeader: React.FC<Props> = ({ connected, peerConnected, turtleMode, setTurtleMode }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const connectedColor = theme.palette.mode === 'light' ? '#b5e3b7' : '#153717';
|
||||
const connectedTextColor = theme.palette.getContrastText(connectedColor);
|
||||
|
||||
return (
|
||||
<Grid container spacing={0.5}>
|
||||
<Grid item xs={0.3} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper elevation={1} sx={connected ? { backgroundColor: connectedColor } : {}}>
|
||||
<Typography variant='caption' sx={{ color: connectedTextColor }}>
|
||||
<Grid
|
||||
container
|
||||
direction='row'
|
||||
justifyContent='space-between'
|
||||
alignItems='flex-end'
|
||||
padding={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Paper
|
||||
style={{
|
||||
width: '7.2em',
|
||||
height: '1.8em',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
elevation={1}
|
||||
sx={connected ? { backgroundColor: connectedColor } : {}}
|
||||
>
|
||||
<Typography align='center' variant='caption' sx={{ color: connectedTextColor }}>
|
||||
{t('You') + ': '}
|
||||
{connected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.4} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper elevation={1} sx={peerConnected ? { backgroundColor: connectedColor } : {}}>
|
||||
<Typography variant='caption' sx={{ color: connectedTextColor }}>
|
||||
<Grid item>
|
||||
{window.ReactNativeWebView === undefined ? (
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
enterTouchDelay={0}
|
||||
placement='top'
|
||||
title={t('Activate slow mode (use it when the connection is slow)')}
|
||||
>
|
||||
<IconButton
|
||||
size='small'
|
||||
color={turtleMode ? 'primary' : 'inherit'}
|
||||
onClick={() => setTurtleMode(!turtleMode)}
|
||||
>
|
||||
<WifiTetheringError />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Paper
|
||||
style={{
|
||||
width: '7.2em',
|
||||
height: '1.8em',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
elevation={1}
|
||||
sx={peerConnected ? { backgroundColor: connectedColor } : {}}
|
||||
>
|
||||
<Typography align='center' variant='caption' sx={{ color: connectedTextColor }}>
|
||||
{t('Peer') + ': '}
|
||||
{peerConnected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.3} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
@ -18,20 +18,26 @@ import ChatBottom from '../ChatBottom';
|
||||
|
||||
interface Props {
|
||||
orderId: number;
|
||||
status: number;
|
||||
userNick: string;
|
||||
takerNick: string;
|
||||
messages: EncryptedChatMessage[];
|
||||
setMessages: (messages: EncryptedChatMessage[]) => void;
|
||||
baseUrl: string;
|
||||
turtleMode: boolean;
|
||||
setTurtleMode: (state: boolean) => void;
|
||||
}
|
||||
|
||||
const EncryptedSocketChat: React.FC<Props> = ({
|
||||
orderId,
|
||||
status,
|
||||
userNick,
|
||||
takerNick,
|
||||
messages,
|
||||
setMessages,
|
||||
baseUrl,
|
||||
turtleMode,
|
||||
setTurtleMode,
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@ -62,6 +68,13 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
// Make sure to not keep reconnecting once status is not Chat
|
||||
useEffect(() => {
|
||||
if (![9, 10].includes(status)) {
|
||||
connection?.close();
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > messageCount) {
|
||||
audio.play();
|
||||
@ -99,7 +112,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
encrypted_private_key: ownEncPrivKey,
|
||||
passphrase: token,
|
||||
},
|
||||
messages: messages,
|
||||
messages,
|
||||
};
|
||||
};
|
||||
|
||||
@ -111,7 +124,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
setPeerConnected(dataFromServer.peer_connected);
|
||||
// If we receive a public key other than ours (our peer key!)
|
||||
if (
|
||||
connection &&
|
||||
connection != null &&
|
||||
dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` &&
|
||||
dataFromServer.message != ownPubKey
|
||||
) {
|
||||
@ -158,7 +171,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
const existingMessage = prev.find(
|
||||
(item) => item.plainTextMessage === dataFromServer.message,
|
||||
);
|
||||
if (existingMessage) {
|
||||
if (existingMessage != null) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
@ -179,14 +192,14 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const onButtonClicked = (e: any) => {
|
||||
if (token && value.indexOf(token) !== -1) {
|
||||
if (token && value.includes(token)) {
|
||||
alert(
|
||||
`Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
|
||||
);
|
||||
setValue('');
|
||||
}
|
||||
// If input string contains '#' send unencrypted and unlogged message
|
||||
else if (connection && value.substring(0, 1) == '#') {
|
||||
else if (connection != null && value.substring(0, 1) == '#') {
|
||||
connection.send({
|
||||
message: value,
|
||||
nick: userNick,
|
||||
@ -201,7 +214,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
setLastSent(value);
|
||||
encryptMessage(value, ownPubKey, peerPubKey, ownEncPrivKey, token).then(
|
||||
(encryptedMessage) => {
|
||||
if (connection) {
|
||||
if (connection != null) {
|
||||
connection.send({
|
||||
message: encryptedMessage.toString().split('\n').join('\\'),
|
||||
nick: userNick,
|
||||
@ -214,15 +227,37 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Container component='main'>
|
||||
<ChatHeader connected={connected} peerConnected={peerConnected} />
|
||||
<div style={{ position: 'relative', left: '-0.14em', margin: '0 auto', width: '17.7em' }}>
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0.5}
|
||||
>
|
||||
<AuditPGPDialog
|
||||
open={audit}
|
||||
onClose={() => setAudit(false)}
|
||||
orderId={Number(orderId)}
|
||||
messages={messages}
|
||||
own_pub_key={ownPubKey || ''}
|
||||
own_enc_priv_key={ownEncPrivKey || ''}
|
||||
peer_pub_key={peerPubKey || 'Not received yet'}
|
||||
passphrase={token || ''}
|
||||
onClickBack={() => setAudit(false)}
|
||||
/>
|
||||
<Grid item>
|
||||
<ChatHeader
|
||||
connected={connected}
|
||||
peerConnected={peerConnected}
|
||||
turtleMode={turtleMode}
|
||||
setTurtleMode={setTurtleMode}
|
||||
/>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{
|
||||
height: '21.42em',
|
||||
maxHeight: '21.42em',
|
||||
width: '17.7em',
|
||||
height: '18.42em',
|
||||
maxHeight: '18.42em',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
@ -250,8 +285,8 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
/>
|
||||
</Paper>
|
||||
<form noValidate onSubmit={onButtonClicked}>
|
||||
<Grid alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid alignItems='stretch' style={{ display: 'flex', width: '100%' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={9}>
|
||||
<TextField
|
||||
label={t('Type a message')}
|
||||
variant='standard'
|
||||
@ -267,12 +302,12 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
sx={{ width: '13.7em' }}
|
||||
fullWidth={true}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}>
|
||||
<Button
|
||||
sx={{ width: '4.68em' }}
|
||||
fullWidth={true}
|
||||
disabled={!connected || waitingEcho || !peerPubKey}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
@ -304,23 +339,8 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style={{ height: '0.3em' }} />
|
||||
|
||||
<Grid container spacing={0}>
|
||||
<AuditPGPDialog
|
||||
open={audit}
|
||||
onClose={() => setAudit(false)}
|
||||
orderId={Number(orderId)}
|
||||
messages={messages}
|
||||
own_pub_key={ownPubKey || ''}
|
||||
own_enc_priv_key={ownEncPrivKey || ''}
|
||||
peer_pub_key={peerPubKey || 'Not received yet'}
|
||||
passphrase={token || ''}
|
||||
onClickBack={() => setAudit(false)}
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ChatBottom
|
||||
orderId={orderId}
|
||||
audit={audit}
|
||||
@ -328,7 +348,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
createJsonFile={createJsonFile}
|
||||
/>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -23,6 +23,8 @@ interface Props {
|
||||
messages: EncryptedChatMessage[];
|
||||
setMessages: (messages: EncryptedChatMessage[]) => void;
|
||||
baseUrl: string;
|
||||
turtleMode: boolean;
|
||||
setTurtleMode: (state: boolean) => void;
|
||||
}
|
||||
|
||||
const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
@ -33,6 +35,8 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
messages,
|
||||
setMessages,
|
||||
baseUrl,
|
||||
setTurtleMode,
|
||||
turtleMode,
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@ -94,7 +98,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
encrypted_private_key: ownEncPrivKey,
|
||||
passphrase: token,
|
||||
},
|
||||
messages: messages,
|
||||
messages,
|
||||
};
|
||||
};
|
||||
|
||||
@ -112,7 +116,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
setLastIndex(lastIndex < dataFromServer.index ? dataFromServer.index : lastIndex);
|
||||
setMessages((prev: EncryptedChatMessage[]) => {
|
||||
const existingMessage = prev.find((item) => item.index === dataFromServer.index);
|
||||
if (existingMessage) {
|
||||
if (existingMessage != null) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
@ -158,7 +162,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const onButtonClicked = (e: any) => {
|
||||
if (token && value.indexOf(token) !== -1) {
|
||||
if (token && value.includes(token)) {
|
||||
alert(
|
||||
`Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
|
||||
);
|
||||
@ -172,9 +176,9 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
offset: lastIndex,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
setPeerConnected(response.peer_connected);
|
||||
if (response != null) {
|
||||
if (response.messages) {
|
||||
setPeerConnected(response.peer_connected);
|
||||
setServerMessages(response.messages);
|
||||
}
|
||||
}
|
||||
@ -197,7 +201,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
offset: lastIndex,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
if (response != null) {
|
||||
setPeerConnected(response.peer_connected);
|
||||
if (response.messages) {
|
||||
setServerMessages(response.messages);
|
||||
@ -215,15 +219,38 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Container component='main'>
|
||||
<ChatHeader connected={true} peerConnected={peerConnected} />
|
||||
<div style={{ position: 'relative', left: '-0.14em', margin: '0 auto', width: '17.7em' }}>
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0.5}
|
||||
>
|
||||
<AuditPGPDialog
|
||||
open={audit}
|
||||
onClose={() => setAudit(false)}
|
||||
orderId={Number(orderId)}
|
||||
messages={messages}
|
||||
own_pub_key={ownPubKey || ''}
|
||||
own_enc_priv_key={ownEncPrivKey || ''}
|
||||
peer_pub_key={peerPubKey || 'Not received yet'}
|
||||
passphrase={token || ''}
|
||||
onClickBack={() => setAudit(false)}
|
||||
/>
|
||||
|
||||
<Grid item>
|
||||
<ChatHeader
|
||||
connected={true}
|
||||
peerConnected={peerConnected}
|
||||
turtleMode={turtleMode}
|
||||
setTurtleMode={setTurtleMode}
|
||||
/>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{
|
||||
height: '21.42em',
|
||||
maxHeight: '21.42em',
|
||||
width: '17.7em',
|
||||
height: '18.42em',
|
||||
maxHeight: '18.42em',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
@ -251,8 +278,8 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
/>
|
||||
</Paper>
|
||||
<form noValidate onSubmit={onButtonClicked}>
|
||||
<Grid alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid alignItems='stretch' style={{ display: 'flex', width: '100%' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={9}>
|
||||
<TextField
|
||||
label={t('Type a message')}
|
||||
variant='standard'
|
||||
@ -261,16 +288,16 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
sx={{ width: '13.7em' }}
|
||||
fullWidth={true}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}>
|
||||
<Button
|
||||
sx={{ width: '4.68em' }}
|
||||
disabled={waitingEcho || !peerPubKey}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
fullWidth={true}
|
||||
>
|
||||
{waitingEcho ? (
|
||||
<div
|
||||
@ -298,22 +325,9 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<div style={{ height: '0.3em' }} />
|
||||
|
||||
<Grid container spacing={0}>
|
||||
<AuditPGPDialog
|
||||
open={audit}
|
||||
onClose={() => setAudit(false)}
|
||||
orderId={Number(orderId)}
|
||||
messages={messages}
|
||||
own_pub_key={ownPubKey || ''}
|
||||
own_enc_priv_key={ownEncPrivKey || ''}
|
||||
peer_pub_key={peerPubKey || 'Not received yet'}
|
||||
passphrase={token || ''}
|
||||
onClickBack={() => setAudit(false)}
|
||||
/>
|
||||
<Grid item>
|
||||
<ChatBottom
|
||||
orderId={orderId}
|
||||
audit={audit}
|
||||
@ -321,7 +335,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
createJsonFile={createJsonFile}
|
||||
/>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -59,7 +59,7 @@ const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected, baseUrl
|
||||
flexWrap: 'wrap',
|
||||
position: 'relative',
|
||||
left: '-0.35em',
|
||||
width: '17.14em',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@ -129,9 +129,7 @@ const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected, baseUrl
|
||||
subheaderTypographyProps={{
|
||||
sx: {
|
||||
wordWrap: 'break-word',
|
||||
width: '14.3em',
|
||||
position: 'relative',
|
||||
right: '1.5em',
|
||||
width: '13em',
|
||||
textAlign: 'left',
|
||||
fontSize: showPGP ? theme.typography.fontSize * 0.78 : null,
|
||||
},
|
||||
|
@ -3,13 +3,14 @@ import EncryptedSocketChat from './EncryptedSocketChat';
|
||||
import EncryptedTurtleChat from './EncryptedTurtleChat';
|
||||
|
||||
interface Props {
|
||||
turtleMode: boolean;
|
||||
orderId: number;
|
||||
takerNick: string;
|
||||
makerNick: string;
|
||||
userNick: string;
|
||||
chatOffset: number;
|
||||
baseUrl: string;
|
||||
messages: EncryptedChatMessage[];
|
||||
setMessages: (state: EncryptedChatMessage[]) => void;
|
||||
}
|
||||
|
||||
export interface EncryptedChatMessage {
|
||||
@ -29,14 +30,15 @@ export interface ServerMessage {
|
||||
}
|
||||
|
||||
const EncryptedChat: React.FC<Props> = ({
|
||||
turtleMode,
|
||||
orderId,
|
||||
takerNick,
|
||||
userNick,
|
||||
chatOffset,
|
||||
baseUrl,
|
||||
setMessages,
|
||||
messages,
|
||||
}: Props): JSX.Element => {
|
||||
const [messages, setMessages] = useState<EncryptedChatMessage[]>([]);
|
||||
const [turtleMode, setTurtleMode] = useState<boolean>(window.ReactNativeWebView !== undefined);
|
||||
|
||||
return turtleMode ? (
|
||||
<EncryptedTurtleChat
|
||||
@ -47,6 +49,8 @@ const EncryptedChat: React.FC<Props> = ({
|
||||
userNick={userNick}
|
||||
chatOffset={chatOffset}
|
||||
baseUrl={baseUrl}
|
||||
turtleMode={turtleMode}
|
||||
setTurtleMode={setTurtleMode}
|
||||
/>
|
||||
) : (
|
||||
<EncryptedSocketChat
|
||||
@ -56,6 +60,8 @@ const EncryptedChat: React.FC<Props> = ({
|
||||
takerNick={takerNick}
|
||||
userNick={userNick}
|
||||
baseUrl={baseUrl}
|
||||
turtleMode={turtleMode}
|
||||
setTurtleMode={setTurtleMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
91
frontend/src/components/TradeBox/Forms/Dispute.tsx
Normal file
91
frontend/src/components/TradeBox/Forms/Dispute.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, TextField, Checkbox, Tooltip, FormControlLabel } from '@mui/material';
|
||||
import { Order } from '../../../models';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { EncryptedChatMessage } from '../EncryptedChat';
|
||||
|
||||
export interface DisputeForm {
|
||||
statement: string;
|
||||
attachLogs: boolean;
|
||||
badStatement: string;
|
||||
}
|
||||
|
||||
export const defaultDispute: DisputeForm = {
|
||||
statement: '',
|
||||
attachLogs: true,
|
||||
badStatement: '',
|
||||
};
|
||||
|
||||
interface DisputeStatementFormProps {
|
||||
loading: boolean;
|
||||
dispute: DisputeForm;
|
||||
setDispute: (state: DisputeForm) => void;
|
||||
onClickSubmit: () => void;
|
||||
}
|
||||
|
||||
export const DisputeStatementForm = ({
|
||||
loading,
|
||||
onClickSubmit,
|
||||
dispute,
|
||||
setDispute,
|
||||
}: DisputeStatementFormProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
sx={{ width: '18em' }}
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0.5}
|
||||
padding={1}
|
||||
>
|
||||
<Grid item>
|
||||
<TextField
|
||||
error={dispute.badStatement != ''}
|
||||
helperText={dispute.badStatement}
|
||||
label={t('Submit dispute statement')}
|
||||
required
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
multiline
|
||||
rows={4}
|
||||
onChange={(e) => setDispute({ ...dispute, statement: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
enterTouchDelay={0}
|
||||
placement='top'
|
||||
title={t(
|
||||
'Attaching chat logs helps the dispute resolution process and adds transparency. However, it might compromise your privacy.',
|
||||
)}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={dispute.attachLogs}
|
||||
onChange={() => setDispute({ ...dispute, attachLogs: !dispute.attachLogs })}
|
||||
/>
|
||||
}
|
||||
label={t('Attach chat logs')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
onClick={onClickSubmit}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
loading={loading}
|
||||
>
|
||||
{t('Submit')}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisputeStatementForm;
|
90
frontend/src/components/TradeBox/Forms/LightningPayout.tsx
Normal file
90
frontend/src/components/TradeBox/Forms/LightningPayout.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography, TextField } from '@mui/material';
|
||||
import { Order } from '../../../models';
|
||||
import WalletsButton from '../WalletsButton';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { pn } from '../../../utils';
|
||||
|
||||
export interface LightningForm {
|
||||
invoice: string;
|
||||
routingBudget: number;
|
||||
badInvoice: string;
|
||||
useLnproxy: boolean;
|
||||
lnproxyServer: string;
|
||||
lnproxyBudget: number;
|
||||
badLnproxy: string;
|
||||
}
|
||||
|
||||
export const defaultLightning: LightningForm = {
|
||||
invoice: '',
|
||||
routingBudget: 0,
|
||||
badInvoice: '',
|
||||
useLnproxy: false,
|
||||
lnproxyServer: '',
|
||||
lnproxyBudget: 0,
|
||||
badLnproxy: '',
|
||||
};
|
||||
|
||||
interface LightningPayoutFormProps {
|
||||
order: Order;
|
||||
loading: boolean;
|
||||
lightning: LightningForm;
|
||||
setLightning: (state: LightningForm) => void;
|
||||
onClickSubmit: (invoice: string) => void;
|
||||
}
|
||||
|
||||
export const LightningPayoutForm = ({
|
||||
order,
|
||||
loading,
|
||||
onClickSubmit,
|
||||
lightning,
|
||||
setLightning,
|
||||
}: LightningPayoutFormProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Grid container direction='column' justifyContent='flex-start' alignItems='center' spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='body2'>
|
||||
{t('Submit a valid invoice for {{amountSats}} Satoshis.', {
|
||||
amountSats: pn(order.invoice_amount),
|
||||
})}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<WalletsButton />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth={true}
|
||||
error={lightning.badInvoice != ''}
|
||||
helperText={lightning.badInvoice ? t(lightning.badInvoice) : ''}
|
||||
label={t('Payout Lightning Invoice')}
|
||||
required
|
||||
value={lightning.invoice}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center', maxHeight: '14.28em' },
|
||||
}}
|
||||
multiline
|
||||
minRows={4}
|
||||
maxRows={8}
|
||||
onChange={(e) => setLightning({ ...lightning, invoice: e.target.value ?? '' })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
onClick={() => onClickSubmit(lightning.invoice)}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
>
|
||||
{t('Submit')}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default LightningPayoutForm;
|
159
frontend/src/components/TradeBox/Forms/OnchainPayout.tsx
Normal file
159
frontend/src/components/TradeBox/Forms/OnchainPayout.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography, TextField, List, Divider, ListItemText, ListItem } from '@mui/material';
|
||||
import { Order } from '../../../models';
|
||||
import WalletsButton from '../WalletsButton';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { pn } from '../../../utils';
|
||||
|
||||
export interface OnchainForm {
|
||||
address: string;
|
||||
miningFee: number;
|
||||
badAddress: string;
|
||||
}
|
||||
|
||||
export const defaultOnchain: OnchainForm = {
|
||||
address: '',
|
||||
miningFee: 10,
|
||||
badAddress: '',
|
||||
};
|
||||
|
||||
interface OnchainPayoutFormProps {
|
||||
order: Order;
|
||||
loading: boolean;
|
||||
onchain: OnchainForm;
|
||||
setOnchain: (state: OnchainForm) => void;
|
||||
onClickSubmit: () => void;
|
||||
}
|
||||
|
||||
export const OnchainPayoutForm = ({
|
||||
order,
|
||||
loading,
|
||||
onClickSubmit,
|
||||
onchain,
|
||||
setOnchain,
|
||||
}: OnchainPayoutFormProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const invalidFee = onchain.miningFee < 1 || onchain.miningFee > 50;
|
||||
const costPerVByte = 141;
|
||||
|
||||
useEffect(() => {
|
||||
setOnchain({ ...onchain, miningFee: order.suggested_mining_fee_rate });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0.5}
|
||||
>
|
||||
<List dense={true}>
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{t('RoboSats coordinator will do a swap and send the Sats to your onchain address.')}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
pn(Math.floor((order.invoice_amount * order.swap_fee_rate) / 100)) +
|
||||
' Sats (' +
|
||||
order.swap_fee_rate +
|
||||
'%)'
|
||||
}
|
||||
secondary={t('Swap fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
pn(Math.floor(Math.max(1, onchain.miningFee) * costPerVByte)) +
|
||||
' Sats (' +
|
||||
Math.max(1, onchain.miningFee) +
|
||||
' Sats/vByte)'
|
||||
}
|
||||
secondary={t('Mining fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
<b>
|
||||
{pn(
|
||||
Math.floor(
|
||||
order.invoice_amount -
|
||||
Math.max(1, onchain.miningFee) * costPerVByte -
|
||||
(order.invoice_amount * order.swap_fee_rate) / 100,
|
||||
),
|
||||
) + ' Sats'}
|
||||
</b>
|
||||
}
|
||||
secondary={t('Final amount you will receive')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Grid item>
|
||||
<Grid container direction='row' justifyContent='center' alignItems='flex-start' spacing={0}>
|
||||
<Grid item xs={7}>
|
||||
<TextField
|
||||
error={onchain.badAddress != ''}
|
||||
helperText={onchain.badAddress ? t(onchain.badAddress) : ''}
|
||||
label={t('Bitcoin Address')}
|
||||
required
|
||||
value={onchain.address}
|
||||
fullWidth={true}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={(e) => setOnchain({ ...onchain, address: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
error={invalidFee}
|
||||
helperText={invalidFee ? t('Invalid') : ''}
|
||||
label={t('Mining Fee')}
|
||||
required
|
||||
fullWidth={true}
|
||||
value={onchain.miningFee}
|
||||
type='number'
|
||||
inputProps={{
|
||||
max: 50,
|
||||
min: 1,
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={(e) => setOnchain({ ...onchain, miningFee: Number(e.target.value) })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
onClick={onClickSubmit}
|
||||
disabled={invalidFee}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
>
|
||||
{t('Submit')}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnchainPayoutForm;
|
7
frontend/src/components/TradeBox/Forms/index.ts
Normal file
7
frontend/src/components/TradeBox/Forms/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export { LightningPayoutForm, defaultLightning } from './LightningPayout';
|
||||
export { OnchainPayoutForm, defaultOnchain } from './OnchainPayout';
|
||||
export { DisputeStatementForm, defaultDispute } from './Dispute';
|
||||
|
||||
export type { LightningForm } from './LightningPayout';
|
||||
export type { OnchainForm } from './OnchainPayout';
|
||||
export type { DisputeForm } from './Dispute';
|
189
frontend/src/components/TradeBox/Prompts/Chat.tsx
Normal file
189
frontend/src/components/TradeBox/Prompts/Chat.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography, Tooltip, Collapse, IconButton } from '@mui/material';
|
||||
import currencies from '../../../../static/assets/currencies.json';
|
||||
|
||||
import { Order } from '../../../models';
|
||||
import { pn } from '../../../utils';
|
||||
import EncryptedChat, { EncryptedChatMessage } from '../EncryptedChat';
|
||||
import Countdown, { zeroPad } from 'react-countdown';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
interface ChatPromptProps {
|
||||
order: Order;
|
||||
onClickConfirmSent: () => void;
|
||||
loadingSent: boolean;
|
||||
onClickConfirmReceived: () => void;
|
||||
loadingReceived: boolean;
|
||||
onClickDispute: () => void;
|
||||
loadingDispute: boolean;
|
||||
baseUrl: string;
|
||||
messages: EncryptedChatMessage[];
|
||||
setMessages: (state: EncryptedChatMessage[]) => void;
|
||||
}
|
||||
|
||||
export const ChatPrompt = ({
|
||||
order,
|
||||
onClickConfirmSent,
|
||||
onClickConfirmReceived,
|
||||
loadingSent,
|
||||
loadingReceived,
|
||||
onClickDispute,
|
||||
loadingDispute,
|
||||
baseUrl,
|
||||
messages,
|
||||
setMessages,
|
||||
}: ChatPromptProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [sentButton, setSentButton] = useState<boolean>(false);
|
||||
const [receivedButton, setReceivedButton] = useState<boolean>(false);
|
||||
const [enableDisputeButton, setEnableDisputeButton] = useState<boolean>(false);
|
||||
const [enableDisputeTime, setEnableDisputeTime] = useState<Date>(new Date(order.expires_at));
|
||||
const [text, setText] = useState<string>('');
|
||||
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
const amount: string = pn(
|
||||
parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4)),
|
||||
);
|
||||
|
||||
const disputeCountdownRenderer = function ({ hours, minutes }) {
|
||||
return (
|
||||
<span>{`${t('To open a dispute you need to wait')} ${hours}h ${zeroPad(minutes)}m `}</span>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// open dispute button enables 12h before expiry
|
||||
const now = Date.now();
|
||||
const expires_at = new Date(order.expires_at);
|
||||
expires_at.setHours(expires_at.getHours() - 12);
|
||||
setEnableDisputeButton(now > expires_at);
|
||||
setEnableDisputeTime(expires_at);
|
||||
|
||||
if (order.status == 9) {
|
||||
// No fiat sent yet
|
||||
if (order.is_buyer) {
|
||||
setSentButton(true);
|
||||
setReceivedButton(false);
|
||||
setText(
|
||||
t(
|
||||
"Say hi! Ask for payment details and click 'Confirm Sent' as soon as the payment is sent.",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setSentButton(false);
|
||||
setReceivedButton(false);
|
||||
setText(
|
||||
t(
|
||||
'Say hi! Be helpful and concise. Let them know how to send you {{amount}} {{currencyCode}}.',
|
||||
{
|
||||
currencyCode,
|
||||
amount,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (order.status == 10) {
|
||||
// Fiat has been sent already
|
||||
if (order.is_buyer) {
|
||||
setSentButton(false);
|
||||
setReceivedButton(false);
|
||||
setText(t('Wait for the seller to confirm he has received the payment.'));
|
||||
} else {
|
||||
setSentButton(false);
|
||||
setReceivedButton(true);
|
||||
setText(t("The buyer has sent the fiat. Click 'Confirm Received' once you receive it."));
|
||||
}
|
||||
}
|
||||
}, [order]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
padding={0}
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant='body2' align='center'>
|
||||
{text}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<EncryptedChat
|
||||
status={order.status}
|
||||
chatOffset={order.chat_last_index}
|
||||
orderId={order.id}
|
||||
takerNick={order.taker_nick}
|
||||
makerNick={order.maker_nick}
|
||||
userNick={order.ur_nick}
|
||||
baseUrl={baseUrl}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
componentsProps={{
|
||||
tooltip: { sx: { position: 'relative', top: '3em' } },
|
||||
}}
|
||||
disableHoverListener={enableDisputeButton}
|
||||
disableTouchListener={enableDisputeButton}
|
||||
enterTouchDelay={0}
|
||||
title={
|
||||
<Countdown date={new Date(enableDisputeTime)} renderer={disputeCountdownRenderer} />
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<LoadingButton
|
||||
loading={loadingDispute}
|
||||
disabled={!enableDisputeButton}
|
||||
color='inherit'
|
||||
onClick={onClickDispute}
|
||||
>
|
||||
{t('Open Dispute')}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item padding={0.5}>
|
||||
{sentButton ? (
|
||||
<Collapse in={sentButton}>
|
||||
<LoadingButton
|
||||
loading={loadingSent}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
onClick={onClickConfirmSent}
|
||||
>
|
||||
{t('Confirm {{amount}} {{currencyCode}} sent', { currencyCode, amount })}
|
||||
</LoadingButton>
|
||||
</Collapse>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{receivedButton ? (
|
||||
<Collapse in={receivedButton}>
|
||||
<LoadingButton
|
||||
loading={loadingReceived}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
onClick={onClickConfirmReceived}
|
||||
>
|
||||
{t('Confirm {{amount}} {{currencyCode}} received', { currencyCode, amount })}
|
||||
</LoadingButton>
|
||||
</Collapse>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPrompt;
|
48
frontend/src/components/TradeBox/Prompts/Dispute.tsx
Normal file
48
frontend/src/components/TradeBox/Prompts/Dispute.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import { DisputeForm, DisputeStatementForm } from '../Forms';
|
||||
|
||||
interface DisputePromptProps {
|
||||
loading: boolean;
|
||||
dispute: DisputeForm;
|
||||
setDispute: (state: DisputeForm) => void;
|
||||
onClickSubmit: () => void;
|
||||
}
|
||||
|
||||
export const DisputePrompt = ({
|
||||
loading,
|
||||
dispute,
|
||||
onClickSubmit,
|
||||
setDispute,
|
||||
}: DisputePromptProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0}
|
||||
padding={1}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant='body2'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<DisputeStatementForm
|
||||
loading={loading}
|
||||
onClickSubmit={onClickSubmit}
|
||||
dispute={dispute}
|
||||
setDispute={setDispute}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisputePrompt;
|
22
frontend/src/components/TradeBox/Prompts/DisputeLoser.tsx
Normal file
22
frontend/src/components/TradeBox/Prompts/DisputeLoser.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { List, ListItem, Divider, Typography } from '@mui/material';
|
||||
|
||||
export const DisputeLoserPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisputeLoserPrompt;
|
29
frontend/src/components/TradeBox/Prompts/DisputeWaitPeer.tsx
Normal file
29
frontend/src/components/TradeBox/Prompts/DisputeWaitPeer.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { List, ListItem, Divider, Typography } from '@mui/material';
|
||||
|
||||
export const DisputeWaitPeerPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{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).',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisputeWaitPeerPrompt;
|
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { List, ListItem, Divider, Typography } from '@mui/material';
|
||||
|
||||
export const DisputeWaitResolutionPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{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).',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisputeWaitResolutionPrompt;
|
22
frontend/src/components/TradeBox/Prompts/DisputeWinner.tsx
Normal file
22
frontend/src/components/TradeBox/Prompts/DisputeWinner.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { List, ListItem, Divider, Typography } from '@mui/material';
|
||||
|
||||
export const DisputeWinnerPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{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).',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisputeWinnerPrompt;
|
26
frontend/src/components/TradeBox/Prompts/EscrowWait.tsx
Normal file
26
frontend/src/components/TradeBox/Prompts/EscrowWait.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { List, ListItem, Divider, Typography } from '@mui/material';
|
||||
|
||||
export const EscrowWaitPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2' align='left'>
|
||||
{t('We are waiting for the seller to lock the trade amount.')}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography variant='body2' align='left'>
|
||||
{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).',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default EscrowWaitPrompt;
|
46
frontend/src/components/TradeBox/Prompts/Expired.tsx
Normal file
46
frontend/src/components/TradeBox/Prompts/Expired.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
import { Order } from '../../../models';
|
||||
|
||||
interface ExpiredPromptProps {
|
||||
order: Order;
|
||||
loadingRenew: boolean;
|
||||
onClickRenew: () => void;
|
||||
}
|
||||
|
||||
export const ExpiredPrompt = ({
|
||||
loadingRenew,
|
||||
order,
|
||||
onClickRenew,
|
||||
}: ExpiredPromptProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='body2' align='center'>
|
||||
{t(order.expiry_message)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
{order.is_maker ? (
|
||||
<Grid item xs={12} style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<LoadingButton
|
||||
loading={loadingRenew}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={onClickRenew}
|
||||
>
|
||||
{t('Renew Order')}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpiredPrompt;
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Grid, Link, Typography, TextField, Tooltip, useTheme } from '@mui/material';
|
||||
import { AccountBalanceWallet, ContentCopy } from '@mui/icons-material';
|
||||
import { NewTabIcon } from '../../Icons';
|
||||
import { Button, Box, Grid, Typography, TextField, Tooltip, useTheme } from '@mui/material';
|
||||
import { ContentCopy } from '@mui/icons-material';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { Order } from '../../../models';
|
||||
import { systemClient } from '../../../services/System';
|
||||
import currencies from '../../../../static/assets/currencies.json';
|
||||
import WalletsButton from '../WalletsButton';
|
||||
|
||||
interface LockInvoicePromptProps {
|
||||
order: Order;
|
||||
@ -19,6 +19,7 @@ export const LockInvoicePrompt = ({ order, concept }: LockInvoicePromptProps): J
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
|
||||
const invoice = concept === 'bond' ? order.bond_invoice : order.escrow_invoice;
|
||||
|
||||
const helperText =
|
||||
concept === 'bond'
|
||||
? t(
|
||||
@ -29,22 +30,6 @@ export const LockInvoicePrompt = ({ order, concept }: LockInvoicePromptProps): J
|
||||
{ currencyCode },
|
||||
);
|
||||
|
||||
const CompatibleWalletsButton = function () {
|
||||
return (
|
||||
<Button
|
||||
color='primary'
|
||||
component={Link}
|
||||
href={'https://learn.robosats.com/docs/wallets/'}
|
||||
target='_blank'
|
||||
align='center'
|
||||
>
|
||||
<AccountBalanceWallet />
|
||||
{t('See Compatible Wallets')}
|
||||
<NewTabIcon sx={{ width: '1.1em', height: '1.1em' }} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const depositHoursMinutes = function () {
|
||||
const hours = Math.floor(order.escrow_duration / 3600);
|
||||
const minutes = Math.floor((order.escrow_duration - hours * 3600) / 60);
|
||||
@ -64,24 +49,50 @@ export const LockInvoicePrompt = ({ order, concept }: LockInvoicePromptProps): J
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0.5}
|
||||
>
|
||||
{order.is_taker && concept == 'bond' ? (
|
||||
<Typography color='secondary'>
|
||||
<b>{t(`You are ${order.is_buyer ? 'BUYING' : 'SELLING'} BTC`)}</b>
|
||||
</Typography>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
{concept === 'bond' ? <CompatibleWalletsButton /> : <ExpirationWarning />}
|
||||
{concept === 'bond' ? <WalletsButton /> : <ExpirationWarning />}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0.5em',
|
||||
borderRadius: '0.3em',
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
bgColor={'rgba(255, 255, 255, 0)'}
|
||||
fgColor={theme.palette.text.primary}
|
||||
value={invoice}
|
||||
value={invoice ?? 'Undefined: BOLT11 invoice not received'}
|
||||
size={theme.typography.fontSize * 21.8}
|
||||
onClick={() => {
|
||||
systemClient.copyToClipboard(invoice);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<Button
|
||||
size='small'
|
||||
@ -101,7 +112,7 @@ export const LockInvoicePrompt = ({ order, concept }: LockInvoicePromptProps): J
|
||||
hiddenLabel
|
||||
variant='standard'
|
||||
size='small'
|
||||
defaultValue={invoice}
|
||||
defaultValue={invoice ?? 'Undefined: BOLT11 invoice not received'}
|
||||
disabled={true}
|
||||
helperText={helperText}
|
||||
color='secondary'
|
||||
|
40
frontend/src/components/TradeBox/Prompts/Paused.tsx
Normal file
40
frontend/src/components/TradeBox/Prompts/Paused.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { List, ListItem, Divider, Grid, Typography } from '@mui/material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
import { PlayCircle } from '@mui/icons-material';
|
||||
|
||||
interface PausedPrompProps {
|
||||
pauseLoading: boolean;
|
||||
onClickResumeOrder: () => void;
|
||||
}
|
||||
|
||||
export const PausedPrompt = ({
|
||||
pauseLoading,
|
||||
onClickResumeOrder,
|
||||
}: PausedPrompProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2' align='left'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<Grid item xs={12} style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<LoadingButton loading={pauseLoading} color='primary' onClick={onClickResumeOrder}>
|
||||
<PlayCircle sx={{ width: '1.6em', height: '1.6em' }} />
|
||||
{t('Unpause Order')}
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default PausedPrompt;
|
121
frontend/src/components/TradeBox/Prompts/Payout.tsx
Normal file
121
frontend/src/components/TradeBox/Prompts/Payout.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography, ToggleButtonGroup, ToggleButton } from '@mui/material';
|
||||
|
||||
import currencies from '../../../../static/assets/currencies.json';
|
||||
|
||||
import { Order } from '../../../models';
|
||||
import { pn } from '../../../utils';
|
||||
import { Bolt, Link } from '@mui/icons-material';
|
||||
import { LightningPayoutForm, LightningForm, OnchainPayoutForm, OnchainForm } from '../Forms';
|
||||
|
||||
interface PayoutPrompProps {
|
||||
order: Order;
|
||||
onClickSubmitInvoice: (invoice: string) => void;
|
||||
lightning: LightningForm;
|
||||
loadingLightning: boolean;
|
||||
setLightning: (state: LightningForm) => void;
|
||||
onClickSubmitAddress: () => void;
|
||||
onchain: OnchainForm;
|
||||
setOnchain: (state: OnchainForm) => void;
|
||||
loadingOnchain: boolean;
|
||||
}
|
||||
|
||||
export const PayoutPrompt = ({
|
||||
order,
|
||||
onClickSubmitInvoice,
|
||||
loadingLightning,
|
||||
lightning,
|
||||
setLightning,
|
||||
onClickSubmitAddress,
|
||||
loadingOnchain,
|
||||
onchain,
|
||||
setOnchain,
|
||||
}: PayoutPrompProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
|
||||
const [tab, setTab] = useState<'lightning' | 'onchain'>('lightning');
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
padding={1}
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={1}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant='body2'>
|
||||
{t(
|
||||
'Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC.',
|
||||
{
|
||||
amountFiat: pn(
|
||||
parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4)),
|
||||
),
|
||||
currencyCode,
|
||||
},
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<ToggleButtonGroup
|
||||
size='small'
|
||||
value={tab}
|
||||
exclusive
|
||||
onChange={(mouseEvent, value: string) => setTab(value)}
|
||||
>
|
||||
<ToggleButton value='lightning' disableRipple={true}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Bolt /> {t('Lightning')}
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton value='onchain' disabled={!order.swap_allowed}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Link /> {t('Onchain')}
|
||||
</div>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
|
||||
<Grid item style={{ display: tab == 'lightning' ? '' : 'none' }}>
|
||||
<LightningPayoutForm
|
||||
order={order}
|
||||
loading={loadingLightning}
|
||||
lightning={lightning}
|
||||
setLightning={setLightning}
|
||||
onClickSubmit={onClickSubmitInvoice}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* ONCHAIN PAYOUT TAB */}
|
||||
<Grid item style={{ display: tab == 'onchain' ? '' : 'none' }}>
|
||||
<OnchainPayoutForm
|
||||
order={order}
|
||||
loading={loadingOnchain}
|
||||
onchain={onchain}
|
||||
setOnchain={setOnchain}
|
||||
onClickSubmit={onClickSubmitAddress}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayoutPrompt;
|
30
frontend/src/components/TradeBox/Prompts/PayoutWait.tsx
Normal file
30
frontend/src/components/TradeBox/Prompts/PayoutWait.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { List, ListItem, Divider, Typography } from '@mui/material';
|
||||
|
||||
export const PayoutWaitPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2' align='left'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<Typography variant='body2' align='left'>
|
||||
{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).',
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayoutWaitPrompt;
|
112
frontend/src/components/TradeBox/Prompts/PublicWait.tsx
Normal file
112
frontend/src/components/TradeBox/Prompts/PublicWait.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Divider,
|
||||
Grid,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
import currencies from '../../../../static/assets/currencies.json';
|
||||
|
||||
import { Order } from '../../../models';
|
||||
import { PauseCircle, Storefront, Percent } from '@mui/icons-material';
|
||||
|
||||
interface PublicWaitPrompProps {
|
||||
order: Order;
|
||||
pauseLoading: boolean;
|
||||
onClickPauseOrder: () => void;
|
||||
}
|
||||
|
||||
export const PublicWaitPrompt = ({
|
||||
order,
|
||||
pauseLoading,
|
||||
onClickPauseOrder,
|
||||
}: PublicWaitPrompProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const currencyCode = currencies[order.currency.toString()];
|
||||
|
||||
const depositHoursMinutes = function () {
|
||||
const hours = Math.floor(order.escrow_duration / 3600);
|
||||
const minutes = Math.floor((order.escrow_duration - hours * 3600) / 60);
|
||||
const dict = { deposit_timer_hours: hours, deposit_timer_minutes: minutes };
|
||||
return dict;
|
||||
};
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<Typography variant='body2' align='left'>
|
||||
{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.',
|
||||
depositHoursMinutes(),
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<Typography variant='body2' align='left'>
|
||||
{t('If the order expires untaken, your bond will return to you (no action needed).')}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={10}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Storefront />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={order.num_similar_orders}
|
||||
secondary={t('Public orders for {{currencyCode}}', {
|
||||
currencyCode,
|
||||
})}
|
||||
/>
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<div style={{ position: 'relative', top: '0.5em', right: '1em' }}>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={500}
|
||||
enterDelay={700}
|
||||
enterNextDelay={2000}
|
||||
title={t('Pause the public order')}
|
||||
>
|
||||
<div>
|
||||
<LoadingButton loading={pauseLoading} color='primary' onClick={onClickPauseOrder}>
|
||||
<PauseCircle sx={{ width: '1.6em', height: '1.6em' }} />
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Percent />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${t('Premium rank')} ${Math.floor(order.premium_percentile * 100)}%`}
|
||||
secondary={t('Among public {{currencyCode}} orders (higher is cheaper)', {
|
||||
currencyCode,
|
||||
})}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicWaitPrompt;
|
140
frontend/src/components/TradeBox/Prompts/RoutingFailed.tsx
Normal file
140
frontend/src/components/TradeBox/Prompts/RoutingFailed.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, CircularProgress, Grid, Typography, useTheme } from '@mui/material';
|
||||
import Countdown, { CountdownRenderProps, zeroPad } from 'react-countdown';
|
||||
|
||||
import { Order } from '../../../models';
|
||||
import { LightningForm, LightningPayoutForm } from '../Forms';
|
||||
|
||||
interface RoutingFailedPromptProps {
|
||||
order: Order;
|
||||
onClickSubmitInvoice: (invoice: string) => void;
|
||||
lightning: LightningForm;
|
||||
loadingLightning: boolean;
|
||||
setLightning: (state: LightningForm) => void;
|
||||
}
|
||||
|
||||
interface FailureReasonProps {
|
||||
failureReason: string;
|
||||
}
|
||||
|
||||
const FailureReason = ({ failureReason }: FailureReasonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '0.3em',
|
||||
border: `1px solid ${theme.palette.text.secondary}`,
|
||||
}}
|
||||
>
|
||||
<Typography variant='body2' align='center'>
|
||||
<b>{t('Failure reason:')}</b>
|
||||
</Typography>
|
||||
<Typography variant='body2' align='center'>
|
||||
{t(failureReason)}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutingFailedPrompt = ({
|
||||
order,
|
||||
onClickSubmitInvoice,
|
||||
loadingLightning,
|
||||
lightning,
|
||||
setLightning,
|
||||
}: RoutingFailedPromptProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const countdownRenderer = function ({ minutes, seconds, completed }: CountdownRenderProps) {
|
||||
if (completed) {
|
||||
return (
|
||||
<Grid container direction='column' alignItems='center' justifyContent='center' spacing={1}>
|
||||
<Grid item>
|
||||
<Typography>{t('Retrying!')}</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<CircularProgress />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
} else {
|
||||
return <span>{`${zeroPad(minutes)}m ${zeroPad(seconds)}s `}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
if (order.invoice_expired && order.failure_reason) {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0.5}
|
||||
padding={1}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant='body2'>
|
||||
{t(
|
||||
'Your invoice has expired or more than 3 payment attempts have been made. Submit a new invoice.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{order.failure_reason ? (
|
||||
<Grid item>
|
||||
<FailureReason failureReason={order.failure_reason} />
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<LightningPayoutForm
|
||||
order={order}
|
||||
loading={loadingLightning}
|
||||
lightning={lightning}
|
||||
setLightning={setLightning}
|
||||
onClickSubmit={onClickSubmitInvoice}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={1}
|
||||
padding={1}
|
||||
>
|
||||
<Grid item>
|
||||
<FailureReason failureReason={order.failure_reason} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant='body2'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<div style={{ height: '0.6em' }} />
|
||||
<Grid item>
|
||||
<Typography align='center'>
|
||||
<b>{t('Next attempt in')}</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Countdown date={new Date(order.next_retry_time)} renderer={countdownRenderer} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RoutingFailedPrompt;
|
31
frontend/src/components/TradeBox/Prompts/SendingSats.tsx
Normal file
31
frontend/src/components/TradeBox/Prompts/SendingSats.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography, CircularProgress } from '@mui/material';
|
||||
|
||||
export const SendingSatsPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={1}
|
||||
padding={1}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant='body2'>
|
||||
{t(
|
||||
'RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must be online in order to receive payments.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<CircularProgress />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendingSatsPrompt;
|
198
frontend/src/components/TradeBox/Prompts/Successful.tsx
Normal file
198
frontend/src/components/TradeBox/Prompts/Successful.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Grid,
|
||||
Typography,
|
||||
Rating,
|
||||
Collapse,
|
||||
Link,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
import currencies from '../../../../static/assets/currencies.json';
|
||||
import TradeSummary from '../TradeSummary';
|
||||
import { Favorite, RocketLaunch, ContentCopy, Refresh } from '@mui/icons-material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { Order } from '../../../models';
|
||||
import { systemClient } from '../../../services/System';
|
||||
|
||||
interface SuccessfulPromptProps {
|
||||
order: Order;
|
||||
ratePlatform: (rating: number) => void;
|
||||
onClickStartAgain: () => void;
|
||||
onClickRenew: () => void;
|
||||
loadingRenew: boolean;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export const SuccessfulPrompt = ({
|
||||
order,
|
||||
ratePlatform,
|
||||
onClickStartAgain,
|
||||
onClickRenew,
|
||||
loadingRenew,
|
||||
baseUrl,
|
||||
}: SuccessfulPromptProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
|
||||
const [rating, setRating] = useState<number | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0.5}
|
||||
padding={1}
|
||||
>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='body2' align='center'>
|
||||
<Trans i18nKey='rate_robosats'>
|
||||
What do you think of <b>RoboSats</b>?
|
||||
</Trans>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Rating
|
||||
name='size-large'
|
||||
defaultValue={0}
|
||||
size='large'
|
||||
onChange={(e) => {
|
||||
const rate = e.target.value;
|
||||
ratePlatform(rate);
|
||||
setRating(rate);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
{rating == 5 ? (
|
||||
<Grid item xs={12}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant='body2' align='center'>
|
||||
<b>{t('Thank you! RoboSats loves you too')}</b>
|
||||
</Typography>
|
||||
<Favorite color='error' />
|
||||
</div>
|
||||
<Typography variant='body2' align='center'>
|
||||
{t(
|
||||
'RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : rating != undefined ? (
|
||||
<Grid>
|
||||
<Typography variant='body2' align='center'>
|
||||
<b>{t('Thank you for using Robosats!')}</b>
|
||||
</Typography>
|
||||
<Typography variant='body2' align='center'>
|
||||
<Trans i18nKey='let_us_know_hot_to_improve'>
|
||||
Let us know how the platform could improve (
|
||||
<Link target='_blank' href='https://t.me/robosats'>
|
||||
Telegram
|
||||
</Link>
|
||||
{' / '}
|
||||
<Link target='_blank' href='https://github.com/Reckless-Satoshi/robosats/issues'>
|
||||
Github
|
||||
</Link>
|
||||
)
|
||||
</Trans>
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{/* SHOW TXID IF USER RECEIVES ONCHAIN */}
|
||||
<Collapse in={order.txid != undefined}>
|
||||
<Alert severity='success'>
|
||||
<AlertTitle>
|
||||
{t('Your TXID')}
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
systemClient.copyToClipboard(order.txid);
|
||||
}}
|
||||
>
|
||||
<ContentCopy sx={{ width: '1em', height: '1em' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</AlertTitle>
|
||||
<Typography
|
||||
variant='body2'
|
||||
align='center'
|
||||
sx={{ wordWrap: 'break-word', width: '15.71em' }}
|
||||
>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={
|
||||
'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/' +
|
||||
(order.network == 'testnet' ? 'testnet/' : '') +
|
||||
'tx/' +
|
||||
order.txid
|
||||
}
|
||||
>
|
||||
{order.txid}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Grid item container alignItems='center' justifyContent='space-evenly'>
|
||||
<Grid item>
|
||||
<Button color='primary' variant='outlined' onClick={onClickStartAgain}>
|
||||
<RocketLaunch sx={{ width: '0.8em' }} />
|
||||
<Typography style={{ display: 'inline-block' }}>{t('Start Again')}</Typography>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
{order.is_maker ? (
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={onClickRenew}
|
||||
loading={loadingRenew}
|
||||
>
|
||||
<Refresh sx={{ width: '0.8em' }} />
|
||||
<Typography style={{ display: 'inline-block' }}>{t('Renew')}</Typography>
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
) : null}
|
||||
</Grid>
|
||||
|
||||
{order.platform_summary ? (
|
||||
<Grid item>
|
||||
<TradeSummary
|
||||
isMaker={order.is_maker}
|
||||
makerNick={order.maker_nick}
|
||||
takerNick={order.taker_nick}
|
||||
currencyCode={currencyCode}
|
||||
makerSummary={order.maker_summary}
|
||||
takerSummary={order.taker_summary}
|
||||
platformSummary={order.platform_summary}
|
||||
orderId={order.id}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuccessfulPrompt;
|
@ -1,35 +1,21 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import { Order } from '../../../models';
|
||||
import stepXofY from '../stepXofY';
|
||||
import { Divider, List, ListItem, Typography } from '@mui/material';
|
||||
|
||||
interface TakerFoundPrompProps {
|
||||
order: Order;
|
||||
}
|
||||
|
||||
export const TakerFoundPrompt = ({ order }: TakerFoundPrompProps): JSX.Element => {
|
||||
export const TakerFoundPrompt = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const Title = function () {
|
||||
return (
|
||||
<Typography color='primary' variant='subtitle1'>
|
||||
<b>{t('A taker has been found!')}</b>
|
||||
{` ${stepXofY(order)}`}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<List dense={true}>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<Typography variant='body2'>
|
||||
{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.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,2 +1,17 @@
|
||||
export { LockInvoicePrompt } from './LockInvoice';
|
||||
export { TakerFoundPrompt } from './TakerFound';
|
||||
export { PublicWaitPrompt } from './PublicWait';
|
||||
export { PausedPrompt } from './Paused';
|
||||
export { ExpiredPrompt } from './Expired';
|
||||
export { PayoutPrompt } from './Payout';
|
||||
export { EscrowWaitPrompt } from './EscrowWait';
|
||||
export { PayoutWaitPrompt } from './PayoutWait';
|
||||
export { ChatPrompt } from './Chat';
|
||||
export { DisputePrompt } from './Dispute';
|
||||
export { DisputeWaitPeerPrompt } from './DisputeWaitPeer';
|
||||
export { DisputeWaitResolutionPrompt } from './DisputeWaitResolution';
|
||||
export { SendingSatsPrompt } from './SendingSats';
|
||||
export { SuccessfulPrompt } from './Successful';
|
||||
export { RoutingFailedPrompt } from './RoutingFailed';
|
||||
export { DisputeWinnerPrompt } from './DisputeWinner';
|
||||
export { DisputeLoserPrompt } from './DisputeLoser';
|
||||
|
@ -3,92 +3,46 @@ 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;
|
||||
text: string;
|
||||
variables?: Object;
|
||||
color?: string;
|
||||
icon?: () => JSX.Element;
|
||||
}
|
||||
|
||||
export const Title = ({ order }: TakerFoundPrompProps): JSX.Element => {
|
||||
export const Title = ({
|
||||
order,
|
||||
text,
|
||||
variables = {},
|
||||
color = 'primary',
|
||||
icon = function () {
|
||||
return <></>;
|
||||
},
|
||||
}: 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) });
|
||||
let textColor = color;
|
||||
if (color == 'warning') {
|
||||
textColor = theme.palette.warning.main;
|
||||
} else if (color == 'success') {
|
||||
textColor = theme.palette.success.main;
|
||||
}
|
||||
|
||||
{
|
||||
/* 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 (
|
||||
<Typography variant='body2'>
|
||||
<b>{text}</b>
|
||||
{stepXofY(order)}
|
||||
<Typography
|
||||
color={textColor}
|
||||
variant='subtitle1'
|
||||
align='center'
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{icon()}
|
||||
<span>
|
||||
<b>{t(text, variables)}</b> {stepXofY(order)}
|
||||
</span>
|
||||
{icon()}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
@ -10,43 +9,39 @@ import {
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Grid,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { pn, saveAsJson } from '../../utils';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
|
||||
// Icons
|
||||
import { FlagWithProps } from '../Icons';
|
||||
import ScheduleIcon from '@mui/icons-material/Schedule';
|
||||
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
|
||||
import RouteIcon from '@mui/icons-material/Route';
|
||||
import AccountBoxIcon from '@mui/icons-material/AccountBox';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
import { ExportIcon, FlagWithProps } from '../Icons';
|
||||
import {
|
||||
Schedule,
|
||||
PriceChange,
|
||||
LockOpen,
|
||||
Download,
|
||||
AccountBalance,
|
||||
Route,
|
||||
AccountBox,
|
||||
Link,
|
||||
} from '@mui/icons-material';
|
||||
import { RoboSatsNoTextIcon, SendReceiveIcon, BitcoinIcon } from '../Icons';
|
||||
|
||||
interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
import { TradeCoordinatorSummary, TradeRobotSummary } from '../../models/Order.model';
|
||||
import { systemClient } from '../../services/System';
|
||||
|
||||
interface Props {
|
||||
isMaker: boolean;
|
||||
makerNick: string;
|
||||
takerNick: string;
|
||||
currencyCode: string;
|
||||
makerSummary: Record<string, Item>;
|
||||
takerSummary: Record<string, Item>;
|
||||
platformSummary: Record<string, Item>;
|
||||
makerSummary: TradeRobotSummary;
|
||||
takerSummary: TradeRobotSummary;
|
||||
platformSummary: TradeCoordinatorSummary;
|
||||
orderId: number;
|
||||
baseUrl: string;
|
||||
}
|
||||
@ -63,31 +58,52 @@ const TradeSummary = ({
|
||||
baseUrl,
|
||||
}: Props): JSX.Element => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const [buttonValue, setButtonValue] = useState<number>(isMaker ? 0 : 2);
|
||||
const userSummary = buttonValue == 0 ? makerSummary : takerSummary;
|
||||
const contractTimestamp = new Date(platformSummary.contract_timestamp);
|
||||
const contractTimestamp = new Date(platformSummary.contract_timestamp ?? null);
|
||||
const total_time = platformSummary.contract_total_time;
|
||||
const hours = parseInt(total_time / 3600);
|
||||
const mins = parseInt((total_time - hours * 3600) / 60);
|
||||
const secs = parseInt(total_time - hours * 3600 - mins * 60);
|
||||
|
||||
const onClickExport = function () {
|
||||
const summary = {
|
||||
order_id: orderId,
|
||||
currency: currencyCode,
|
||||
maker: makerSummary,
|
||||
taker: takerSummary,
|
||||
platform: platformSummary,
|
||||
};
|
||||
if (window.NativeRobosats === undefined) {
|
||||
saveAsJson(`order${orderId}-summary.json`, summary);
|
||||
} else {
|
||||
systemClient.copyToClipboard(JSON.stringify(summary));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid item xs={12}>
|
||||
<Accordion
|
||||
defaultExpanded={true}
|
||||
elevation={0}
|
||||
sx={{ width: 322, position: 'relative', right: 8 }}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '0.3em',
|
||||
padding: '0.5em',
|
||||
}}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon sx={{ width: 28 }} color='primary' />}>
|
||||
<Typography sx={{ flexGrow: 1 }} color='text.secondary'>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography align='center' color='text.secondary'>
|
||||
{t('Trade Summary')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Tooltip enterTouchDelay={250} title={t('Export trade summary')}>
|
||||
<IconButton color='primary' onClick={onClickExport}>
|
||||
<ExportIcon sx={{ width: '0.8em', height: '0.8em' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
left: 14,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@ -98,7 +114,7 @@ const TradeSummary = ({
|
||||
<ToggleButton value={0} disableRipple={true} onClick={() => setButtonValue(0)}>
|
||||
<RobotAvatar
|
||||
baseUrl={baseUrl}
|
||||
style={{ height: 28, width: 28 }}
|
||||
style={{ height: '1.5em', width: '1.5em' }}
|
||||
nickname={makerNick}
|
||||
/>
|
||||
|
||||
@ -113,29 +129,11 @@ const TradeSummary = ({
|
||||
<RobotAvatar
|
||||
baseUrl={baseUrl}
|
||||
avatarClass='smallAvatar'
|
||||
style={{ height: 28, width: 28 }}
|
||||
style={{ height: '1.5em', width: '1.5em' }}
|
||||
nickname={takerNick}
|
||||
/>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Tooltip enterTouchDelay={250} title={t('Save trade summary as file')}>
|
||||
<span>
|
||||
<IconButton
|
||||
color='primary'
|
||||
onClick={() =>
|
||||
saveAsJson(`order${orderId}-summary.json`, {
|
||||
order_id: orderId,
|
||||
currency: currencyCode,
|
||||
maker: makerSummary,
|
||||
taker: takerSummary,
|
||||
platform: platformSummary,
|
||||
})
|
||||
}
|
||||
>
|
||||
<DownloadIcon sx={{ width: 26, height: 26 }} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* Maker/Taker Summary */}
|
||||
<div style={{ display: [0, 2].includes(buttonValue) ? '' : 'none' }}>
|
||||
@ -146,20 +144,20 @@ const TradeSummary = ({
|
||||
overlap='circular'
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
badgeContent={
|
||||
<div style={{ position: 'relative', left: '3px', top: '2px' }}>
|
||||
<div style={{ position: 'relative', left: '0.1em', top: '0.1em' }}>
|
||||
{userSummary.is_buyer ? (
|
||||
<SendReceiveIcon
|
||||
sx={{ transform: 'scaleX(-1)', height: '18px', width: '18px' }}
|
||||
sx={{ transform: 'scaleX(-1)', height: '0.7em', width: '0.7em' }}
|
||||
color='secondary'
|
||||
/>
|
||||
) : (
|
||||
<SendReceiveIcon sx={{ height: '18px', width: '18px' }} color='primary' />
|
||||
<SendReceiveIcon sx={{ height: '0.7em', width: '0.7em' }} color='primary' />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AccountBoxIcon
|
||||
sx={{ position: 'relative', left: -2, width: 28, height: 28 }}
|
||||
<AccountBox
|
||||
sx={{ position: 'relative', left: '-0.1em', width: '1.5em', height: '1.5em' }}
|
||||
/>
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
@ -186,9 +184,7 @@ const TradeSummary = ({
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
(userSummary.is_buyer
|
||||
? pn(userSummary.sent_fiat)
|
||||
: pn(userSummary.received_fiat)) +
|
||||
(userSummary.is_buyer ? pn(userSummary.sent_fiat) : pn(userSummary.received_fiat)) +
|
||||
' ' +
|
||||
currencyCode
|
||||
}
|
||||
@ -211,9 +207,7 @@ const TradeSummary = ({
|
||||
<ListItemText
|
||||
primary={t('{{tradeFeeSats}} Sats ({{tradeFeePercent}}%)', {
|
||||
tradeFeeSats: pn(userSummary.trade_fee_sats),
|
||||
tradeFeePercent: parseFloat(
|
||||
(userSummary.trade_fee_percent * 100).toPrecision(3),
|
||||
),
|
||||
tradeFeePercent: parseFloat((userSummary.trade_fee_percent * 100).toPrecision(3)),
|
||||
})}
|
||||
secondary={'Trade fee'}
|
||||
/>
|
||||
@ -222,7 +216,7 @@ const TradeSummary = ({
|
||||
{userSummary.is_swap ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<LinkIcon />
|
||||
<Link />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('{{swapFeeSats}} Sats ({{swapFeePercent}}%)', {
|
||||
@ -242,7 +236,7 @@ const TradeSummary = ({
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<LockOpenIcon color='success' />
|
||||
<LockOpen color='success' />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('{{bondSats}} Sats ({{bondPercent}}%)', {
|
||||
@ -251,7 +245,10 @@ const TradeSummary = ({
|
||||
})}
|
||||
secondary={buttonValue === 0 ? t('Maker bond') : t('Taker bond')}
|
||||
/>
|
||||
<ListItemText sx={{ color: '#2e7d32' }} primary={<b>{t('Unlocked')}</b>} />
|
||||
<ListItemText
|
||||
sx={{ color: theme.palette.success.main }}
|
||||
primary={<b>{t('Unlocked')}</b>}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
@ -260,7 +257,7 @@ const TradeSummary = ({
|
||||
<List dense={true}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AccountBalanceIcon />
|
||||
<AccountBalance />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('{{revenueSats}} Sats', {
|
||||
@ -272,7 +269,7 @@ const TradeSummary = ({
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<RouteIcon />
|
||||
<Route />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('{{routingFeeSats}} MiliSats', {
|
||||
@ -284,7 +281,7 @@ const TradeSummary = ({
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PriceChangeIcon />
|
||||
<PriceChange />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${pn(
|
||||
@ -300,21 +297,18 @@ const TradeSummary = ({
|
||||
secondary={t('Timestamp')}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<ScheduleIcon />
|
||||
<Schedule />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${String(hours).padStart(2, '0')}:${String(mins).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:${String(secs).padStart(2, '0')}`}
|
||||
primary={`${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(
|
||||
secs,
|
||||
).padStart(2, '0')}`}
|
||||
secondary={t('Completed in')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
25
frontend/src/components/TradeBox/WalletsButton.tsx
Normal file
25
frontend/src/components/TradeBox/WalletsButton.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Link } from '@mui/material';
|
||||
|
||||
import { AccountBalanceWallet } from '@mui/icons-material';
|
||||
import { NewTabIcon } from '../Icons';
|
||||
|
||||
const WalletsButton = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button
|
||||
color='primary'
|
||||
component={Link}
|
||||
href={'https://learn.robosats.com/docs/wallets/'}
|
||||
target='_blank'
|
||||
align='center'
|
||||
>
|
||||
<AccountBalanceWallet />
|
||||
{t('See Compatible Wallets')}
|
||||
<NewTabIcon sx={{ width: '0.7em', height: '0.7em' }} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalletsButton;
|
File diff suppressed because it is too large
Load Diff
687
frontend/src/components/TradeBox/index.tsx
Normal file
687
frontend/src/components/TradeBox/index.tsx
Normal file
@ -0,0 +1,687 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Divider, Grid } from '@mui/material';
|
||||
|
||||
import { apiClient } from '../../services/api';
|
||||
import { getWebln, pn } from '../../utils';
|
||||
|
||||
import {
|
||||
ConfirmCancelDialog,
|
||||
ConfirmCollabCancelDialog,
|
||||
ConfirmDisputeDialog,
|
||||
ConfirmFiatReceivedDialog,
|
||||
WebLNDialog,
|
||||
} from './Dialogs';
|
||||
|
||||
import Title from './Title';
|
||||
import {
|
||||
LockInvoicePrompt,
|
||||
TakerFoundPrompt,
|
||||
PublicWaitPrompt,
|
||||
PausedPrompt,
|
||||
ExpiredPrompt,
|
||||
PayoutPrompt,
|
||||
PayoutWaitPrompt,
|
||||
EscrowWaitPrompt,
|
||||
ChatPrompt,
|
||||
DisputePrompt,
|
||||
DisputeWaitPeerPrompt,
|
||||
DisputeWaitResolutionPrompt,
|
||||
SendingSatsPrompt,
|
||||
SuccessfulPrompt,
|
||||
RoutingFailedPrompt,
|
||||
DisputeWinnerPrompt,
|
||||
DisputeLoserPrompt,
|
||||
} from './Prompts';
|
||||
import BondStatus from './BondStatus';
|
||||
import CancelButton from './CancelButton';
|
||||
import {
|
||||
defaultLightning,
|
||||
LightningForm,
|
||||
defaultOnchain,
|
||||
OnchainForm,
|
||||
DisputeForm,
|
||||
defaultDispute,
|
||||
} from './Forms';
|
||||
|
||||
import { Order } from '../../models';
|
||||
import { EncryptedChatMessage } from './EncryptedChat';
|
||||
import { systemClient } from '../../services/System';
|
||||
import CollabCancelAlert from './CollabCancelAlert';
|
||||
import { Bolt } from '@mui/icons-material';
|
||||
|
||||
interface loadingButtonsProps {
|
||||
cancel: boolean;
|
||||
fiatSent: boolean;
|
||||
fiatReceived: boolean;
|
||||
submitInvoice: boolean;
|
||||
submitAddress: boolean;
|
||||
submitStatement: boolean;
|
||||
openDispute: boolean;
|
||||
pauseOrder: boolean;
|
||||
renewOrder: boolean;
|
||||
}
|
||||
|
||||
const noLoadingButtons: loadingButtonsProps = {
|
||||
cancel: false,
|
||||
fiatSent: false,
|
||||
fiatReceived: false,
|
||||
submitInvoice: false,
|
||||
submitAddress: false,
|
||||
submitStatement: false,
|
||||
openDispute: false,
|
||||
pauseOrder: false,
|
||||
renewOrder: false,
|
||||
};
|
||||
|
||||
interface OpenDialogProps {
|
||||
confirmCancel: boolean;
|
||||
confirmCollabCancel: boolean;
|
||||
confirmFiatReceived: boolean;
|
||||
confirmDispute: boolean;
|
||||
webln: boolean;
|
||||
}
|
||||
|
||||
const closeAll: OpenDialogProps = {
|
||||
confirmCancel: false,
|
||||
confirmCollabCancel: false,
|
||||
confirmFiatReceived: false,
|
||||
confirmDispute: false,
|
||||
webln: false,
|
||||
};
|
||||
|
||||
interface TradeBoxProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
setBadOrder: (state: string | undefined) => void;
|
||||
onRenewOrder: () => void;
|
||||
onStartAgain: () => void;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const TradeBox = ({
|
||||
order,
|
||||
setOrder,
|
||||
baseUrl,
|
||||
setBadOrder,
|
||||
onRenewOrder,
|
||||
onStartAgain,
|
||||
}: TradeBoxProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Buttons and Dialogs
|
||||
const [loadingButtons, setLoadingButtons] = useState<loadingButtonsProps>(noLoadingButtons);
|
||||
const [open, setOpen] = useState<OpenDialogProps>(closeAll);
|
||||
const [waitingWebln, setWaitingWebln] = useState<boolean>(false);
|
||||
const [lastOrderStatus, setLastOrderStatus] = useState<number>(-1);
|
||||
|
||||
// Forms
|
||||
const [onchain, setOnchain] = useState<OnchainForm>(defaultOnchain);
|
||||
const [lightning, setLightning] = useState<LightningForm>(defaultLightning);
|
||||
const [dispute, setDispute] = useState<DisputeForm>(defaultDispute);
|
||||
|
||||
// Chat
|
||||
const [messages, setMessages] = useState<EncryptedChatMessage[]>([]);
|
||||
|
||||
interface SubmitActionProps {
|
||||
action:
|
||||
| 'cancel'
|
||||
| 'dispute'
|
||||
| 'pause'
|
||||
| 'confirm'
|
||||
| 'update_invoice'
|
||||
| 'update_address'
|
||||
| 'submit_statement'
|
||||
| 'rate_platform';
|
||||
invoice?: string;
|
||||
address?: string;
|
||||
mining_fee_rate?: number;
|
||||
statement?: string;
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
const submitAction = function ({
|
||||
action,
|
||||
invoice,
|
||||
address,
|
||||
mining_fee_rate,
|
||||
statement,
|
||||
rating,
|
||||
}: SubmitActionProps) {
|
||||
apiClient
|
||||
.post(baseUrl, '/api/order/?order_id=' + order.id, {
|
||||
action,
|
||||
invoice,
|
||||
address,
|
||||
mining_fee_rate,
|
||||
statement,
|
||||
rating,
|
||||
})
|
||||
.catch(() => {
|
||||
setOpen(closeAll);
|
||||
setLoadingButtons({ ...noLoadingButtons });
|
||||
})
|
||||
.then((data: Order) => {
|
||||
setOpen(closeAll);
|
||||
setLoadingButtons({ ...noLoadingButtons });
|
||||
if (data.bad_request) {
|
||||
setBadOrder(data.bad_request);
|
||||
} else if (data.bad_address) {
|
||||
setOnchain({ ...onchain, badAddress: data.bad_address });
|
||||
} else if (data.bad_invoice) {
|
||||
setLightning({ ...lightning, badInvoice: data.bad_invoice });
|
||||
} else if (data.bad_statement) {
|
||||
setDispute({ ...dispute, badStatement: data.bad_statement });
|
||||
} else {
|
||||
setOrder({ ...order, ...data });
|
||||
setBadOrder(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = function () {
|
||||
setLoadingButtons({ ...noLoadingButtons, cancel: true });
|
||||
submitAction({ action: 'cancel' });
|
||||
};
|
||||
|
||||
const openDispute = function () {
|
||||
setLoadingButtons({ ...noLoadingButtons, openDispute: true });
|
||||
submitAction({ action: 'dispute' });
|
||||
};
|
||||
|
||||
const confirmFiatReceived = function () {
|
||||
setLoadingButtons({ ...noLoadingButtons, fiatReceived: true });
|
||||
submitAction({ action: 'confirm' });
|
||||
};
|
||||
|
||||
const confirmFiatSent = function () {
|
||||
setLoadingButtons({ ...noLoadingButtons, fiatSent: true });
|
||||
submitAction({ action: 'confirm' });
|
||||
};
|
||||
|
||||
const updateInvoice = function (invoice: string) {
|
||||
setLoadingButtons({ ...noLoadingButtons, submitInvoice: true });
|
||||
submitAction({ action: 'update_invoice', invoice });
|
||||
};
|
||||
|
||||
const updateAddress = function () {
|
||||
setLoadingButtons({ ...noLoadingButtons, submitAddress: true });
|
||||
submitAction({
|
||||
action: 'update_address',
|
||||
address: onchain.address,
|
||||
mining_fee_rate: onchain.miningFee,
|
||||
});
|
||||
};
|
||||
|
||||
const pauseOrder = function () {
|
||||
setLoadingButtons({ ...noLoadingButtons, pauseOrder: true });
|
||||
submitAction({ action: 'pause' });
|
||||
};
|
||||
|
||||
const submitStatement = function () {
|
||||
let statement = dispute.statement;
|
||||
if (dispute.attachLogs) {
|
||||
const payload = { statement, messages, token: systemClient.getItem('robot_token') };
|
||||
statement = JSON.stringify(payload, null, 2);
|
||||
}
|
||||
setLoadingButtons({ ...noLoadingButtons, submitStatement: true });
|
||||
submitAction({ action: 'submit_statement', statement });
|
||||
};
|
||||
const ratePlatform = function (rating: number) {
|
||||
submitAction({ action: 'rate_platform', rating });
|
||||
};
|
||||
|
||||
const handleWebln = async (order: Order) => {
|
||||
const webln = await getWebln().catch(() => console.log('WebLN not available'));
|
||||
// If Webln implements locked payments compatibility, this logic might be simplier
|
||||
if (webln == undefined) {
|
||||
return null;
|
||||
} else if (order.is_maker && order.status == 0) {
|
||||
webln.sendPayment(order.bond_invoice);
|
||||
setWaitingWebln(true);
|
||||
setOpen({ ...open, webln: true });
|
||||
} else if (order.is_taker && order.status == 3) {
|
||||
webln.sendPayment(order.bond_invoice);
|
||||
setWaitingWebln(true);
|
||||
setOpen({ ...open, webln: true });
|
||||
} else if (order.is_seller && (order.status == 6 || order.status == 7)) {
|
||||
webln.sendPayment(order.escrow_invoice);
|
||||
setWaitingWebln(true);
|
||||
setOpen({ ...open, webln: true });
|
||||
} else if (order.is_buyer && (order.status == 6 || order.status == 8)) {
|
||||
setWaitingWebln(true);
|
||||
setOpen({ ...open, webln: true });
|
||||
webln
|
||||
.makeInvoice(order.trade_satoshis)
|
||||
.then((invoice: any) => {
|
||||
if (invoice) {
|
||||
updateInvoice(invoice.paymentRequest);
|
||||
setWaitingWebln(false);
|
||||
setOpen(closeAll);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setWaitingWebln(false);
|
||||
setOpen(closeAll);
|
||||
});
|
||||
} else {
|
||||
setWaitingWebln(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Effect on Order Status change (used for WebLN)
|
||||
useEffect(() => {
|
||||
if (order.status != lastOrderStatus) {
|
||||
setLastOrderStatus(order.status);
|
||||
handleWebln(order);
|
||||
}
|
||||
}, [order.status]);
|
||||
|
||||
const statusToContract = function (order: Order) {
|
||||
const status = order.status;
|
||||
const isBuyer = order.is_buyer;
|
||||
const isMaker = order.is_maker;
|
||||
|
||||
let title: string = 'Unknown Order Status';
|
||||
let titleVariables: object = {};
|
||||
let titleColor: string = 'primary';
|
||||
let titleIcon: () => JSX.Element = function () {
|
||||
return <></>;
|
||||
};
|
||||
let prompt = () => <span>Wops!</span>;
|
||||
let bondStatus: 'hide' | 'locked' | 'unlocked' | 'settled' = 'hide';
|
||||
|
||||
// 0: 'Waiting for maker bond'
|
||||
if (status == 0) {
|
||||
if (isMaker) {
|
||||
title = 'Lock {{amountSats}} Sats to PUBLISH order';
|
||||
titleVariables = { amountSats: pn(order.bond_satoshis) };
|
||||
prompt = () => {
|
||||
return <LockInvoicePrompt order={order} concept={'bond'} />;
|
||||
};
|
||||
bondStatus = 'hide';
|
||||
}
|
||||
|
||||
// 1: 'Public'
|
||||
} else if (status == 1) {
|
||||
if (isMaker) {
|
||||
title = 'Your order is public';
|
||||
prompt = () => {
|
||||
return (
|
||||
<PublicWaitPrompt
|
||||
order={order}
|
||||
pauseLoading={loadingButtons.pauseOrder}
|
||||
onClickPauseOrder={pauseOrder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
bondStatus = 'locked';
|
||||
}
|
||||
|
||||
// 2: 'Paused'
|
||||
} else if (status == 2) {
|
||||
if (isMaker) {
|
||||
title = 'Your order is paused';
|
||||
prompt = () => {
|
||||
return (
|
||||
<PausedPrompt
|
||||
pauseLoading={loadingButtons.pauseOrder}
|
||||
onClickResumeOrder={pauseOrder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
bondStatus = 'locked';
|
||||
}
|
||||
|
||||
// 3: 'Waiting for taker bond'
|
||||
} else if (status == 3) {
|
||||
if (isMaker) {
|
||||
title = 'A taker has been found!';
|
||||
prompt = () => {
|
||||
return <TakerFoundPrompt />;
|
||||
};
|
||||
bondStatus = 'locked';
|
||||
} else {
|
||||
title = 'Lock {{amountSats}} Sats to TAKE order';
|
||||
titleVariables = { amountSats: pn(order.bond_satoshis) };
|
||||
prompt = () => {
|
||||
return <LockInvoicePrompt order={order} concept={'bond'} />;
|
||||
};
|
||||
bondStatus = 'hide';
|
||||
}
|
||||
|
||||
// 5: 'Expired'
|
||||
} else if (status == 5) {
|
||||
title = 'The order has expired';
|
||||
prompt = () => {
|
||||
return (
|
||||
<ExpiredPrompt
|
||||
loadingRenew={loadingButtons.renewOrder}
|
||||
order={order}
|
||||
onClickRenew={() => {
|
||||
onRenewOrder();
|
||||
setLoadingButtons({ ...noLoadingButtons, renewOrder: true });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
bondStatus = 'hide'; // To do: show bond status according to expiry message.
|
||||
|
||||
// 6: 'Waiting for trade collateral and buyer invoice'
|
||||
} else if (status == 6) {
|
||||
bondStatus = 'locked';
|
||||
if (isBuyer) {
|
||||
title = 'Submit payout info for {{amountSats}} Sats';
|
||||
titleVariables = { amountSats: pn(order.invoice_amount) };
|
||||
prompt = function () {
|
||||
return (
|
||||
<PayoutPrompt
|
||||
order={order}
|
||||
onClickSubmitInvoice={updateInvoice}
|
||||
loadingLightning={loadingButtons.submitInvoice}
|
||||
lightning={lightning}
|
||||
setLightning={setLightning}
|
||||
onClickSubmitAddress={updateAddress}
|
||||
loadingOnchain={loadingButtons.submitAddress}
|
||||
onchain={onchain}
|
||||
setOnchain={setOnchain}
|
||||
/>
|
||||
);
|
||||
};
|
||||
} else {
|
||||
title = 'Lock {{amountSats}} Sats as collateral';
|
||||
titleVariables = { amountSats: pn(order.escrow_satoshis) };
|
||||
titleColor = 'warning';
|
||||
prompt = () => {
|
||||
return <LockInvoicePrompt order={order} concept={'escrow'} />;
|
||||
};
|
||||
}
|
||||
|
||||
// 7: 'Waiting only for seller trade collateral'
|
||||
} else if (status == 7) {
|
||||
bondStatus = 'locked';
|
||||
if (isBuyer) {
|
||||
title = 'Your info looks good!';
|
||||
prompt = () => {
|
||||
return <PayoutWaitPrompt />;
|
||||
};
|
||||
} else {
|
||||
title = 'Lock {{amountSats}} Sats as collateral';
|
||||
titleVariables = { amountSats: pn(order.escrow_satoshis) };
|
||||
titleColor = 'warning';
|
||||
prompt = () => {
|
||||
return <LockInvoicePrompt order={order} concept={'escrow'} />;
|
||||
};
|
||||
}
|
||||
|
||||
// 8: 'Waiting only for buyer invoice'
|
||||
} else if (status == 8) {
|
||||
bondStatus = 'locked';
|
||||
if (isBuyer) {
|
||||
title = 'Submit payout info for {{amountSats}} Sats';
|
||||
titleVariables = { amountSats: pn(order.invoice_amount) };
|
||||
prompt = () => {
|
||||
return (
|
||||
<PayoutPrompt
|
||||
order={order}
|
||||
onClickSubmitInvoice={updateInvoice}
|
||||
loadingLightning={loadingButtons.submitInvoice}
|
||||
lightning={lightning}
|
||||
setLightning={setLightning}
|
||||
onClickSubmitAddress={updateAddress}
|
||||
loadingOnchain={loadingButtons.submitAddress}
|
||||
onchain={onchain}
|
||||
setOnchain={setOnchain}
|
||||
/>
|
||||
);
|
||||
};
|
||||
} else {
|
||||
title = 'The trade collateral is locked!';
|
||||
prompt = () => {
|
||||
return <EscrowWaitPrompt />;
|
||||
};
|
||||
}
|
||||
|
||||
// 9: 'Sending fiat - In chatroom'
|
||||
// 10: 'Fiat sent - In chatroom'
|
||||
} else if (status == 9 || status == 10) {
|
||||
title = isBuyer ? 'Chat with the seller' : 'Chat with the buyer';
|
||||
prompt = function () {
|
||||
return (
|
||||
<ChatPrompt
|
||||
order={order}
|
||||
onClickConfirmSent={confirmFiatSent}
|
||||
onClickConfirmReceived={() => setOpen({ ...open, confirmFiatReceived: true })}
|
||||
loadingSent={loadingButtons.fiatSent}
|
||||
loadingReceived={loadingButtons.fiatReceived}
|
||||
onClickDispute={() => setOpen({ ...open, confirmDispute: true })}
|
||||
loadingDispute={loadingButtons.openDispute}
|
||||
baseUrl={baseUrl}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
/>
|
||||
);
|
||||
};
|
||||
bondStatus = 'locked';
|
||||
|
||||
// 11: 'In dispute'
|
||||
} else if (status == 11) {
|
||||
bondStatus = 'settled';
|
||||
if (order.statement_submitted) {
|
||||
title = 'We have received your statement';
|
||||
prompt = function () {
|
||||
return <DisputeWaitPeerPrompt />;
|
||||
};
|
||||
} else {
|
||||
title = 'A dispute has been opened';
|
||||
prompt = function () {
|
||||
return (
|
||||
<DisputePrompt
|
||||
loading={loadingButtons.submitStatement}
|
||||
dispute={dispute}
|
||||
setDispute={setDispute}
|
||||
onClickSubmit={submitStatement}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
// 12: 'Collaboratively cancelled'
|
||||
} else if (status == 12) {
|
||||
// 13: 'Sending satoshis to buyer'
|
||||
} else if (status == 13) {
|
||||
if (isBuyer) {
|
||||
bondStatus = 'unlocked';
|
||||
title = 'Attempting Lightning Payment';
|
||||
prompt = function () {
|
||||
return <SendingSatsPrompt />;
|
||||
};
|
||||
} else {
|
||||
title = 'Trade finished!';
|
||||
titleColor = 'success';
|
||||
titleIcon = function () {
|
||||
return <Bolt xs={{ width: '1em', height: '1em' }} color='warning' />;
|
||||
};
|
||||
prompt = function () {
|
||||
return (
|
||||
<SuccessfulPrompt
|
||||
baseUrl={baseUrl}
|
||||
order={order}
|
||||
ratePlatform={ratePlatform}
|
||||
onClickStartAgain={onStartAgain}
|
||||
loadingRenew={loadingButtons.renewOrder}
|
||||
onClickRenew={() => {
|
||||
onRenewOrder();
|
||||
setLoadingButtons({ ...noLoadingButtons, renewOrder: true });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 14: 'Sucessful trade'
|
||||
} else if (status == 14) {
|
||||
title = 'Trade finished!';
|
||||
titleColor = 'success';
|
||||
titleIcon = function () {
|
||||
return <Bolt xs={{ width: '1em', height: '1em' }} color='warning' />;
|
||||
};
|
||||
prompt = function () {
|
||||
return (
|
||||
<SuccessfulPrompt
|
||||
baseUrl={baseUrl}
|
||||
order={order}
|
||||
ratePlatform={ratePlatform}
|
||||
onClickStartAgain={onStartAgain}
|
||||
loadingRenew={loadingButtons.renewOrder}
|
||||
onClickRenew={() => {
|
||||
onRenewOrder();
|
||||
setLoadingButtons({ ...noLoadingButtons, renewOrder: true });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
// 15: 'Failed lightning network routing'
|
||||
} else if (status == 15) {
|
||||
if (isBuyer) {
|
||||
bondStatus = 'unlocked';
|
||||
title = 'Lightning Routing Failed';
|
||||
prompt = function () {
|
||||
return (
|
||||
<RoutingFailedPrompt
|
||||
order={order}
|
||||
onClickSubmitInvoice={updateInvoice}
|
||||
loadingLightning={loadingButtons.submitInvoice}
|
||||
lightning={lightning}
|
||||
setLightning={setLightning}
|
||||
/>
|
||||
);
|
||||
};
|
||||
} else {
|
||||
title = 'Trade finished!';
|
||||
titleColor = 'success';
|
||||
titleIcon = function () {
|
||||
return <Bolt xs={{ width: '1em', height: '1em' }} color='warning' />;
|
||||
};
|
||||
prompt = function () {
|
||||
return (
|
||||
<SuccessfulPrompt
|
||||
baseUrl={baseUrl}
|
||||
order={order}
|
||||
ratePlatform={ratePlatform}
|
||||
onClickStartAgain={onStartAgain}
|
||||
loadingRenew={loadingButtons.renewOrder}
|
||||
onClickRenew={() => {
|
||||
onRenewOrder();
|
||||
setLoadingButtons({ ...noLoadingButtons, renewOrder: true });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 16: 'Wait for dispute resolution'
|
||||
} else if (status == 16) {
|
||||
bondStatus = 'settled';
|
||||
title = 'We have the statements';
|
||||
prompt = function () {
|
||||
return <DisputeWaitResolutionPrompt />;
|
||||
};
|
||||
|
||||
// 17: 'Maker lost dispute'
|
||||
// 18: 'Taker lost dispute'
|
||||
} else if ((status == 17 && isMaker) || (status == 18 && !isMaker)) {
|
||||
title = 'You have won the dispute';
|
||||
prompt = function () {
|
||||
return <DisputeWinnerPrompt />;
|
||||
};
|
||||
} else if ((status == 17 && !isMaker) || (status == 18 && isMaker)) {
|
||||
title = 'You have lost the dispute';
|
||||
prompt = function () {
|
||||
return <DisputeLoserPrompt />;
|
||||
};
|
||||
bondStatus = 'settled';
|
||||
}
|
||||
|
||||
return { title, titleVariables, titleColor, prompt, bondStatus, titleIcon };
|
||||
};
|
||||
|
||||
const contract = statusToContract(order);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<WebLNDialog
|
||||
open={open.webln}
|
||||
onClose={() => setOpen(closeAll)}
|
||||
waitingWebln={waitingWebln}
|
||||
isBuyer={order.is_buyer}
|
||||
/>
|
||||
<ConfirmDisputeDialog
|
||||
open={open.confirmDispute}
|
||||
onClose={() => setOpen(closeAll)}
|
||||
onAgreeClick={openDispute}
|
||||
/>
|
||||
<ConfirmCancelDialog
|
||||
open={open.confirmCancel}
|
||||
onClose={() => setOpen(closeAll)}
|
||||
onCancelClick={cancel}
|
||||
/>
|
||||
<ConfirmCollabCancelDialog
|
||||
open={open.confirmCollabCancel}
|
||||
onClose={() => setOpen(closeAll)}
|
||||
onCollabCancelClick={cancel}
|
||||
loading={loadingButtons.cancel}
|
||||
peerAskedCancel={order.pending_cancel}
|
||||
/>
|
||||
<ConfirmFiatReceivedDialog
|
||||
open={open.confirmFiatReceived}
|
||||
order={order}
|
||||
loadingButton={loadingButtons.fiatReceived}
|
||||
onClose={() => setOpen(closeAll)}
|
||||
onConfirmClick={confirmFiatReceived}
|
||||
/>
|
||||
<CollabCancelAlert order={order} />
|
||||
<Grid
|
||||
container
|
||||
padding={1}
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Title
|
||||
order={order}
|
||||
text={contract.title}
|
||||
color={contract.titleColor}
|
||||
icon={contract.titleIcon}
|
||||
variables={contract.titleVariables}
|
||||
/>
|
||||
</Grid>
|
||||
<Divider />
|
||||
|
||||
<Grid item>{contract.prompt()}</Grid>
|
||||
|
||||
{contract.bondStatus != 'hide' ? (
|
||||
<Grid item sx={{ width: '100%' }}>
|
||||
<Divider />
|
||||
<BondStatus status={contract.bondStatus} isMaker={order.is_maker} />
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<CancelButton
|
||||
order={order}
|
||||
onClickCancel={cancel}
|
||||
openCancelDialog={() => setOpen({ ...closeAll, confirmCancel: true })}
|
||||
openCollabCancelDialog={() => setOpen({ ...closeAll, confirmCollabCancel: true })}
|
||||
loading={loadingButtons.cancel}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeBox;
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,5 @@
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
export interface Info {
|
||||
num_public_buy_orders: number;
|
||||
num_public_sell_orders: number;
|
||||
@ -23,8 +25,6 @@ export interface Info {
|
||||
openUpdateClient: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
const semver = packageJson.version.split('.');
|
||||
|
||||
export const defaultInfo: Info = {
|
||||
|
@ -14,6 +14,7 @@ export interface TradeRobotSummary {
|
||||
export interface TradeCoordinatorSummary {
|
||||
contract_timestamp: Date;
|
||||
contract_total_time: number;
|
||||
contract_exchange_rate: number;
|
||||
routing_fee_sats: number;
|
||||
trade_revenue_sats: number;
|
||||
}
|
||||
@ -38,13 +39,14 @@ export interface Order {
|
||||
taker: number;
|
||||
escrow_duration: number;
|
||||
total_secs_exp: number;
|
||||
penalty: Date;
|
||||
penalty: Date | undefined;
|
||||
is_maker: boolean;
|
||||
is_taker: boolean;
|
||||
is_participant: boolean;
|
||||
maker_status: 'Active' | 'Seen recently' | 'Inactive';
|
||||
taker_status: 'Active' | 'Seen recently' | 'Inactive';
|
||||
price_now: number;
|
||||
price_now: number | undefined;
|
||||
premium_now: number | undefined;
|
||||
premium_percentile: number;
|
||||
num_similar_orders: number;
|
||||
tg_enabled: boolean; // deprecated
|
||||
|
@ -7,6 +7,7 @@ class Settings extends BaseSettings {
|
||||
const fontSizeCookie = systemClient.getItem('settings_fontsize_basic');
|
||||
this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 14;
|
||||
}
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'basic';
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ class Settings extends BaseSettings {
|
||||
const fontSizeCookie = systemClient.getItem('settings_fontsize_pro');
|
||||
this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 12;
|
||||
}
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'pro';
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ const ToolBar = ({ height = '3em', settings, setSettings }: ToolBarProps): JSX.E
|
||||
elevation={6}
|
||||
sx={{
|
||||
width: `100%`,
|
||||
height: height,
|
||||
height,
|
||||
textAlign: 'center',
|
||||
padding: '1em',
|
||||
borderRadius: 0,
|
||||
|
@ -51,9 +51,11 @@ class SystemNativeClient implements SystemClient {
|
||||
public getItem: (key: string) => string = (key) => {
|
||||
return this.getCookie(key);
|
||||
};
|
||||
|
||||
public setItem: (key: string, value: string) => void = (key, value) => {
|
||||
this.setCookie(key, value);
|
||||
};
|
||||
|
||||
public deleteItem: (key: string) => void = (key) => {
|
||||
this.deleteCookie(key);
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ class SystemWebClient implements SystemClient {
|
||||
}
|
||||
};
|
||||
|
||||
//Cookies
|
||||
// Cookies
|
||||
public getCookie: (key: string) => string = (key) => {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
@ -44,9 +44,11 @@ class SystemWebClient implements SystemClient {
|
||||
|
||||
return cookieValue || '';
|
||||
};
|
||||
|
||||
public setCookie: (key: string, value: string) => void = (key, value) => {
|
||||
document.cookie = `${key}=${value};path=/;SameSite=Strict`;
|
||||
};
|
||||
|
||||
public deleteCookie: (key: string) => void = (key) => {
|
||||
document.cookie = `${name}= ; expires = Thu, 01 Jan 1970 00:00:00 GMT`;
|
||||
};
|
||||
@ -56,9 +58,11 @@ class SystemWebClient implements SystemClient {
|
||||
const value = window.localStorage.getItem(key);
|
||||
return value || '';
|
||||
};
|
||||
|
||||
public setItem: (key: string, value: string) => void = (key, value) => {
|
||||
window.localStorage.setItem(key, value);
|
||||
};
|
||||
|
||||
public deleteItem: (key: string) => void = (key) => {
|
||||
window.localStorage.removeItem(key);
|
||||
};
|
||||
|
@ -17,6 +17,10 @@ class WebsocketConnectionWeb implements WebsocketConnection {
|
||||
);
|
||||
};
|
||||
|
||||
public close: () => void = () => {
|
||||
this.rws.close();
|
||||
};
|
||||
|
||||
public onMessage: (event: (message: any) => void) => void = (event) => {
|
||||
this.rws.addEventListener('message', event);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ export interface WebsocketConnection {
|
||||
onMessage: (event: (message: any) => void) => void;
|
||||
onClose: (event: () => void) => void;
|
||||
onError: (event: () => void) => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface WebsocketClient {
|
||||
|
@ -19,6 +19,7 @@ class ApiWebClient implements ApiClient {
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
|
||||
return await fetch(baseUrl + path, requestOptions).then(
|
||||
async (response) => await response.json(),
|
||||
);
|
||||
|
@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
const saveAsJson = (filename, dataObjToWrite) => {
|
||||
console.log(filename, dataObjToWrite);
|
||||
const blob = new Blob([JSON.stringify(dataObjToWrite, null, 2)], { type: 'text/json' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
|
@ -349,7 +349,7 @@
|
||||
"Messages": "Messages",
|
||||
"Verified signature by {{nickname}}": "Verified signature by {{nickname}}",
|
||||
"Cannot verify signature of {{nickname}}": "Cannot verify signature of {{nickname}}",
|
||||
"Activate turtle mode (Use it when the connection is slow)": "Activate turtle mode (Use it when the connection is slow)",
|
||||
"Activate slow mode (use it when the connection is slow)": "Activate slow mode (use it when the connection is slow)",
|
||||
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
||||
"Contract Box": "Contract Box",
|
||||
"Robots show commitment to their peers": "Robots show commitment to their peers",
|
||||
|
@ -338,7 +338,7 @@
|
||||
"Messages": "Mensajes",
|
||||
"Verified signature by {{nickname}}": "Firma de {{nickname}} verificada",
|
||||
"Cannot verify signature of {{nickname}}": "No se pudo verificar la firma de {{nickname}}",
|
||||
"Activate turtle mode (Use it when the connection is slow)": "Activar modo tortuga (Úsalo cuando tu conexión es lenta)",
|
||||
"Activate slow mode (use it when the connection is slow)": "Activar modo lento (Úsalo cuando tu conexión es inestable)",
|
||||
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
||||
"Contract Box": "Contrato",
|
||||
"Robots show commitment to their peers": "Los Robots deben mostrar su compromiso",
|
||||
|
Loading…
Reference in New Issue
Block a user