Add App context (#369)

* Refactor fetchRobot

* Add appcontext

* Robot recovery fixes

* Add usecontext on maker and settings forms

* Add usecontext to booktable

* Add useContext to order page and main dialogs

* Small fixes
This commit is contained in:
Reckless_Satoshi 2023-02-24 19:17:13 +00:00 committed by GitHub
parent d815fb8a8e
commit d88c2a5eff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 825 additions and 1016 deletions

View File

@ -1,5 +1,8 @@
## What does this PR do? ## What does this PR do?
Fixes #<PR_NUMBER/> Fixes #<ISSUE_NUMBER>
This PR introduces/refactors/...
## Checklist before merging ## Checklist before merging
- [ ] Make sure you have installed and initialized [pre-commit](https://pre-commit.com). `pip install pre-commit` and `pre-commit install` - [ ] If it's a frontend feature, I have ran prettier `cd frontend; npm run format`. If it's a mobile app feature I ran `cd mobile; npm run format`.
- [ ] If I added new phrases to the user interface, I have ran prettier `cd frontend/static/locales; python collect_phrases.py` to collect them for translation.

View File

@ -21,17 +21,15 @@ repos:
- id: check-docstring-first - id: check-docstring-first
- repo: local - repo: local
hooks: hooks:
- id: collect-phrases # - id: collect-phrases
name: collect-phrases # name: Collect i18n phrases
stages: # stages:
- commit # - commit
- merge-commit # - merge-commit
language: system # language: system
files: ^frontend/src/ # files: ^frontend/
types_or: [javascript, jsx, ts, tsx] # uses https://github.com/pre-commit/identify # types_or: [javascript, jsx, ts, tsx] # uses https://github.com/pre-commit/identify
entry: bash -c 'cd frontend/static/locales && python3 collect_phrases.py' # entry: bash -c 'cd frontend/static/locales && python3 collect_phrases.py'
- repo: local
hooks:
- id: prettier-frontend - id: prettier-frontend
name: prettier-frontend name: prettier-frontend
stages: stages:

View File

@ -759,9 +759,8 @@ class UserView(APIView):
if last_order: if last_order:
context["last_order_id"] = last_order.id context["last_order_id"] = last_order.id
# Sends the welcome back message, only if created +3 mins ago # Sends the welcome back message.
if request.user.date_joined < (timezone.now() - timedelta(minutes=3)): context["found"] = "We found your Robot avatar. Welcome back!"
context["found"] = "We found your Robot avatar. Welcome back!"
return Response(context, status=status.HTTP_202_ACCEPTED) return Response(context, status=status.HTTP_202_ACCEPTED)
else: else:
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion chance) # It is unlikely, but maybe the nickname is taken (1 in 20 Billion chance)

View File

@ -2,6 +2,7 @@ import React, { Suspense, useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import Main from './basic/Main'; import Main from './basic/Main';
import { CssBaseline } from '@mui/material'; import { CssBaseline } from '@mui/material';
import { AppContextProvider } from './contexts/AppContext';
import { ThemeProvider, createTheme, Theme } from '@mui/material/styles'; import { ThemeProvider, createTheme, Theme } from '@mui/material/styles';
import UnsafeAlert from './components/UnsafeAlert'; import UnsafeAlert from './components/UnsafeAlert';
import TorConnectionBadge from './components/TorConnection'; import TorConnectionBadge from './components/TorConnection';
@ -29,14 +30,6 @@ const makeTheme = function (settings: Settings) {
const App = (): JSX.Element => { const App = (): JSX.Element => {
const [theme, setTheme] = useState<Theme>(makeTheme(new Settings())); const [theme, setTheme] = useState<Theme>(makeTheme(new Settings()));
const [settings, setSettings] = useState<Settings>(new Settings()); const [settings, setSettings] = useState<Settings>(new Settings());
const [torStatus, setTorStatus] = useState<string>('NOTINIT');
useEffect(() => {
window.addEventListener('torStatus', (event) => {
// UX improv: delay the "Conencted" status by 10 secs to avoid long waits for first requests
setTimeout(() => setTorStatus(event?.detail), event?.detail === '"Done"' ? 10000 : 0);
});
}, []);
useEffect(() => { useEffect(() => {
setTheme(makeTheme(settings)); setTheme(makeTheme(settings));
@ -50,13 +43,15 @@ const App = (): JSX.Element => {
<Suspense fallback='loading language'> <Suspense fallback='loading language'>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <AppContextProvider settings={settings} setSettings={setSettings}>
{window.NativeRobosats === undefined ? ( <CssBaseline />
<UnsafeAlert settings={settings} setSettings={setSettings} /> {window.NativeRobosats === undefined ? (
) : ( <UnsafeAlert settings={settings} setSettings={setSettings} />
<TorConnectionBadge torStatus={torStatus} /> ) : (
)} <TorConnectionBadge />
<Main settings={settings} setSettings={setSettings} torStatus={torStatus} /> )}
<Main />
</AppContextProvider>
</ThemeProvider> </ThemeProvider>
</I18nextProvider> </I18nextProvider>
</Suspense> </Suspense>

View File

@ -147,56 +147,36 @@ const BookPage = ({
> >
<Grid item> <Grid item>
<BookTable <BookTable
clickRefresh={() => fetchBook()}
book={book}
fav={fav}
setFav={setFav}
maxWidth={maxBookTableWidth} // EM units maxWidth={maxBookTableWidth} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units maxHeight={windowSize.height * 0.825 - 5} // EM units
fullWidth={windowSize.width} // EM units fullWidth={windowSize.width} // EM units
fullHeight={windowSize.height} // EM units fullHeight={windowSize.height} // EM units
defaultFullscreen={false} defaultFullscreen={false}
onOrderClicked={onOrderClicked} onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/> />
</Grid> </Grid>
<Grid item> <Grid item>
<DepthChart <DepthChart
orders={book.orders}
lastDayPremium={lastDayPremium}
currency={fav.currency}
limits={limits.list}
maxWidth={chartWidthEm} // EM units maxWidth={chartWidthEm} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units maxHeight={windowSize.height * 0.825 - 5} // EM units
onOrderClicked={onOrderClicked} onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/> />
</Grid> </Grid>
</Grid> </Grid>
) : view === 'depth' ? ( ) : view === 'depth' ? (
<DepthChart <DepthChart
orders={book.orders}
lastDayPremium={lastDayPremium}
currency={fav.currency}
limits={limits.list}
maxWidth={windowSize.width * 0.8} // EM units maxWidth={windowSize.width * 0.8} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units maxHeight={windowSize.height * 0.825 - 5} // EM units
onOrderClicked={onOrderClicked} onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/> />
) : ( ) : (
<BookTable <BookTable
book={book}
clickRefresh={() => fetchBook()}
fav={fav}
setFav={setFav}
maxWidth={windowSize.width * 0.97} // EM units maxWidth={windowSize.width * 0.97} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units maxHeight={windowSize.height * 0.825 - 5} // EM units
fullWidth={windowSize.width} // EM units fullWidth={windowSize.width} // EM units
fullHeight={windowSize.height} // EM units fullHeight={windowSize.height} // EM units
defaultFullscreen={false} defaultFullscreen={false}
onOrderClicked={onOrderClicked} onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/> />
)} )}
</Grid> </Grid>

View File

@ -1,312 +1,63 @@
import React, { useEffect, useState } from 'react'; import React, { useContext } from 'react';
import { HashRouter, BrowserRouter, Switch, Route } from 'react-router-dom'; import { HashRouter, BrowserRouter, Switch, Route } from 'react-router-dom';
import { useTheme, Box, Slide, Typography } from '@mui/material'; import { Box, Slide, Typography } from '@mui/material';
import RobotPage from './RobotPage'; import RobotPage from './RobotPage';
import MakerPage from './MakerPage'; import MakerPage from './MakerPage';
import BookPage from './BookPage'; import BookPage from './BookPage';
import OrderPage from './OrderPage'; import OrderPage from './OrderPage';
import SettingsPage from './SettingsPage'; import SettingsPage from './SettingsPage';
import NavBar, { Page } from './NavBar'; import NavBar from './NavBar';
import MainDialogs, { OpenDialogs } from './MainDialogs'; import MainDialogs from './MainDialogs';
import RobotAvatar from '../components/RobotAvatar'; import RobotAvatar from '../components/RobotAvatar';
import {
Book,
LimitList,
Maker,
Robot,
Info,
Settings,
Favorites,
defaultMaker,
defaultInfo,
Coordinator,
Order,
} from '../models';
import { apiClient } from '../services/api';
import { checkVer, getHost } from '../utils';
import { sha256 } from 'js-sha256';
import defaultCoordinators from '../../static/federation.json';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Notifications from '../components/Notifications'; import Notifications from '../components/Notifications';
import { AppContextProps, AppContext } from '../contexts/AppContext';
const getWindowSize = function (fontSize: number) { const Main = (): JSX.Element => {
// returns window size in EM units
return {
width: window.innerWidth / fontSize,
height: window.innerHeight / fontSize,
};
};
// 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;
}
interface MainProps {
settings: Settings;
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
setSettings: (state: Settings) => void;
}
const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const {
book,
// All app data structured fetchBook,
const [book, setBook] = useState<Book>({ orders: [], loading: true }); maker,
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({ setMaker,
list: [], clearOrder,
loading: true, torStatus,
}); settings,
const [robot, setRobot] = useState<Robot>(new Robot()); limits,
const [maker, setMaker] = useState<Maker>(defaultMaker); fetchLimits,
const [info, setInfo] = useState<Info>(defaultInfo); robot,
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators); setRobot,
const [baseUrl, setBaseUrl] = useState<string>(''); fetchRobot,
const [fav, setFav] = useState<Favorites>({ type: null, mode: 'fiat', currency: 0 }); setOrder,
setDelay,
const [delay, setDelay] = useState<number>(60000); info,
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(setInterval(() => null, delay)); fav,
const [order, setOrder] = useState<Order | undefined>(undefined); setFav,
const [badOrder, setBadOrder] = useState<string | undefined>(undefined); baseUrl,
order,
page,
setPage,
slideDirection,
setSlideDirection,
currentOrder,
setCurrentOrder,
closeAll,
open,
setOpen,
windowSize,
badOrder,
navbarHeight,
setBadOrder,
} = useContext<AppContextProps>(AppContext);
const Router = window.NativeRobosats === undefined ? BrowserRouter : HashRouter; const Router = window.NativeRobosats === undefined ? BrowserRouter : HashRouter;
const basename = window.NativeRobosats === undefined ? '' : window.location.pathname; const basename = window.NativeRobosats === undefined ? '' : window.location.pathname;
const entryPage: Page | '' =
window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : '';
const [page, setPage] = useState<Page>(entryPage == '' ? 'robot' : entryPage);
const [slideDirection, setSlideDirection] = useState<SlideDirection>({
in: undefined,
out: undefined,
});
const [currentOrder, setCurrentOrder] = useState<number | undefined>(undefined);
const navbarHeight = 2.5;
const closeAll = {
more: false,
learn: false,
community: false,
info: false,
coordinator: false,
stats: false,
update: false,
profile: false,
};
const [open, setOpen] = useState<OpenDialogs>(closeAll);
const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(
getWindowSize(theme.typography.fontSize),
);
useEffect(() => {
if (typeof window !== undefined) {
window.addEventListener('resize', onResize);
}
if (baseUrl != '') {
fetchBook();
fetchLimits();
}
return () => {
if (typeof window !== undefined) {
window.removeEventListener('resize', onResize);
}
};
}, [baseUrl]);
useEffect(() => {
let host = '';
if (window.NativeRobosats === undefined) {
host = getHost();
} else {
host =
settings.network === 'mainnet'
? coordinators[0].mainnetOnion
: coordinators[0].testnetOnion;
}
setBaseUrl(`${location.protocol}//${host}`);
}, [settings.network]);
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const onResize = function () {
setWindowSize(getWindowSize(theme.typography.fontSize));
};
const fetchBook = function () {
setBook({ ...book, loading: true });
apiClient.get(baseUrl, '/api/book/').then((data: any) =>
setBook({
loading: false,
orders: data.not_found ? [] : data,
}),
);
};
const fetchLimits = async () => {
setLimits({ ...limits, loading: true });
const data = apiClient.get(baseUrl, '/api/limits/').then((data) => {
setLimits({ list: data ?? [], loading: false });
return data;
});
return await data;
};
const fetchInfo = function () {
setInfo({ ...info, loading: true });
apiClient.get(baseUrl, '/api/info/').then((data: Info) => {
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
setInfo({
...data,
openUpdateClient: versionInfo.updateAvailable,
coordinatorVersion: versionInfo.coordinatorVersion,
clientVersion: versionInfo.clientVersion,
loading: false,
});
});
};
useEffect(() => {
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
fetchInfo();
}
}, [open.stats, open.coordinator]);
useEffect(() => {
// Sets Setting network from coordinator API param if accessing via web
if (settings.network == undefined && info.network) {
setSettings((settings: Settings) => {
return { ...settings, network: info.network };
});
}
}, [info]);
const fetchRobot = function ({ keys = false }) {
const requestBody = {
token_sha256: sha256(robot.token),
};
if (keys) {
requestBody.pub_key = robot.pubKey;
requestBody.enc_priv_key = robot.encPrivKey;
}
setRobot({ ...robot, loading: true });
apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => {
setCurrentOrder(
data.active_order_id
? data.active_order_id
: data.last_order_id
? data.last_order_id
: null,
);
setRobot({
...robot,
nickname: data.nickname,
token: robot.token,
loading: 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,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
bitsEntropy: data.token_bits_entropy,
shannonEntropy: data.token_shannon_entropy,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
copiedToken: data.found ? true : robot.copiedToken,
});
});
};
useEffect(() => {
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) {
fetchRobot({ keys: true }); // create new robot with existing token and keys (on network and coordinator change)
}
}
}, [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 ( return (
<Router basename={basename}> <Router basename={basename}>
{/* load robot avatar image, set avatarLoaded: true */}
<RobotAvatar <RobotAvatar
style={{ display: 'none' }} style={{ display: 'none' }}
nickname={robot.nickname} nickname={robot.nickname}
@ -353,6 +104,7 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
<RobotPage <RobotPage
setPage={setPage} setPage={setPage}
torStatus={torStatus} torStatus={torStatus}
fetchRobot={fetchRobot}
setCurrentOrder={setCurrentOrder} setCurrentOrder={setCurrentOrder}
windowSize={windowSize} windowSize={windowSize}
robot={robot} robot={robot}
@ -403,21 +155,7 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
appear={slideDirection.in != undefined} appear={slideDirection.in != undefined}
> >
<div> <div>
<MakerPage <MakerPage hasRobot={robot.avatarLoaded} />
book={book}
limits={limits}
fetchLimits={fetchLimits}
maker={maker}
setMaker={setMaker}
clearOrder={clearOrder}
setPage={setPage}
setCurrentOrder={setCurrentOrder}
fav={fav}
setFav={setFav}
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
hasRobot={robot.avatarLoaded}
baseUrl={baseUrl}
/>
</div> </div>
</Slide> </Slide>
</Route> </Route>
@ -432,17 +170,8 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
> >
<div> <div>
<OrderPage <OrderPage
baseUrl={baseUrl}
order={order}
settings={settings}
setOrder={setOrder}
setCurrentOrder={setCurrentOrder}
badOrder={badOrder}
locationOrderId={props.match.params.orderId} locationOrderId={props.match.params.orderId}
setBadOrder={setBadOrder}
hasRobot={robot.avatarLoaded} hasRobot={robot.avatarLoaded}
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
setPage={setPage}
/> />
</div> </div>
</Slide> </Slide>
@ -456,34 +185,14 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
appear={slideDirection.in != undefined} appear={slideDirection.in != undefined}
> >
<div> <div>
<SettingsPage <SettingsPage />
fav={fav}
setFav={setFav}
settings={settings}
setSettings={setSettings}
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
/>
</div> </div>
</Slide> </Slide>
</Route> </Route>
</Switch> </Switch>
</Box> </Box>
<div style={{ alignContent: 'center', display: 'flex' }}> <div style={{ alignContent: 'center', display: 'flex' }}>
<NavBar <NavBar width={windowSize.width} height={navbarHeight} hasRobot={robot.avatarLoaded} />
nickname={robot.avatarLoaded ? robot.nickname : null}
color={settings.network === 'mainnet' ? 'primary' : 'secondary'}
width={windowSize.width}
height={navbarHeight}
page={page}
setPage={setPage}
open={open}
setOpen={setOpen}
closeAll={closeAll}
setSlideDirection={setSlideDirection}
currentOrder={currentOrder}
hasRobot={robot.avatarLoaded}
baseUrl={baseUrl}
/>
</div> </div>
<MainDialogs <MainDialogs
open={open} open={open}

View File

@ -1,5 +1,4 @@
import React, { useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { Info, Robot } from '../../models';
import { import {
CommunityDialog, CommunityDialog,
CoordinatorSummaryDialog, CoordinatorSummaryDialog,
@ -9,7 +8,8 @@ import {
StatsDialog, StatsDialog,
UpdateClientDialog, UpdateClientDialog,
} from '../../components/Dialogs'; } from '../../components/Dialogs';
import { Page } from '../NavBar'; import { pn } from '../../utils';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
export interface OpenDialogs { export interface OpenDialogs {
more: boolean; more: boolean;
@ -19,32 +19,31 @@ export interface OpenDialogs {
coordinator: boolean; coordinator: boolean;
stats: boolean; stats: boolean;
update: boolean; update: boolean;
profile: boolean; // temporary until new Robot Page is ready profile: boolean;
} }
interface MainDialogsProps { const MainDialogs = (): JSX.Element => {
open: OpenDialogs; const {
setOpen: (state: OpenDialogs) => void; open,
info: Info; setOpen,
robot: Robot; info,
setRobot: (state: Robot) => void; limits,
setPage: (state: Page) => void; closeAll,
setCurrentOrder: (state: number) => void; robot,
closeAll: OpenDialogs; setRobot,
baseUrl: string; setPage,
} setCurrentOrder,
baseUrl,
} = useContext<AppContextProps>(AppContext);
const [maxAmount, setMaxAmount] = useState<string>('...loading...');
useEffect(() => {
if (limits.list[1000]) {
setMaxAmount(pn(limits.list[1000].max_amount * 100000000));
}
}, [limits.list]);
const MainDialogs = ({
open,
setOpen,
info,
closeAll,
robot,
setRobot,
setPage,
setCurrentOrder,
baseUrl,
}: MainDialogsProps): JSX.Element => {
useEffect(() => { useEffect(() => {
if (info.openUpdateClient) { if (info.openUpdateClient) {
setOpen({ ...closeAll, update: true }); setOpen({ ...closeAll, update: true });
@ -61,7 +60,7 @@ const MainDialogs = ({
/> />
<InfoDialog <InfoDialog
open={open.info} open={open.info}
maxAmount='4,000,000' maxAmount={maxAmount}
onClose={() => setOpen({ ...open, info: false })} onClose={() => setOpen({ ...open, info: false })}
/> />
<LearnDialog open={open.learn} onClose={() => setOpen({ ...open, learn: false })} /> <LearnDialog open={open.learn} onClose={() => setOpen({ ...open, learn: false })} />

View File

@ -1,52 +1,26 @@
import React, { useState } from 'react'; import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Grid, Paper, Collapse, Typography } from '@mui/material'; import { Grid, Paper, Collapse, Typography } from '@mui/material';
import { LimitList, Maker, Book, Favorites, Order } from '../../models';
import { filterOrders } from '../../utils'; import { filterOrders } from '../../utils';
import MakerForm from '../../components/MakerForm'; import MakerForm from '../../components/MakerForm';
import BookTable from '../../components/BookTable'; import BookTable from '../../components/BookTable';
import { Page } from '../NavBar'; import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface MakerPageProps { interface MakerPageProps {
limits: { list: LimitList; loading: boolean };
fetchLimits: () => void;
book: Book;
fav: Favorites;
maker: Maker;
setFav: (state: Favorites) => void;
setMaker: (state: Maker) => void;
clearOrder: () => void;
windowSize: { width: number; height: number };
hasRobot: boolean; hasRobot: boolean;
setCurrentOrder: (state: number) => void;
setPage: (state: Page) => void;
baseUrl: string;
} }
const MakerPage = ({ const MakerPage = ({ hasRobot = false }: MakerPageProps): JSX.Element => {
limits, const { book, fav, maker, clearOrder, windowSize, setCurrentOrder, navbarHeight, setPage } =
fetchLimits, useContext<AppContextProps>(AppContext);
book,
fav,
maker,
setFav,
setMaker,
clearOrder,
windowSize,
setCurrentOrder,
setPage,
hasRobot = false,
baseUrl,
}: MakerPageProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const maxHeight = windowSize.height * 0.85 - 3; const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
const [showMatches, setShowMatches] = useState<boolean>(false); const [showMatches, setShowMatches] = useState<boolean>(false);
const matches = filterOrders({ const matches = filterOrders({
@ -71,14 +45,13 @@ const MakerPage = ({
</Grid> </Grid>
<Grid item> <Grid item>
<BookTable <BookTable
book={{ orders: matches, loading: book.loading }} orderList={matches}
maxWidth={Math.min(windowSize.width, 60)} // EM units maxWidth={Math.min(windowSize.width, 60)} // EM units
maxHeight={Math.min(matches.length * 3.25 + 3, 16)} // EM units maxHeight={Math.min(matches.length * 3.25 + 3, 16)} // EM units
defaultFullscreen={false} defaultFullscreen={false}
showControls={false} showControls={false}
showFooter={false} showFooter={false}
showNoResults={false} showNoResults={false}
baseUrl={baseUrl}
/> />
</Grid> </Grid>
</Grid> </Grid>
@ -95,12 +68,6 @@ const MakerPage = ({
}} }}
> >
<MakerForm <MakerForm
limits={limits}
fetchLimits={fetchLimits}
fav={fav}
setFav={setFav}
maker={maker}
setMaker={setMaker}
onOrderCreated={(id) => { onOrderCreated={(id) => {
clearOrder(); clearOrder();
setCurrentOrder(id); setCurrentOrder(id);
@ -113,8 +80,6 @@ const MakerPage = ({
onSubmit={() => setShowMatches(matches.length > 0)} onSubmit={() => setShowMatches(matches.length > 0)}
onReset={() => setShowMatches(false)} onReset={() => setShowMatches(false)}
submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'} submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'}
setPage={setPage}
baseUrl={baseUrl}
/> />
</Paper> </Paper>
</Grid> </Grid>

View File

@ -21,19 +21,12 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
interface MoreTooltipProps { interface MoreTooltipProps {
open: OpenDialogs; open: OpenDialogs;
nickname: string | null;
setOpen: (state: OpenDialogs) => void; setOpen: (state: OpenDialogs) => void;
closeAll: OpenDialogs; closeAll: OpenDialogs;
children: JSX.Element; children: JSX.Element;
} }
const MoreTooltip = ({ const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): JSX.Element => {
open,
setOpen,
closeAll,
nickname,
children,
}: MoreTooltipProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
return ( return (

View File

@ -1,11 +1,9 @@
import React, { useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Tabs, Tab, Paper, useTheme } from '@mui/material'; import { Tabs, Tab, Paper, useTheme } from '@mui/material';
import MoreTooltip from './MoreTooltip'; import MoreTooltip from './MoreTooltip';
import { OpenDialogs } from '../MainDialogs';
import { Page } from '.'; import { Page } from '.';
import { import {
@ -17,47 +15,35 @@ import {
MoreHoriz, MoreHoriz,
} from '@mui/icons-material'; } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar'; import RobotAvatar from '../../components/RobotAvatar';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
type Direction = 'left' | 'right' | undefined;
interface NavBarProps { interface NavBarProps {
page: Page;
nickname?: string | null;
setPage: (state: Page) => void;
setSlideDirection: (state: { in: Direction; out: Direction }) => void;
width: number; width: number;
height: number; height: number;
open: OpenDialogs;
setOpen: (state: OpenDialogs) => void;
closeAll: OpenDialogs;
currentOrder: number | undefined;
hasRobot: boolean; hasRobot: boolean;
baseUrl: string;
color: 'primary' | 'secondary';
} }
const NavBar = ({ const NavBar = ({ width, height, hasRobot = false }: NavBarProps): JSX.Element => {
page, const {
setPage, page,
setSlideDirection, settings,
open, setPage,
nickname = null, setSlideDirection,
setOpen, open,
closeAll, robot,
width, setOpen,
height, closeAll,
currentOrder, currentOrder,
hasRobot = false, baseUrl,
baseUrl, } = useContext<AppContextProps>(AppContext);
color,
}: NavBarProps): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const smallBar = width < 50; const smallBar = width < 50;
const tabSx = smallBar const tabSx = smallBar
? { position: 'relative', bottom: nickname ? '1em' : '0em', minWidth: '1em' } ? { position: 'relative', bottom: robot.nickname ? '1em' : '0em', minWidth: '1em' }
: { position: 'relative', bottom: '1em', minWidth: '2em' }; : { position: 'relative', bottom: '1em', minWidth: '2em' };
const pagesPosition = { const pagesPosition = {
robot: 1, robot: 1,
@ -102,21 +88,21 @@ const NavBar = ({
TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }} TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }}
variant='fullWidth' variant='fullWidth'
value={page} value={page}
indicatorColor={color} indicatorColor={settings.network === 'mainnet' ? 'primary' : 'secondary'}
textColor={color} textColor={settings.network === 'mainnet' ? 'primary' : 'secondary'}
onChange={changePage} onChange={changePage}
> >
<Tab <Tab
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }} sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
value='none' value='none'
disabled={nickname === null} disabled={robot.nickname === null}
onClick={() => setOpen({ ...closeAll, profile: !open.profile })} onClick={() => setOpen({ ...closeAll, profile: !open.profile })}
icon={ icon={
nickname ? ( robot.nickname && robot.avatarLoaded ? (
<RobotAvatar <RobotAvatar
style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }} style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }}
avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'} avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'}
nickname={nickname} nickname={robot.nickname}
baseUrl={baseUrl} baseUrl={baseUrl}
/> />
) : ( ) : (
@ -171,7 +157,7 @@ const NavBar = ({
open.more ? null : setOpen({ ...open, more: true }); open.more ? null : setOpen({ ...open, more: true });
}} }}
icon={ icon={
<MoreTooltip open={open} nickname={nickname} setOpen={setOpen} closeAll={closeAll}> <MoreTooltip open={open} setOpen={setOpen} closeAll={closeAll}>
<MoreHoriz /> <MoreHoriz />
</MoreTooltip> </MoreTooltip>
} }

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material'; import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@ -9,40 +9,31 @@ import OrderDetails from '../../components/OrderDetails';
import { Page } from '../NavBar'; import { Page } from '../NavBar';
import { Order, Settings } from '../../models'; import { Order, Settings } from '../../models';
import { apiClient } from '../../services/api'; import { apiClient } from '../../services/api';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface OrderPageProps { interface OrderPageProps {
windowSize: { width: number; height: number };
order: Order;
settings: Settings;
setOrder: (state: Order) => void;
setCurrentOrder: (state: number) => void;
fetchOrder: () => void;
badOrder: string | undefined;
setBadOrder: (state: string | undefined) => void;
hasRobot: boolean; hasRobot: boolean;
setPage: (state: Page) => void;
baseUrl: string;
locationOrderId: number; locationOrderId: number;
} }
const OrderPage = ({ const OrderPage = ({ hasRobot = false, locationOrderId }: OrderPageProps): JSX.Element => {
windowSize, const {
order, windowSize,
settings, order,
setOrder, settings,
setCurrentOrder, setOrder,
badOrder, setCurrentOrder,
setBadOrder, badOrder,
setPage, setBadOrder,
hasRobot = false, setPage,
baseUrl, baseUrl,
locationOrderId, navbarHeight,
}: OrderPageProps): JSX.Element => { } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const doublePageWidth: number = 50; const doublePageWidth: number = 50;
const maxHeight: number = windowSize.height * 0.85 - 3; const maxHeight: number = (windowSize.height - navbarHeight) * 0.85 - 3;
const [tab, setTab] = useState<'order' | 'contract'>('contract'); const [tab, setTab] = useState<'order' | 'contract'>('contract');

View File

@ -1,12 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Collapse, Grid, Typography, useTheme } from '@mui/material'; import { Button, Grid, Typography, useTheme } from '@mui/material';
import { useParams } from 'react-router-dom';
import { Page } from '../NavBar';
import { Robot } from '../../models'; import { Robot } from '../../models';
import { Casino, Download, ContentCopy, SmartToy, Bolt } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar';
import TokenInput from './TokenInput'; import TokenInput from './TokenInput';
import Key from '@mui/icons-material/Key'; import Key from '@mui/icons-material/Key';
@ -17,8 +12,6 @@ interface RecoveryProps {
inputToken: string; inputToken: string;
setInputToken: (state: string) => void; setInputToken: (state: string) => void;
getGenerateRobot: (token: string) => void; getGenerateRobot: (token: string) => void;
setPage: (state: Page) => void;
baseUrl: string;
} }
const Recovery = ({ const Recovery = ({
@ -28,11 +21,8 @@ const Recovery = ({
setView, setView,
setInputToken, setInputToken,
getGenerateRobot, getGenerateRobot,
setPage,
baseUrl,
}: RecoveryProps): JSX.Element => { }: RecoveryProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme();
const recoveryDisabled = () => { const recoveryDisabled = () => {
return !(inputToken.length > 20); return !(inputToken.length > 20);
@ -47,11 +37,14 @@ const Recovery = ({
return ( return (
<Grid container direction='column' alignItems='center' spacing={1} padding={2}> <Grid container direction='column' alignItems='center' spacing={1} padding={2}>
<Grid item>
<Typography variant='h5' align='center'>
{t('Robot recovery')}
</Typography>
</Grid>
<Grid item> <Grid item>
<Typography align='center'> <Typography align='center'>
{t( {t('Enter your robot token to re-build your robot and gain access to its trades.')}
'Please, introduce your robot token to re-build your robot and gain access to its trades.',
)}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>

View File

@ -7,7 +7,6 @@ import RobotAvatar from '../../components/RobotAvatar';
import TokenInput from './TokenInput'; import TokenInput from './TokenInput';
import { Page } from '../NavBar'; import { Page } from '../NavBar';
import { Robot } from '../../models'; import { Robot } from '../../models';
import { genBase62Token } from '../../utils';
interface RobotProfileProps { interface RobotProfileProps {
robot: Robot; robot: Robot;
@ -21,7 +20,6 @@ interface RobotProfileProps {
setPage: (state: Page) => void; setPage: (state: Page) => void;
baseUrl: string; baseUrl: string;
badRequest: string; badRequest: string;
robotFound: boolean;
width: number; width: number;
} }
@ -37,7 +35,6 @@ const RobotProfile = ({
setView, setView,
badRequest, badRequest,
baseUrl, baseUrl,
robotFound,
width, width,
}: RobotProfileProps): JSX.Element => { }: RobotProfileProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -104,15 +101,13 @@ const RobotProfile = ({
/> />
</Grid> </Grid>
{/* {robotFound ? ( {robot.found ? (
<Grid item> <Typography align='center' variant='h6'>
<Typography variant='h6'> {t('Welcome back!')}
{t('Welcome back!')} </Typography>
</Typography>
</Grid>
) : ( ) : (
<></> <></>
)} */} )}
{robot.activeOrderId ? ( {robot.activeOrderId ? (
<Grid item> <Grid item>
@ -184,7 +179,6 @@ const RobotProfile = ({
<Grid item> <Grid item>
<Button <Button
disabled={!(robot.avatarLoaded && robot.nickname)}
size='small' size='small'
color='primary' color='primary'
onClick={() => { onClick={() => {

View File

@ -59,7 +59,7 @@ const TokenInput = ({
required={true} required={true}
label={label || undefined} label={label || undefined}
value={inputToken} value={inputToken}
autoFocus={autoFocusTarget == 'texfield'} autoFocus={autoFocusTarget == 'textfield'}
fullWidth={fullWidth} fullWidth={fullWidth}
sx={{ borderColor: 'primary' }} sx={{ borderColor: 'primary' }}
variant={editable ? 'outlined' : 'filled'} variant={editable ? 'outlined' : 'filled'}

View File

@ -24,7 +24,7 @@ const Welcome = ({ setView, width, getGenerateRobot }: WelcomeProps): JSX.Elemen
paddingTop={2.2} paddingTop={2.2}
padding={0.5} padding={0.5}
> >
<Grid item> <Grid item style={{ paddingTop: '2em', paddingBottom: '1.5em' }}>
<svg width={0} height={0}> <svg width={0} height={0}>
<linearGradient id='linearColors' x1={1} y1={0} x2={1} y2={1}> <linearGradient id='linearColors' x1={1} y1={0} x2={1} y2={1}>
<stop offset={0} stopColor={theme.palette.primary.main} /> <stop offset={0} stopColor={theme.palette.primary.main} />

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Paper, Paper,
@ -14,36 +14,19 @@ import { useParams } from 'react-router-dom';
import { Page } from '../NavBar'; import { Page } from '../NavBar';
import { Robot } from '../../models'; import { Robot } from '../../models';
import { tokenStrength } from '../../utils';
import { systemClient } from '../../services/System'; import { systemClient } from '../../services/System';
import { apiClient } from '../../services/api'; import { apiClient } from '../../services/api';
import { genKey } from '../../pgp';
import { sha256 } from 'js-sha256';
import Onboarding from './Onboarding'; import Onboarding from './Onboarding';
import Welcome from './Welcome'; import Welcome from './Welcome';
import RobotProfile from './RobotProfile'; import RobotProfile from './RobotProfile';
import Recovery from './Recovery'; import Recovery from './Recovery';
import { TorIcon } from '../../components/Icons'; import { TorIcon } from '../../components/Icons';
import { genKey } from '../../pgp';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface RobotPageProps { const RobotPage = (): JSX.Element => {
setPage: (state: Page) => void; const { setPage, setCurrentOrder, fetchRobot, torStatus, windowSize, robot, setRobot, baseUrl } =
setCurrentOrder: (state: number) => void; useContext<AppContextProps>(AppContext);
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
robot: Robot;
setRobot: (state: Robot) => void;
windowSize: { width: number; height: number };
baseUrl: string;
}
const RobotPage = ({
setPage,
setCurrentOrder,
torStatus,
windowSize,
robot,
setRobot,
baseUrl,
}: RobotPageProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
const refCode = params.refCode; const refCode = params.refCode;
@ -51,7 +34,6 @@ const RobotPage = ({
const maxHeight = windowSize.height * 0.85 - 3; const maxHeight = windowSize.height * 0.85 - 3;
const theme = useTheme(); const theme = useTheme();
const [robotFound, setRobotFound] = useState<boolean>(false);
const [badRequest, setBadRequest] = useState<string | undefined>(undefined); const [badRequest, setBadRequest] = useState<string | undefined>(undefined);
const [inputToken, setInputToken] = useState<string>(''); const [inputToken, setInputToken] = useState<string>('');
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>( const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
@ -63,82 +45,24 @@ const RobotPage = ({
setInputToken(robot.token); setInputToken(robot.token);
} }
if (robot.nickname == null && robot.token) { if (robot.nickname == null && robot.token) {
getGenerateRobot(robot.token); fetchRobot({ action: 'login' });
} }
}, []); }, []);
const getGenerateRobot = (token: string) => { const getGenerateRobot = (token: string) => {
const strength = tokenStrength(token);
setRobot({ ...robot, loading: true, avatarLoaded: false });
setInputToken(token); setInputToken(token);
genKey(token).then(function (key) {
const requestBody = genKey(token).then(function (key) { fetchRobot({
return { action: 'generate',
token_sha256: sha256(token), newKeys: {
public_key: key.publicKeyArmored, pubKey: key.publicKeyArmored,
encrypted_private_key: key.encryptedPrivateKeyArmored, encPrivKey: key.encryptedPrivateKeyArmored,
unique_values: strength.uniqueValues, },
counts: strength.counts, newToken: token,
length: token.length, refCode,
ref_code: refCode, setBadRequest,
}; });
}); });
requestBody.then(
async (body) =>
await apiClient.post(baseUrl, '/api/user/', body).then((data: any) => {
setRobotFound(data?.found);
setBadRequest(data?.bad_request);
setCurrentOrder(
data.active_order_id
? data.active_order_id
: data.last_order_id
? data.last_order_id
: null,
);
// Add nick and token to App state (token only if not a bad request)
data.bad_request
? setRobot({
...robot,
avatarLoaded: true,
loading: false,
nickname: data.nickname ?? robot.nickname,
activeOrderId: data.active_order_id ?? null,
referralCode: data.referral_code ?? robot.referralCode,
earnedRewards: data.earned_rewards ?? robot.earnedRewards,
lastOrderId: data.last_order_id ?? robot.lastOrderId,
stealthInvoices: data.wants_stealth ?? robot.stealthInvoices,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
})
: setRobot({
...robot,
nickname: data.nickname,
token,
loading: false,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
referralCode: data.referral_code,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
bitsEntropy: data.token_bits_entropy,
shannonEntropy: data.token_shannon_entropy,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
copiedToken: data.found ? true : robot.copiedToken,
}) &
systemClient.setItem('robot_token', token) &
systemClient.setItem('pub_key', data.public_key.split('\n').join('\\')) &
systemClient.setItem(
'enc_priv_key',
data.encrypted_private_key.split('\n').join('\\'),
);
}),
);
}; };
const deleteRobot = () => { const deleteRobot = () => {
@ -148,7 +72,6 @@ const RobotPage = ({
const logoutRobot = () => { const logoutRobot = () => {
setInputToken(''); setInputToken('');
setRobotFound(false);
systemClient.deleteCookie('sessionid'); systemClient.deleteCookie('sessionid');
systemClient.deleteItem('robot_token'); systemClient.deleteItem('robot_token');
systemClient.deleteItem('pub_key'); systemClient.deleteItem('pub_key');
@ -237,7 +160,6 @@ const RobotPage = ({
<RobotProfile <RobotProfile
setView={setView} setView={setView}
robot={robot} robot={robot}
robotFound={robotFound}
setRobot={setRobot} setRobot={setRobot}
setCurrentOrder={setCurrentOrder} setCurrentOrder={setCurrentOrder}
badRequest={badRequest} badRequest={badRequest}

View File

@ -1,27 +1,14 @@
import React from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Grid, Paper, useTheme } from '@mui/material'; import { Grid, Paper, useTheme } from '@mui/material';
import SettingsForm from '../../components/SettingsForm'; import SettingsForm from '../../components/SettingsForm';
import { Settings, Favorites } from '../../models'; import { AppContextProps, AppContext } from '../../contexts/AppContext';
interface SettingsPageProps { const SettingsPage = (): JSX.Element => {
fav: Favorites; const { windowSize, navbarHeight } = useContext<AppContextProps>(AppContext);
setFav: (state: Favorites) => void;
settings: Settings;
setSettings: (state: Settings) => void;
windowSize: { width: number; height: number };
}
const SettingsPage = ({
fav,
setFav,
settings,
setSettings,
windowSize,
}: SettingsPageProps): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const maxHeight = windowSize.height * 0.85 - 3; const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
return ( return (
<Paper <Paper
@ -36,13 +23,7 @@ const SettingsPage = ({
> >
<Grid container> <Grid container>
<Grid item> <Grid item>
<SettingsForm <SettingsForm showNetwork={!(window.NativeRobosats === undefined)} />
fav={fav}
setFav={setFav}
settings={settings}
setSettings={setSettings}
showNetwork={!(window.NativeRobosats === undefined)}
/>
</Grid> </Grid>
</Grid> </Grid>
</Paper> </Paper>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Box, Box,
@ -17,7 +17,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { DataGrid, GridPagination } from '@mui/x-data-grid'; import { DataGrid, GridPagination } from '@mui/x-data-grid';
import currencyDict from '../../../static/assets/currencies.json'; import currencyDict from '../../../static/assets/currencies.json';
import { Book, Favorites } from '../../models'; import { PublicOrder } from '../../models';
import { filterOrders, hexToRgb, statusBadgeColor, pn, amountToString } from '../../utils'; import { filterOrders, hexToRgb, statusBadgeColor, pn, amountToString } from '../../utils';
import BookControl from './BookControl'; import BookControl from './BookControl';
@ -27,11 +27,10 @@ import RobotAvatar from '../RobotAvatar';
// Icons // Icons
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material'; import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface BookTableProps { interface BookTableProps {
clickRefresh?: () => void; orderList?: PublicOrder[];
book: Book;
fav?: Favorites;
maxWidth: number; maxWidth: number;
maxHeight: number; maxHeight: number;
fullWidth?: number; fullWidth?: number;
@ -42,16 +41,11 @@ interface BookTableProps {
showControls?: boolean; showControls?: boolean;
showFooter?: boolean; showFooter?: boolean;
showNoResults?: boolean; showNoResults?: boolean;
setFav?: (state: Favorites) => void;
onOrderClicked?: (id: number) => void; onOrderClicked?: (id: number) => void;
baseUrl: string;
} }
const BookTable = ({ const BookTable = ({
clickRefresh, orderList,
book,
fav = { currency: 1, type: 0, mode: 'fiat' },
setFav,
maxWidth = 100, maxWidth = 100,
maxHeight = 70, maxHeight = 70,
fullWidth = 100, fullWidth = 100,
@ -63,10 +57,12 @@ const BookTable = ({
showFooter = true, showFooter = true,
showNoResults = true, showNoResults = true,
onOrderClicked = () => null, onOrderClicked = () => null,
baseUrl,
}: BookTableProps): JSX.Element => { }: BookTableProps): JSX.Element => {
const { book, fetchBook, fav, setFav, baseUrl } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const orders = orderList ?? book.orders;
const [pageSize, setPageSize] = useState(0); const [pageSize, setPageSize] = useState(0);
const [fullscreen, setFullscreen] = useState(defaultFullscreen); const [fullscreen, setFullscreen] = useState(defaultFullscreen);
const [paymentMethods, setPaymentMethods] = useState<string[]>([]); const [paymentMethods, setPaymentMethods] = useState<string[]>([]);
@ -641,7 +637,7 @@ const BookTable = ({
</IconButton> </IconButton>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<IconButton onClick={clickRefresh}> <IconButton onClick={() => fetchBook()}>
<Refresh /> <Refresh />
</IconButton> </IconButton>
</Grid> </Grid>
@ -729,11 +725,11 @@ const BookTable = ({
rows={ rows={
showControls showControls
? filterOrders({ ? filterOrders({
orders: book.orders, orders,
baseFilter: fav, baseFilter: fav,
paymentMethods, paymentMethods,
}) })
: book.orders : orders
} }
loading={book.loading} loading={book.loading}
columns={columns} columns={columns}
@ -748,7 +744,7 @@ const BookTable = ({
setPaymentMethods, setPaymentMethods,
}, },
}} }}
pageSize={book.loading && book.orders.length == 0 ? 0 : pageSize} pageSize={book.loading && orders.length == 0 ? 0 : pageSize}
rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]} rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => { onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize); setPageSize(newPageSize);
@ -769,11 +765,11 @@ const BookTable = ({
rows={ rows={
showControls showControls
? filterOrders({ ? filterOrders({
orders: book.orders, orders,
baseFilter: fav, baseFilter: fav,
paymentMethods, paymentMethods,
}) })
: book.orders : orders
} }
loading={book.loading} loading={book.loading}
columns={columns} columns={columns}
@ -788,7 +784,7 @@ const BookTable = ({
setPaymentMethods, setPaymentMethods,
}, },
}} }}
pageSize={book.loading && book.orders.length == 0 ? 0 : pageSize} pageSize={book.loading && orders.length == 0 ? 0 : pageSize}
rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]} rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => { onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize); setPageSize(newPageSize);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useContext } from 'react';
import { import {
ResponsiveLine, ResponsiveLine,
Serie, Serie,
@ -26,32 +26,24 @@ import { amountToString, matchMedian, statusBadgeColor } from '../../../utils';
import currencyDict from '../../../../static/assets/currencies.json'; import currencyDict from '../../../../static/assets/currencies.json';
import { PaymentStringAsIcons } from '../../PaymentMethods'; import { PaymentStringAsIcons } from '../../PaymentMethods';
import getNivoScheme from '../NivoScheme'; import getNivoScheme from '../NivoScheme';
import { AppContextProps, AppContext } from '../../../contexts/AppContext';
interface DepthChartProps { interface DepthChartProps {
orders: PublicOrder[];
lastDayPremium?: number | undefined;
currency: number;
limits: LimitList;
maxWidth: number; maxWidth: number;
maxHeight: number; maxHeight: number;
fillContainer?: boolean; fillContainer?: boolean;
elevation?: number; elevation?: number;
onOrderClicked?: (id: number) => void; onOrderClicked?: (id: number) => void;
baseUrl: string;
} }
const DepthChart: React.FC<DepthChartProps> = ({ const DepthChart: React.FC<DepthChartProps> = ({
orders,
lastDayPremium,
currency,
limits,
maxWidth, maxWidth,
maxHeight, maxHeight,
fillContainer = false, fillContainer = false,
elevation = 6, elevation = 6,
onOrderClicked = () => null, onOrderClicked = () => null,
baseUrl,
}) => { }) => {
const { book, fav, info, limits, baseUrl } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const [enrichedOrders, setEnrichedOrders] = useState<Order[]>([]); const [enrichedOrders, setEnrichedOrders] = useState<Order[]>([]);
@ -66,22 +58,22 @@ const DepthChart: React.FC<DepthChartProps> = ({
const width = maxWidth < 20 ? 20 : maxWidth > 72.8 ? 72.8 : maxWidth; const width = maxWidth < 20 ? 20 : maxWidth > 72.8 ? 72.8 : maxWidth;
useEffect(() => { useEffect(() => {
setCurrencyCode(currency === 0 ? 1 : currency); setCurrencyCode(fav.currency === 0 ? 1 : fav.currency);
}, [currency]); }, [fav.currency]);
useEffect(() => { useEffect(() => {
if (Object.keys(limits).length > 0) { if (Object.keys(limits.list).length > 0) {
const enriched = orders.map((order) => { const enriched = book.orders.map((order) => {
// We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate // We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate
// for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a // for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a
// simple rule of three // simple rule of three
order.base_amount = order.base_amount =
(order.price * limits[currencyCode].price) / limits[order.currency].price; (order.price * limits.list[currencyCode].price) / limits.list[order.currency].price;
return order; return order;
}); });
setEnrichedOrders(enriched); setEnrichedOrders(enriched);
} }
}, [limits, orders, currencyCode]); }, [limits.list, book.orders, currencyCode]);
useEffect(() => { useEffect(() => {
if (enrichedOrders.length > 0) { if (enrichedOrders.length > 0) {
@ -102,16 +94,16 @@ const DepthChart: React.FC<DepthChartProps> = ({
setXRange(maxRange); setXRange(maxRange);
setRangeSteps(rangeSteps); setRangeSteps(rangeSteps);
} else { } else {
if (lastDayPremium === undefined) { if (info.last_day_nonkyc_btc_premium === undefined) {
const premiums: number[] = enrichedOrders.map((order) => order?.premium || 0); const premiums: number[] = enrichedOrders.map((order) => order?.premium || 0);
setCenter(~~matchMedian(premiums)); setCenter(~~matchMedian(premiums));
} else { } else {
setCenter(lastDayPremium); setCenter(info.last_day_nonkyc_btc_premium);
} }
setXRange(8); setXRange(8);
setRangeSteps(0.5); setRangeSteps(0.5);
} }
}, [enrichedOrders, xType, lastDayPremium, currencyCode]); }, [enrichedOrders, xType, info.last_day_nonkyc_btc_premium, currencyCode]);
const generateSeries: () => void = () => { const generateSeries: () => void = () => {
const sortedOrders: PublicOrder[] = const sortedOrders: PublicOrder[] =

View File

@ -117,7 +117,7 @@ const CoordinatorSummaryDialog = ({ open = false, onClose, info }: Props): JSX.E
primaryTypographyProps={{ fontSize: '14px' }} primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }} secondaryTypographyProps={{ fontSize: '12px' }}
primary={`${info.last_day_nonkyc_btc_premium}%`} primary={`${info.last_day_nonkyc_btc_premium}%`}
secondary={t('24h non-KYC bitcoin premium')} secondary={t('Last 24h mean premium')}
/> />
</ListItem> </ListItem>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
InputAdornment, InputAdornment,
@ -29,7 +29,6 @@ import { LimitList, Maker, Favorites, defaultMaker } from '../../models';
import { LocalizationProvider, TimePicker } from '@mui/x-date-pickers'; import { LocalizationProvider, TimePicker } from '@mui/x-date-pickers';
import DateFnsUtils from '@date-io/date-fns'; import DateFnsUtils from '@date-io/date-fns';
import { useHistory } from 'react-router-dom';
import { ConfirmationDialog } from '../Dialogs'; import { ConfirmationDialog } from '../Dialogs';
import { apiClient } from '../../services/api'; import { apiClient } from '../../services/api';
@ -41,35 +40,21 @@ import { amountToString, pn } from '../../utils';
import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit } from '@mui/icons-material'; import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit } from '@mui/icons-material';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Page } from '../../basic/NavBar'; import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface MakerFormProps { interface MakerFormProps {
limits: { list: LimitList; loading: boolean };
fetchLimits: () => void;
pricingMethods?: boolean;
maker: Maker;
fav: Favorites;
setFav: (state: Favorites) => void;
setMaker: (state: Maker) => void;
disableRequest?: boolean; disableRequest?: boolean;
pricingMethods?: boolean;
collapseAll?: boolean; collapseAll?: boolean;
onSubmit?: () => void; onSubmit?: () => void;
onReset?: () => void; onReset?: () => void;
submitButtonLabel?: string; submitButtonLabel?: string;
onOrderCreated?: (id: number) => void; onOrderCreated?: (id: number) => void;
hasRobot?: boolean; hasRobot?: boolean;
setPage?: (state: Page) => void;
baseUrl: string;
} }
const MakerForm = ({ const MakerForm = ({
limits,
fetchLimits,
pricingMethods = false, pricingMethods = false,
fav,
setFav,
maker,
setMaker,
disableRequest = false, disableRequest = false,
collapseAll = false, collapseAll = false,
onSubmit = () => {}, onSubmit = () => {},
@ -77,12 +62,12 @@ const MakerForm = ({
submitButtonLabel = 'Create Order', submitButtonLabel = 'Create Order',
onOrderCreated = () => null, onOrderCreated = () => null,
hasRobot = true, hasRobot = true,
setPage = () => null,
baseUrl,
}: MakerFormProps): JSX.Element => { }: MakerFormProps): JSX.Element => {
const { fav, setFav, limits, fetchLimits, maker, setMaker, setPage, baseUrl } =
useContext<AppContextProps>(AppContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const history = useHistory();
const [badRequest, setBadRequest] = useState<string | null>(null); const [badRequest, setBadRequest] = useState<string | null>(null);
const [amountLimits, setAmountLimits] = useState<number[]>([1, 1000]); const [amountLimits, setAmountLimits] = useState<number[]>([1, 1000]);
const [satoshisLimits, setSatoshisLimits] = useState<number[]>([20000, 4000000]); const [satoshisLimits, setSatoshisLimits] = useState<number[]>([20000, 4000000]);

View File

@ -1,5 +1,6 @@
import React from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AppContextProps, AppContext } from '../../contexts/AppContext';
import { import {
Grid, Grid,
Paper, Paper,
@ -14,7 +15,6 @@ import {
ToggleButtonGroup, ToggleButtonGroup,
ToggleButton, ToggleButton,
} from '@mui/material'; } from '@mui/material';
import { Favorites, Settings } from '../../models';
import SelectLanguage from './SelectLanguage'; import SelectLanguage from './SelectLanguage';
import { import {
Translate, Translate,
@ -31,21 +31,11 @@ import SwapCalls from '@mui/icons-material/SwapCalls';
interface SettingsFormProps { interface SettingsFormProps {
dense?: boolean; dense?: boolean;
fav: Favorites;
setFav: (state: Favorites) => void;
settings: Settings;
setSettings: (state: Settings) => void;
showNetwork?: boolean; showNetwork?: boolean;
} }
const SettingsForm = ({ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps): JSX.Element => {
dense = false, const { fav, setFav, settings, setSettings } = useContext<AppContextProps>(AppContext);
fav,
setFav,
settings,
setSettings,
showNetwork = false,
}: SettingsFormProps): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const fontSizes = [ const fontSizes = [

View File

@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useContext } from 'react';
import { Box, CircularProgress, Tooltip } from '@mui/material'; import { Box, CircularProgress, Tooltip } from '@mui/material';
import { TorIcon } from './Icons'; import { TorIcon } from './Icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AppContext, AppContextProps } from '../contexts/AppContext';
interface TorIndicatorProps { interface TorIndicatorProps {
color: 'inherit' | 'error' | 'warning' | 'success' | 'primary' | 'secondary' | 'info' | undefined; color: 'inherit' | 'error' | 'warning' | 'success' | 'primary' | 'secondary' | 'info' | undefined;
@ -53,11 +54,8 @@ const TorIndicator = ({
); );
}; };
interface TorConnectionBadgeProps { const TorConnectionBadge = (): JSX.Element => {
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE'; const { torStatus } = useContext<AppContextProps>(AppContext);
}
const TorConnectionBadge = ({ torStatus }: TorConnectionBadgeProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
if (window?.NativeRobosats == null) { if (window?.NativeRobosats == null) {

View File

@ -0,0 +1,501 @@
import React, { useEffect, useState } from 'react';
import { Page } from '../basic/NavBar';
import { OpenDialogs } from '../basic/MainDialogs';
import {
Book,
LimitList,
Maker,
Robot,
Info,
Settings,
Favorites,
defaultMaker,
defaultInfo,
Coordinator,
Order,
} from '../models';
import { apiClient } from '../services/api';
import { checkVer, getHost, tokenStrength } from '../utils';
import { sha256 } from 'js-sha256';
import defaultCoordinators from '../../static/federation.json';
import { useTheme } from '@mui/material';
import { systemClient } from '../services/System';
const getWindowSize = function (fontSize: number) {
// returns window size in EM units
return {
width: window.innerWidth / fontSize,
height: window.innerHeight / fontSize,
};
};
// 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'
];
export interface SlideDirection {
in: 'left' | 'right' | undefined;
out: 'left' | 'right' | undefined;
}
export interface fetchRobotProps {
action?: 'login' | 'generate';
newKeys?: { encPrivKey: string; pubKey: string } | null;
newToken?: string | null;
refCode?: string | null;
setBadRequest?: (state: string) => void;
}
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
export interface AppContextProps {
torStatus: TorStatus;
federation: Coordinator[];
setFederation: (state: Coordinator[]) => void;
settings: Settings;
setSettings: (state: Settings) => void;
book: Book;
info: Info;
setBook: (state: Book) => void;
fetchBook: () => void;
limits: { list: LimitList; loading: boolean };
setLimits: (state: { list: LimitList; loading: boolean }) => void;
fetchLimits: () => void;
maker: Maker;
setMaker: (state: Maker) => void;
clearOrder: () => void;
robot: Robot;
setRobot: (state: Robot) => void;
focusedCoordinator: number;
setFocusedCoordinator: (state: number) => void;
baseUrl: string;
setBaseUrl: (state: string) => void;
fav: Favorites;
setFav: (state: Favorites) => void;
order: Order | undefined;
setOrder: (state: Order | undefined) => void;
badOrder: string;
setBadOrder: (state: string | undefined) => void;
setDelay: (state: number) => void;
page: Page;
setPage: (state: Page) => void;
slideDirection: SlideDirection;
setSlideDirection: (state: SlideDirection) => void;
currentOrder: number | undefined;
setCurrentOrder: (state: number) => void;
navbarHeight: number;
closeAll: OpenDialogs;
open: OpenDialogs;
setOpen: (state: OpenDialogs) => void;
windowSize: { width: number; height: number };
clientVersion: {
semver: Version;
short: string;
long: string;
};
}
const entryPage: Page | '' =
window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : '';
const closeAll = {
more: false,
learn: false,
community: false,
info: false,
coordinator: false,
exchange: false,
client: false,
update: false,
profile: false,
};
// export const initialState = {
// federation: defaultFederation,
// setFederation: () => null,
// settings: new Settings(),
// setSettings: () => null,
// book: { orders: [], loading: true },
// setBook: () => null,
// fetchBook: () => null,
// limits: {
// list: [],
// loading: true,
// },
// setLimits:() => null,
// fetchLimits: ()=> null,
// maker: defaultMaker,
// setMaker: () => null,
// clearOrder: () => null,
// robot: new Robot(),
// setRobot: () => null,
// info: defaultExchange,
// setExchange: () => null,
// focusedCoordinator: 0,
// setFocusedCoordinator: () => null,
// baseUrl: '',
// setBaseUrl: () => null,
// fav: { type: null, currency: 0 },
// setFav: () => null,
// order: undefined,
// setOrder: () => null,
// badOrder: '',
// setBadOrder: () => null,
// setDelay: () => null,
// page: entryPage == '' ? 'robot' : entryPage,
// setPage: () => null,
// slideDirection: {
// in: undefined,
// out: undefined,
// },
// setSlideDirection: () => null,
// currentOrder: undefined,
// setCurrentOrder: () => null,
// navbarHeight: 2.5,
// closeAll,
// open: closeAll,
// setOpen: () => null,
// windowSize: getWindowSize(14),
// }
export interface AppContextProviderProps {
children: React.ReactNode;
settings: Settings;
setSettings: (state: Settings) => void;
}
export const AppContextProvider = ({
children,
settings,
setSettings,
}: AppContextProviderProps): JSX.Element => {
const theme = useTheme();
// All app data structured
const [torStatus, setTorStatus] = useState<TorStatus>('NOTINIT');
const [book, setBook] = useState<Book>({ orders: [], loading: true });
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({
list: [],
loading: true,
});
const [robot, setRobot] = useState<Robot>(new Robot());
const [maker, setMaker] = useState<Maker>(defaultMaker);
const [info, setInfo] = useState<Info>(defaultInfo);
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators);
const [baseUrl, setBaseUrl] = useState<string>('');
const [fav, setFav] = useState<Favorites>({ type: null, mode: 'fiat', currency: 0 });
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 entryPage: Page | '' =
window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : '';
const [page, setPage] = useState<Page>(entryPage == '' ? 'robot' : entryPage);
const [slideDirection, setSlideDirection] = useState<SlideDirection>({
in: undefined,
out: undefined,
});
const [currentOrder, setCurrentOrder] = useState<number | undefined>(undefined);
const navbarHeight = 2.5;
const closeAll = {
more: false,
learn: false,
community: false,
info: false,
coordinator: false,
stats: false,
update: false,
profile: false,
};
const [open, setOpen] = useState<OpenDialogs>(closeAll);
const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(
getWindowSize(theme.typography.fontSize),
);
useEffect(() => {
if (typeof window !== undefined) {
window.addEventListener('resize', onResize);
}
if (baseUrl != '') {
fetchBook();
fetchLimits();
}
return () => {
if (typeof window !== undefined) {
window.removeEventListener('resize', onResize);
}
};
}, [baseUrl]);
useEffect(() => {
let host = '';
if (window.NativeRobosats === undefined) {
host = getHost();
} else {
host =
settings.network === 'mainnet'
? coordinators[0].mainnetOnion
: coordinators[0].testnetOnion;
}
setBaseUrl(`${location.protocol}//${host}`);
}, [settings.network]);
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const onResize = function () {
setWindowSize(getWindowSize(theme.typography.fontSize));
};
const fetchBook = function () {
setBook({ ...book, loading: true });
apiClient.get(baseUrl, '/api/book/').then((data: any) =>
setBook({
loading: false,
orders: data.not_found ? [] : data,
}),
);
};
const fetchLimits = async () => {
setLimits({ ...limits, loading: true });
const data = apiClient.get(baseUrl, '/api/limits/').then((data) => {
setLimits({ list: data ?? [], loading: false });
return data;
});
return await data;
};
const fetchInfo = function () {
setInfo({ ...info, loading: true });
apiClient.get(baseUrl, '/api/info/').then((data: Info) => {
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
setInfo({
...data,
openUpdateClient: versionInfo.updateAvailable,
coordinatorVersion: versionInfo.coordinatorVersion,
clientVersion: versionInfo.clientVersion,
loading: false,
});
});
};
useEffect(() => {
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
fetchInfo();
}
}, [open.stats, open.coordinator]);
useEffect(() => {
// Sets Setting network from coordinator API param if accessing via web
if (settings.network == undefined && info.network) {
setSettings((settings: Settings) => {
return { ...settings, network: info.network };
});
}
}, [info]);
// 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);
};
const fetchRobot = function ({
action = 'login',
newKeys = null,
newToken = null,
refCode = null,
setBadRequest = () => {},
}: fetchRobotProps) {
setRobot({ ...robot, loading: true, avatarLoaded: false });
setBadRequest('');
let requestBody = {};
if (action == 'login') {
requestBody.token_sha256 = sha256(newToken ?? robot.token);
} else if (action == 'generate' && newToken != null) {
const strength = tokenStrength(newToken);
requestBody.token_sha256 = sha256(newToken);
requestBody.unique_values = strength.uniqueValues;
requestBody.counts = strength.counts;
requestBody.length = newToken.length;
requestBody.ref_code = refCode;
requestBody.public_key = newKeys.pubKey ?? robot.pubkey;
requestBody.encrypted_private_key = newKeys.encPrivKey ?? robot.encPrivKey;
}
apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => {
setCurrentOrder(
data.active_order_id
? data.active_order_id
: data.last_order_id
? data.last_order_id
: null,
);
if (data.bad_request) {
setBadRequest(data.bad_request);
setRobot({
...robot,
loading: false,
nickname: data.nickname ?? robot.nickname,
activeOrderId: data.active_order_id ?? null,
referralCode: data.referral_code ?? robot.referralCode,
earnedRewards: data.earned_rewards ?? robot.earnedRewards,
lastOrderId: data.last_order_id ?? robot.lastOrderId,
stealthInvoices: data.wants_stealth ?? robot.stealthInvoices,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: false,
});
} else {
setRobot({
...robot,
nickname: data.nickname,
token: newToken ?? robot.token,
loading: false,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
referralCode: data.referral_code,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: data?.found,
bitsEntropy: data.token_bits_entropy,
shannonEntropy: data.token_shannon_entropy,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
copiedToken: data.found ? true : robot.copiedToken,
});
systemClient.setItem('robot_token', newToken ?? robot.token);
systemClient.setItem('pub_key', data.public_key.split('\n').join('\\'));
systemClient.setItem('enc_priv_key', data.encrypted_private_key.split('\n').join('\\'));
}
});
};
useEffect(() => {
if (baseUrl != '' && page != 'robot') {
if (open.profile || (robot.token && robot.nickname === null)) {
fetchRobot({ action: 'login' }); // fetch existing robot
} else if (robot.token && robot.encPrivKey && robot.pubKey) {
fetchRobot({ action: 'login' }); // create new robot with existing token and keys (on network and coordinator change)
}
}
}, [open.profile, baseUrl]);
return (
<AppContext.Provider
value={{
torStatus,
settings,
setSettings,
book,
setBook,
fetchBook,
fetchRobot,
limits,
info,
setLimits,
fetchLimits,
maker,
setMaker,
clearOrder,
robot,
setRobot,
baseUrl,
setBaseUrl,
fav,
setFav,
order,
setOrder,
badOrder,
setBadOrder,
setDelay,
page,
setPage,
slideDirection,
setSlideDirection,
currentOrder,
setCurrentOrder,
navbarHeight,
closeAll,
open,
setOpen,
windowSize,
}}
>
{children}
</AppContext.Provider>
);
};
export const AppContext = React.createContext();

View File

@ -22,6 +22,7 @@ class Robot {
public tgBotName: string = 'unknown'; public tgBotName: string = 'unknown';
public tgToken: string = 'unknown'; public tgToken: string = 'unknown';
public loading: boolean = false; public loading: boolean = false;
public found: boolean = false;
public avatarLoaded: boolean = false; public avatarLoaded: boolean = false;
public copiedToken: boolean = false; public copiedToken: boolean = false;
} }

View File

@ -1,20 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useContext, useState } from 'react';
import GridLayout, { Layout } from 'react-grid-layout'; import GridLayout, { Layout } from 'react-grid-layout';
import { Grid, styled, useTheme } from '@mui/material'; import { Grid, styled, useTheme } from '@mui/material';
import { apiClient } from '../services/api';
import checkVer from '../utils/checkVer';
import {
Book,
LimitList,
Maker,
Robot,
Info,
Settings,
Favorites,
defaultMaker,
defaultInfo,
} from '../models';
import { import {
PlaceholderWidget, PlaceholderWidget,
@ -26,21 +12,7 @@ import {
import ToolBar from '../pro/ToolBar'; import ToolBar from '../pro/ToolBar';
import LandingDialog from '../pro/LandingDialog'; import LandingDialog from '../pro/LandingDialog';
import defaultCoordinators from '../../static/federation.json'; import { AppContext, AppContextProps } from '../contexts/AppContext';
import { getHost } from '../utils';
const getWindowSize = function (fontSize: number) {
// returns window size in EM units
return {
width: window.innerWidth / fontSize,
height: window.innerHeight / fontSize,
};
};
interface MainProps {
settings: Settings;
setSettings: (state: Settings) => void;
}
// To Do. Add dotted grid when layout is not frozen // To Do. Add dotted grid when layout is not frozen
// ${freeze ? // ${freeze ?
@ -57,115 +29,58 @@ const StyledRGL = styled(GridLayout)(
`, `,
); );
const Main = ({ settings, setSettings }: MainProps): JSX.Element => { const defaultLayout: Layout = [
{ i: 'Maker', w: 10, h: 16, x: 67, y: 0, minW: 8, maxW: 22, minH: 10, maxH: 28 },
{ i: 'Book', w: 43, h: 15, x: 34, y: 16, minW: 6, maxW: 70, minH: 9, maxH: 25 },
{ i: 'DepthChart', w: 15, h: 10, x: 19, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Garage', w: 52, h: 16, x: 0, y: 0, minW: 15, maxW: 78, minH: 8, maxH: 30 },
{ i: 'History', w: 8, h: 10, x: 11, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Trade', w: 15, h: 16, x: 52, y: 0, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Settings', w: 11, h: 15, x: 0, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Other', w: 23, h: 5, x: 11, y: 26, minW: 2, maxW: 50, minH: 4, maxH: 25 },
];
const Main = (): JSX.Element => {
const {
book,
fetchBook,
maker,
setMaker,
setSettings,
clearOrder,
torStatus,
settings,
limits,
fetchLimits,
robot,
setRobot,
fetchRobot,
setOrder,
setDelay,
info,
fav,
setFav,
baseUrl,
order,
page,
setPage,
currentOrder,
setCurrentOrder,
open,
setOpen,
windowSize,
badOrder,
setBadOrder,
} = useContext<AppContextProps>(AppContext);
const theme = useTheme(); const theme = useTheme();
const em: number = theme.typography.fontSize; const em: number = theme.typography.fontSize;
const toolbarHeight: number = 3; const toolbarHeight: number = 3;
const gridCellSize: number = 2; const gridCellSize: number = 2;
const defaultLayout: Layout = [ const [openLanding, setOpenLanding] = useState<boolean>(true);
{ i: 'Maker', w: 10, h: 16, x: 67, y: 0, minW: 8, maxW: 22, minH: 10, maxH: 28 },
{ i: 'Book', w: 43, h: 15, x: 34, y: 16, minW: 6, maxW: 70, minH: 9, maxH: 25 },
{ i: 'DepthChart', w: 15, h: 10, x: 19, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Garage', w: 52, h: 16, x: 0, y: 0, minW: 15, maxW: 78, minH: 8, maxH: 30 },
{ i: 'History', w: 10, h: 10, x: 9, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Trade', w: 15, h: 16, x: 52, y: 0, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Settings', w: 9, h: 15, x: 0, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Other', w: 25, h: 5, x: 9, y: 26, minW: 2, maxW: 50, minH: 4, maxH: 25 },
];
// All app data structured
const [book, setBook] = useState<Book>({ orders: [], loading: true });
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({
list: [],
loading: true,
});
const [robot, setRobot] = useState<Robot>(new Robot());
const [maker, setMaker] = useState<Maker>(defaultMaker);
const [info, setInfo] = useState<Info>(defaultInfo);
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators);
const [fav, setFav] = useState<Favorites>({ type: null, currency: 0 });
const [baseUrl, setBaseUrl] = useState<string>('');
const [layout, setLayout] = useState<Layout>(defaultLayout); const [layout, setLayout] = useState<Layout>(defaultLayout);
const [openLanding, setOpenLanding] = useState<boolean>(true);
const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(
getWindowSize(em),
);
useEffect(() => {
if (typeof window !== undefined) {
window.addEventListener('resize', onResize);
}
if (baseUrl != '') {
fetchBook();
fetchLimits();
}
return () => {
if (typeof window !== undefined) {
window.removeEventListener('resize', onResize);
}
};
}, [baseUrl]);
useEffect(() => {
let host = '';
if (window.NativeRobosats === undefined) {
host = getHost();
} else {
host =
settings.network === 'mainnet'
? coordinators[0].mainnetOnion
: coordinators[0].testnetOnion;
}
setBaseUrl(`http://${host}`);
}, [settings.network]);
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const onResize = function () {
setWindowSize(getWindowSize(em));
};
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const fetchLimits = async () => {
setLimits({ ...limits, loading: true });
const data = apiClient.get(baseUrl, '/api/limits/').then((data) => {
setLimits({ list: data ?? [], loading: false });
return data;
});
return await data;
};
const fetchBook = function () {
setBook({ ...book, loading: true });
apiClient.get(baseUrl, '/api/book/').then((data: any) =>
setBook({
loading: false,
orders: data.not_found ? [] : data,
}),
);
};
const fetchInfo = function () {
apiClient.get(baseUrl, '/api/info/').then((data: any) => {
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
setInfo({
...data,
openUpdateClient: versionInfo.updateAvailable,
coordinatorVersion: versionInfo.coordinatorVersion,
clientVersion: versionInfo.clientVersion,
});
});
};
console.log(layout);
return ( return (
<Grid container direction='column' sx={{ width: `${windowSize.width}em` }}> <Grid container direction='column' sx={{ width: `${windowSize.width}em` }}>
<Grid item> <Grid item>
@ -191,41 +106,16 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
onLayoutChange={(layout: Layout) => setLayout(layout)} onLayoutChange={(layout: Layout) => setLayout(layout)}
> >
<div key='Maker'> <div key='Maker'>
<MakerWidget <MakerWidget />
baseUrl={baseUrl}
limits={limits}
fetchLimits={fetchLimits}
fav={fav}
setFav={setFav}
maker={maker}
setMaker={setMaker}
/>
</div> </div>
<div key='Book'> <div key='Book'>
<BookWidget <BookWidget layout={layout[1]} gridCellSize={gridCellSize} />
baseUrl={baseUrl}
book={book}
layout={layout[1]}
gridCellSize={gridCellSize}
fetchBook={fetchBook}
fav={fav}
setFav={setFav}
windowSize={windowSize}
/>
</div> </div>
<div key='DepthChart'> <div key='DepthChart'>
<DepthChartWidget <DepthChartWidget gridCellSize={gridCellSize} layout={layout[2]} />
baseUrl={baseUrl}
orders={book.orders}
gridCellSize={gridCellSize}
limitList={limits.list}
layout={layout[2]}
currency={fav.currency}
windowSize={windowSize}
/>
</div> </div>
<div key='Settings'> <div key='Settings'>
<SettingsWidget settings={settings} setSettings={setSettings} /> <SettingsWidget />
</div> </div>
<div key='Garage'> <div key='Garage'>
<PlaceholderWidget label='Robot Garage' /> <PlaceholderWidget label='Robot Garage' />

View File

@ -1,7 +1,8 @@
import React from 'react'; import React, { useContext } from 'react';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
import { Book, Favorites } from '../../models'; import { Book, Favorites } from '../../models';
import { Paper, useTheme } from '@mui/material'; import { Paper } from '@mui/material';
import BookTable from '../../components/BookTable'; import BookTable from '../../components/BookTable';
interface BookWidgetProps { interface BookWidgetProps {
@ -24,13 +25,7 @@ const BookWidget = React.forwardRef(
( (
{ {
layout, layout,
baseUrl,
gridCellSize = 2, gridCellSize = 2,
book,
fetchBook,
fav,
setFav,
windowSize,
style, style,
className, className,
onMouseDown, onMouseDown,
@ -39,24 +34,18 @@ const BookWidget = React.forwardRef(
}: BookWidgetProps, }: BookWidgetProps,
ref, ref,
) => { ) => {
const theme = useTheme(); const { book, windowSize, fav } = useContext<AppContextProps>(AppContext);
return React.useMemo(() => { return React.useMemo(() => {
return ( return (
<Paper elevation={3} style={{ width: '100%', height: '100%' }}> <Paper elevation={3} style={{ width: '100%', height: '100%' }}>
<BookTable <BookTable
baseUrl={baseUrl}
elevation={0} elevation={0}
clickRefresh={() => fetchBook()}
book={book}
fav={fav}
fillContainer={true} fillContainer={true}
maxWidth={layout.w * gridCellSize} // EM units maxWidth={layout.w * gridCellSize} // EM units
maxHeight={layout.h * gridCellSize} // EM units maxHeight={layout.h * gridCellSize} // EM units
fullWidth={windowSize.width} // EM units fullWidth={windowSize.width} // EM units
fullHeight={windowSize.height} // EM units fullHeight={windowSize.height} // EM units
defaultFullscreen={false} defaultFullscreen={false}
onCurrencyChange={(e) => setFav({ ...fav, currency: e.target.value })}
onTypeChange={(mouseEvent, val) => setFav({ ...fav, type: val })}
/> />
</Paper> </Paper>
); );

View File

@ -1,22 +1,16 @@
import React from 'react'; import React, { useContext } from 'react';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
import { Order, LimitList } from '../../models';
import { Paper, useTheme } from '@mui/material'; import { Paper, useTheme } from '@mui/material';
import DepthChart from '../../components/Charts/DepthChart'; import DepthChart from '../../components/Charts/DepthChart';
interface DepthChartWidgetProps { interface DepthChartWidgetProps {
layout: any; layout: any;
gridCellSize: number; gridCellSize: number;
orders: PublicOrder[];
currency: number;
limitList: LimitList;
windowSize: { width: number; height: number };
style?: Object; style?: Object;
className?: string; className?: string;
onMouseDown?: () => void; onMouseDown?: () => void;
onMouseUp?: () => void; onMouseUp?: () => void;
onTouchEnd?: () => void; onTouchEnd?: () => void;
baseUrl: string;
} }
const DepthChartWidget = React.forwardRef( const DepthChartWidget = React.forwardRef(
@ -24,11 +18,6 @@ const DepthChartWidget = React.forwardRef(
{ {
layout, layout,
gridCellSize, gridCellSize,
limitList,
orders,
baseUrl,
currency,
windowSize,
style, style,
className, className,
onMouseDown, onMouseDown,
@ -38,22 +27,19 @@ const DepthChartWidget = React.forwardRef(
ref, ref,
) => { ) => {
const theme = useTheme(); const theme = useTheme();
const { fav, book, limits } = useContext<AppContextProps>(AppContext);
return React.useMemo(() => { return React.useMemo(() => {
return ( return (
<Paper elevation={3} style={{ width: '100%', height: '100%' }}> <Paper elevation={3} style={{ width: '100%', height: '100%' }}>
<DepthChart <DepthChart
baseUrl={baseUrl}
elevation={0} elevation={0}
orders={orders}
currency={currency}
limits={limitList}
maxWidth={layout.w * gridCellSize} // EM units maxWidth={layout.w * gridCellSize} // EM units
maxHeight={layout.h * gridCellSize} // EM units maxHeight={layout.h * gridCellSize} // EM units
fillContainer={true} fillContainer={true}
/> />
</Paper> </Paper>
); );
}, [currency, orders, limitList, layout]); }, [fav.currency, book, limits, layout]);
}, },
); );

View File

@ -1,17 +1,11 @@
import React from 'react'; import React, { useContext } from 'react';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
import MakerForm from '../../components/MakerForm'; import MakerForm from '../../components/MakerForm';
import { LimitList, Maker, Favorites } from '../../models'; import { LimitList, Maker, Favorites } from '../../models';
import { Paper } from '@mui/material'; import { Paper } from '@mui/material';
interface MakerWidgetProps { interface MakerWidgetProps {
limits: { list: LimitList; loading: boolean };
fetchLimits: () => void;
fav: Favorites;
maker: Maker;
setFav: (state: Favorites) => void;
setMaker: (state: Maker) => void;
baseUrl: string;
style?: Object; style?: Object;
className?: string; className?: string;
onMouseDown?: () => void; onMouseDown?: () => void;
@ -20,38 +14,15 @@ interface MakerWidgetProps {
} }
const MakerWidget = React.forwardRef( const MakerWidget = React.forwardRef(
( ({ style, className, onMouseDown, onMouseUp, onTouchEnd }: MakerWidgetProps, ref) => {
{ const { maker, fav, limits } = useContext<AppContextProps>(AppContext);
maker,
setMaker,
limits,
fetchLimits,
fav,
setFav,
baseUrl,
style,
className,
onMouseDown,
onMouseUp,
onTouchEnd,
}: MakerWidgetProps,
ref,
) => {
return React.useMemo(() => { return React.useMemo(() => {
return ( return (
<Paper <Paper
elevation={3} elevation={3}
style={{ padding: 8, overflow: 'auto', width: '100%', height: '100%' }} style={{ padding: 8, overflow: 'auto', width: '100%', height: '100%' }}
> >
<MakerForm <MakerForm />
baseUrl={baseUrl}
limits={limits}
fetchLimits={fetchLimits}
maker={maker}
setMaker={setMaker}
fav={fav}
setFav={setFav}
/>
</Paper> </Paper>
); );
}, [maker, limits, fav]); }, [maker, limits, fav]);

View File

@ -1,12 +1,10 @@
import React from 'react'; import React, { useContext } from 'react';
import { AppContextProps, AppContext } from '../../contexts/AppContext';
import { Settings } from '../../models'; import { Settings } from '../../models';
import { Paper, useTheme } from '@mui/material'; import { Paper } from '@mui/material';
import SettingsForm from '../../components/SettingsForm'; import SettingsForm from '../../components/SettingsForm';
interface SettingsWidgetProps { interface SettingsWidgetProps {
settings: Settings;
setSettings: (state: Settings) => void;
style?: Object; style?: Object;
className?: string; className?: string;
onMouseDown?: () => void; onMouseDown?: () => void;
@ -15,26 +13,15 @@ interface SettingsWidgetProps {
} }
const SettingsWidget = React.forwardRef( const SettingsWidget = React.forwardRef(
( ({ style, className, onMouseDown, onMouseUp, onTouchEnd }: SettingsWidgetProps, ref) => {
{ const { settings } = useContext<AppContextProps>(AppContext);
settings,
setSettings,
style,
className,
onMouseDown,
onMouseUp,
onTouchEnd,
}: SettingsWidgetProps,
ref,
) => {
const theme = useTheme();
return React.useMemo(() => { return React.useMemo(() => {
return ( return (
<Paper <Paper
elevation={3} elevation={3}
style={{ width: '100%', height: '100%', position: 'relative', top: '0.6em', left: '0em' }} style={{ width: '100%', height: '100%', position: 'relative', top: '0.6em', left: '0em' }}
> >
<SettingsForm dense={true} settings={settings} setSettings={setSettings} /> <SettingsForm dense={true} />
</Paper> </Paper>
); );
}, [settings]); }, [settings]);

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connexió xifrada i anònima mitjançant TOR.", "Connection encrypted and anonymized using TOR.": "Connexió xifrada i anònima mitjançant TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "Això garanteix la màxima privadesa, però és possible que sentis que l'aplicació es comporta lenta. Si es perd la connexió, reinicia l'aplicació.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "Això garanteix la màxima privadesa, però és possible que sentis que l'aplicació es comporta lenta. Si es perd la connexió, reinicia l'aplicació.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Si us plau, introdueix el teu token per reconstruir el teu robot i accedir a les seves operacions.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Introdueix el teu token per reconstruir el teu robot i accedir a les seves operacions.",
"Paste token here": "Enganxa el token aquí", "Paste token here": "Enganxa el token aquí",
"Recover": "Recuperar", "Recover": "Recuperar",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.", "Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.", "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx", "#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.", "Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here", "Paste token here": "Paste token here",
"Recover": "Recover", "Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx", "#8": "Phrases in basic/RobotPage/Welcome.tsx",