From b25230378e1b542e18004534839456106a27ad33 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com> Date: Mon, 7 Nov 2022 10:13:02 +0000 Subject: [PATCH] Small fixes and partial TradeBox functional component (#309) * Add Order model * Add permanent settings * Fix maker on book page * Add chat models * Attempt to fix Android cookies * Add Tradebox Dialogs * Add Lock Invoice box * Add taker found prompt * Fix load setting cookies * Revert TradeBox for test release * Refactor ordering of constructing theme * Add load encrypted seetings --- frontend/src/App.tsx | 49 +- frontend/src/basic/BookPage/index.tsx | 11 +- frontend/src/basic/Main.tsx | 15 +- frontend/src/basic/MainDialogs/index.tsx | 7 + frontend/src/basic/MakerPage/index.tsx | 9 +- frontend/src/basic/NavBar/NavBar.tsx | 479 +---- frontend/src/basic/UserGenPage.js | 4 +- .../components/Charts/DepthChart/index.tsx | 18 +- frontend/src/components/Dialogs/Profile.tsx | 33 +- .../src/components/MakerForm/MakerForm.tsx | 1 - .../src/components/SettingsForm/index.tsx | 25 +- .../src/components/TradeBox/BondStatus.tsx | 44 + .../TradeBox/Dialogs/ConfirmDispute.tsx | 51 + .../TradeBox/Dialogs/ConfirmFiatReceived.tsx | 60 + .../src/components/TradeBox/Dialogs/index.ts | 2 + .../TradeBox/EncryptedChat/index.tsx | 12 +- .../TradeBox/Prompts/LockInvoice.tsx | 114 ++ .../TradeBox/Prompts/TakerFound.tsx | 36 + .../src/components/TradeBox/Prompts/index.ts | 2 + .../src/components/TradeBox/Title/index.tsx | 96 + frontend/src/components/TradeBox/stepXofY.ts | 47 + .../src/components/TradeBox/wip_index.tsx | 1605 +++++++++++++++++ frontend/src/components/UnsafeAlert.js | 37 +- frontend/src/models/Book.model.ts | 6 +- frontend/src/models/Chat.model.ts | 23 + frontend/src/models/Order.model.ts | 100 + frontend/src/models/Settings.default.basic.ts | 17 +- frontend/src/models/Settings.default.pro.ts | 18 +- frontend/src/models/Settings.model.ts | 53 +- frontend/src/models/index.ts | 11 +- .../System/SystemNativeClient/index.ts | 5 +- .../services/System/SystemWebClient/index.ts | 2 +- mobile/App.tsx | 3 + 33 files changed, 2384 insertions(+), 611 deletions(-) create mode 100644 frontend/src/components/TradeBox/BondStatus.tsx create mode 100644 frontend/src/components/TradeBox/Dialogs/ConfirmDispute.tsx create mode 100644 frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx create mode 100644 frontend/src/components/TradeBox/Dialogs/index.ts create mode 100644 frontend/src/components/TradeBox/Prompts/LockInvoice.tsx create mode 100644 frontend/src/components/TradeBox/Prompts/TakerFound.tsx create mode 100644 frontend/src/components/TradeBox/Prompts/index.ts create mode 100644 frontend/src/components/TradeBox/Title/index.tsx create mode 100644 frontend/src/components/TradeBox/stepXofY.ts create mode 100644 frontend/src/components/TradeBox/wip_index.tsx create mode 100644 frontend/src/models/Chat.model.ts create mode 100644 frontend/src/models/Order.model.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fb813daf..93974b34 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,38 +10,28 @@ import { I18nextProvider } from 'react-i18next'; import i18n from './i18n/Web'; import { systemClient } from './services/System'; -import { Settings, defaultSettings } from './models'; +import { Settings } from './models'; -const defaultTheme: Theme = createTheme({ - palette: { - mode: defaultSettings.mode, - background: { - default: defaultSettings.mode === 'dark' ? '#070707' : '#fff', +const makeTheme = function (settings: Settings) { + const theme: Theme = createTheme({ + palette: { + mode: settings.mode, + background: { + default: settings.mode === 'dark' ? '#070707' : '#fff', + }, }, - }, - typography: { fontSize: defaultSettings.fontSize }, -}); + typography: { fontSize: settings.fontSize }, + }); + + return theme; +}; const App = (): JSX.Element => { - const [theme, setTheme] = useState(defaultTheme); - const [settings, setSettings] = useState(defaultSettings); - - const updateTheme = function () { - setTheme( - createTheme({ - palette: { - mode: settings.mode, - background: { - default: settings.mode === 'dark' ? '#070707' : '#fff', - }, - }, - typography: { fontSize: settings.fontSize }, - }), - ); - }; + const [theme, setTheme] = useState(makeTheme(new Settings())); + const [settings, setSettings] = useState(new Settings()); useEffect(() => { - updateTheme(); + setTheme(makeTheme(settings)); }, [settings.fontSize, settings.mode]); useEffect(() => { @@ -53,8 +43,11 @@ const App = (): JSX.Element => { - - + {window.NativeRobosats === undefined ? ( + + ) : ( + + )}
diff --git a/frontend/src/basic/BookPage/index.tsx b/frontend/src/basic/BookPage/index.tsx index 23099d05..d96960ef 100644 --- a/frontend/src/basic/BookPage/index.tsx +++ b/frontend/src/basic/BookPage/index.tsx @@ -26,7 +26,7 @@ interface BookPageProps { setMaker: (state: Maker) => void; hasRobot: boolean; setPage: (state: Page) => void; - setOrder: (state: number) => void; + setCurrentOrder: (state: number) => void; } const BookPage = ({ @@ -42,7 +42,7 @@ const BookPage = ({ windowSize, hasRobot = false, setPage = () => null, - setOrder = () => null, + setCurrentOrder = () => null, }: BookPageProps): JSX.Element => { const { t } = useTranslation(); const history = useHistory(); @@ -76,7 +76,7 @@ const BookPage = ({ if (hasRobot) { history.push('/order/' + id); setPage('order'); - setOrder(id); + setCurrentOrder(id); } else { setOpenNoRobot(true); } @@ -125,6 +125,11 @@ const BookPage = ({ setFav={setFav} setPage={setPage} hasRobot={hasRobot} + onOrderCreated={(id) => { + setCurrentOrder(id); + setPage('order'); + history.push('/order/' + id); + }} /> diff --git a/frontend/src/basic/Main.tsx b/frontend/src/basic/Main.tsx index 0412160f..7f582022 100644 --- a/frontend/src/basic/Main.tsx +++ b/frontend/src/basic/Main.tsx @@ -73,7 +73,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => { in: undefined, out: undefined, }); - const [order, setOrder] = useState(null); + const [currentOrder, setCurrentOrder] = useState(null); const navbarHeight = 2.5; const closeAll = { @@ -163,7 +163,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => { setRobot({ ...robot, loading: true }); apiClient.post('/api/user/', requestBody).then((data: any) => { - setOrder( + setCurrentOrder( data.active_order_id ? data.active_order_id : data.last_order_id @@ -230,8 +230,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
{ windowSize={windowSize} hasRobot={robot.avatarLoaded} setPage={setPage} - setOrder={setOrder} + setCurrentOrder={setCurrentOrder} />
@@ -282,7 +281,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => { maker={maker} setMaker={setMaker} setPage={setPage} - setOrder={setOrder} + setCurrentOrder={setCurrentOrder} fav={fav} setFav={setFav} windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }} @@ -334,13 +333,15 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => { setOpen={setOpen} closeAll={closeAll} setSlideDirection={setSlideDirection} - order={order} + currentOrder={currentOrder} hasRobot={robot.avatarLoaded} /> void; + setPage: (state: Page) => void; + setCurrentOrder: (state: number) => void; closeAll: OpenDialogs; } @@ -37,6 +40,8 @@ const MainDialogs = ({ closeAll, robot, setRobot, + setPage, + setCurrentOrder, }: MainDialogsProps): JSX.Element => { useEffect(() => { if (info.openUpdateClient) { @@ -77,6 +82,8 @@ const MainDialogs = ({ onClose={() => setOpen({ ...open, profile: false })} robot={robot} setRobot={setRobot} + setPage={setPage} + setCurrentOrder={setCurrentOrder} /> ); diff --git a/frontend/src/basic/MakerPage/index.tsx b/frontend/src/basic/MakerPage/index.tsx index ea46f1b3..2635c04a 100644 --- a/frontend/src/basic/MakerPage/index.tsx +++ b/frontend/src/basic/MakerPage/index.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; import { Grid, Paper, Collapse, Typography } from '@mui/material'; import { LimitList, Maker, Book, Favorites } from '../../models'; @@ -21,7 +22,7 @@ interface MakerPageProps { setMaker: (state: Maker) => void; windowSize: { width: number; height: number }; hasRobot: boolean; - setOrder: (state: number) => void; + setCurrentOrder: (state: number) => void; setPage: (state: Page) => void; } @@ -34,11 +35,12 @@ const MakerPage = ({ setFav, setMaker, windowSize, - setOrder, + setCurrentOrder, setPage, hasRobot = false, }: MakerPageProps): JSX.Element => { const { t } = useTranslation(); + const history = useHistory(); const maxHeight = windowSize.height * 0.85 - 3; const [showMatches, setShowMatches] = useState(false); @@ -95,8 +97,9 @@ const MakerPage = ({ maker={maker} setMaker={setMaker} onOrderCreated={(id) => { - setOrder(id); + setCurrentOrder(id); setPage('order'); + history.push('/order/' + id); }} hasRobot={hasRobot} disableRequest={matches.length > 0 && !showMatches} diff --git a/frontend/src/basic/NavBar/NavBar.tsx b/frontend/src/basic/NavBar/NavBar.tsx index 6a9c36ca..79b2ae1f 100644 --- a/frontend/src/basic/NavBar/NavBar.tsx +++ b/frontend/src/basic/NavBar/NavBar.tsx @@ -30,7 +30,7 @@ interface NavBarProps { open: OpenDialogs; setOpen: (state: OpenDialogs) => void; closeAll: OpenDialogs; - order: number | null; + currentOrder: number | null; hasRobot: boolean; } @@ -44,7 +44,7 @@ const NavBar = ({ closeAll, width, height, - order, + currentOrder, hasRobot = false, }: NavBarProps): JSX.Element => { const theme = useTheme(); @@ -77,7 +77,7 @@ const NavBar = ({ } else { handleSlideDirection(page, newPage); setPage(newPage); - const param = newPage === 'order' ? order ?? '' : ''; + const param = newPage === 'order' ? currentOrder ?? '' : ''; setTimeout( () => history.push(`/${newPage}/${param}`), theme.transitions.duration.leavingScreen * 3, @@ -92,7 +92,7 @@ const NavBar = ({ return ( } iconPosition='start' /> @@ -176,472 +176,3 @@ const NavBar = ({ }; export default NavBar; - -// constructor(props) { -// super(props); -// this.state = { -// profileShown: false, -// openStatsForNerds: false, -// openCommunity: false, -// openExchangeSummary: false, -// openClaimRewards: false, -// openProfile: false, -// showRewards: false, -// rewardInvoice: null, -// badInvoice: false, -// showRewardsSpinner: false, -// withdrawn: false, -// }; -// } - -// handleClickOpenStatsForNerds = () => { -// this.setState({ openStatsForNerds: true }); -// }; - -// handleClickCloseStatsForNerds = () => { -// this.setState({ openStatsForNerds: false }); -// }; - -// handleClickOpenCommunity = () => { -// this.setState({ openCommunity: true }); -// }; - -// handleClickCloseCommunity = () => { -// this.setState({ openCommunity: false }); -// }; - -// handleClickOpenProfile = () => { -// this.setState({ openProfile: true, profileShown: true }); -// }; - -// handleClickCloseProfile = () => { -// this.setState({ openProfile: false }); -// }; - -// handleClickOpenExchangeSummary = () => { -// this.setState({ openExchangeSummary: true }); -// }; - -// handleClickCloseExchangeSummary = () => { -// this.setState({ openExchangeSummary: false }); -// }; - -// handleSubmitInvoiceClicked = (e, rewardInvoice) => { -// this.setState({ badInvoice: false, showRewardsSpinner: true }); - -// apiClient -// .post('/api/reward/', { -// invoice: rewardInvoice, -// }) -// .then((data) => { -// this.setState({ badInvoice: data.bad_invoice, showRewardsSpinner: false }); -// this.props.setInfo({ -// ...this.props.info, -// badInvoice: data.bad_invoice, -// openClaimRewards: !data.successful_withdrawal, -// withdrawn: !!data.successful_withdrawal, -// showRewardsSpinner: false, -// }); -// this.props.setRobot({ -// ...this.props.robot, -// earnedRewards: data.successful_withdrawal ? 0 : this.props.robot.earnedRewards, -// }); -// }); -// e.preventDefault(); -// }; - -// handleSetStealthInvoice = (wantsStealth) => { -// apiClient -// .put('/api/stealth/', { wantsStealth }) -// .then((data) => -// this.props.setRobot({ ...this.props.robot, stealthInvoices: data?.wantsStealth }), -// ); -// }; - -// showProfileButton = () => { -// return ( -// this.props.robot.avatarLoaded && -// (this.props.robot.token -// ? systemClient.getCookie('robot_token') === this.props.robot.token -// : true) && -// systemClient.getCookie('sessionid') -// ); -// }; - -// bottomBarDesktop = () => { -// const { t } = this.props; -// const hasRewards = this.props.robot.earnedRewards > 0; -// const hasOrder = !!( -// (this.props.robot.activeOrderId > 0) & -// !this.state.profileShown & -// this.props.robot.avatarLoaded -// ); -// const fontSize = this.props.theme.typography.fontSize; -// const fontSizeFactor = fontSize / 14; // default fontSize is 14 -// const typographyProps = { -// primaryTypographyProps: { fontSize }, -// secondaryTypographyProps: { fontSize: (fontSize * 12) / 14 }, -// }; -// return ( -// -// -// -//
-// -// -// -// 0) & !this.state.profileShown -// ? 'primary' -// : undefined -// } -// nickname={this.props.robot.nickname} -// onLoad={() => -// this.props.setRobot({ ...this.props.robot, avatarLoaded: true }) -// } -// /> -// -// -// -// -//
-//
- -// -// -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// -// - -// -// -// {this.LangSelect()} -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -//
-//
-// ); -// }; - -// bottomBarPhone = () => { -// const { t } = this.props; -// const hasRewards = this.props.robot.earnedRewards > 0; -// const hasOrder = !!( -// (this.props.info.active_order_id > 0) & -// !this.state.profileShown & -// this.props.robot.avatarLoaded -// ); -// return ( -// -// -// -//
-// -// -// 0) & !this.state.profileShown -// ? 'primary' -// : undefined -// } -// nickname={this.props.robot.nickname} -// onLoad={() => this.props.setRobot({ ...this.props.robot, avatarLoaded: true })} -// /> -// -// -//
-//
- -// -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// - -// -// -// {this.LangSelect()} -// -// -// -// -// -// -// -// -// -// -// this.props.fetchInfo()} -// onClick={this.handleClickOpenStatsForNerds} -// > -// -// -// -// -// -//
-//
-// ); -// }; - -// render() { -// return ( -//
- -// -// this.props.setInfo({ ...this.props.info, openUpdateClient: false }) -// } -// /> - -// - -// this.props.setRobot({ ...robot, ...newParam })} -// stealthInvoices={this.props.robot.stealthInvoices} -// handleSetStealthInvoice={this.handleSetStealthInvoice} -// /> - -// - -// {this.bottomBarDesktop()} - -// {this.bottomBarPhone()} -//
-// ); -// } -// } - -// export default NavBar; diff --git a/frontend/src/basic/UserGenPage.js b/frontend/src/basic/UserGenPage.js index 15e7d0a9..23fd4f34 100644 --- a/frontend/src/basic/UserGenPage.js +++ b/frontend/src/basic/UserGenPage.js @@ -73,12 +73,12 @@ class UserGenPage extends Component { requestBody.then((body) => apiClient.post('/api/user/', body).then((data) => { this.setState({ found: data.found, bad_request: data.bad_request }); - this.props.setOrder( + this.props.setCurrentOrder( data.active_order_id ? data.active_order_id : data.last_order_id ? data.last_order_id - : this.props.order, + : null, ); // Add nick and token to App state (token only if not a bad request) data.bad_request diff --git a/frontend/src/components/Charts/DepthChart/index.tsx b/frontend/src/components/Charts/DepthChart/index.tsx index 438b98e2..d6d3aae8 100644 --- a/frontend/src/components/Charts/DepthChart/index.tsx +++ b/frontend/src/components/Charts/DepthChart/index.tsx @@ -21,7 +21,7 @@ import { import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; -import { Order, LimitList } from '../../../models'; +import { PublicOrder, LimitList } from '../../../models'; import RobotAvatar from '../../RobotAvatar'; import { amountToString, matchMedian, statusBadgeColor } from '../../../utils'; import currencyDict from '../../../../static/assets/currencies.json'; @@ -29,7 +29,7 @@ import { PaymentStringAsIcons } from '../../PaymentMethods'; import getNivoScheme from '../NivoScheme'; interface DepthChartProps { - orders: Order[]; + orders: PublicOrder[]; lastDayPremium?: number | undefined; currency: number; limits: LimitList; @@ -114,15 +114,17 @@ const DepthChart: React.FC = ({ }, [enrichedOrders, xType, lastDayPremium, currencyCode]); const generateSeries: () => void = () => { - const sortedOrders: Order[] = + const sortedOrders: PublicOrder[] = xType === 'base_amount' ? enrichedOrders.sort( (order1, order2) => (order1?.base_amount || 0) - (order2?.base_amount || 0), ) : enrichedOrders.sort((order1, order2) => order1.premium - order2.premium); - const sortedBuyOrders: Order[] = sortedOrders.filter((order) => order.type == 0).reverse(); - const sortedSellOrders: Order[] = sortedOrders.filter((order) => order.type == 1); + const sortedBuyOrders: PublicOrder[] = sortedOrders + .filter((order) => order.type == 0) + .reverse(); + const sortedSellOrders: PublicOrder[] = sortedOrders.filter((order) => order.type == 1); const buySerie: Datum[] = generateSerie(sortedBuyOrders); const sellSerie: Datum[] = generateSerie(sortedSellOrders); @@ -142,7 +144,7 @@ const DepthChart: React.FC = ({ ]); }; - const generateSerie = (orders: Order[]): Datum[] => { + const generateSerie = (orders: PublicOrder[]): Datum[] => { if (center == undefined) { return []; } @@ -159,7 +161,7 @@ const DepthChart: React.FC = ({ y: lastSumOrders, }, { - // Order Point + // PublicOrder Point x: xType === 'base_amount' ? order.base_amount : order.premium, y: sumOrders, order, @@ -220,7 +222,7 @@ const DepthChart: React.FC = ({ const generateTooltip: React.FunctionComponent = ( pointTooltip: PointTooltipProps, ) => { - const order: Order = pointTooltip.point.data.order; + const order: PublicOrder = pointTooltip.point.data.order; return order ? ( diff --git a/frontend/src/components/Dialogs/Profile.tsx b/frontend/src/components/Dialogs/Profile.tsx index 7250a5ea..6dcab0bb 100644 --- a/frontend/src/components/Dialogs/Profile.tsx +++ b/frontend/src/components/Dialogs/Profile.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useTheme } from '@mui/material/styles'; -import { Link as LinkRouter } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { Badge, @@ -41,16 +41,27 @@ import { getHost, getWebln } from '../../utils'; import RobotAvatar from '../RobotAvatar'; import { apiClient } from '../../services/api'; import { Robot } from '../../models'; +import { Page } from '../../basic/NavBar'; interface Props { open: boolean; onClose: () => void; robot: Robot; setRobot: (state: Robot) => void; + setPage: (state: Page) => void; + setCurrentOrder: (state: number) => void; } -const ProfileDialog = ({ open = false, onClose, robot, setRobot }: Props): JSX.Element => { +const ProfileDialog = ({ + open = false, + onClose, + robot, + setRobot, + setPage, + setCurrentOrder, +}: Props): JSX.Element => { const { t } = useTranslation(); + const history = useHistory(); const theme = useTheme(); const host = getHost(); @@ -179,9 +190,12 @@ const ProfileDialog = ({ open = false, onClose, robot, setRobot }: Props): JSX.E {robot.activeOrderId ? ( { + history.push('/order/' + robot.activeOrderId); + setPage('order'); + setCurrentOrder(robot.activeOrderId); + onClose(); + }} > @@ -195,9 +209,12 @@ const ProfileDialog = ({ open = false, onClose, robot, setRobot }: Props): JSX.E ) : robot.lastOrderId ? ( { + history.push('/order/' + robot.lastOrderId); + setPage('order'); + setCurrentOrder(robot.lastOrderId); + onClose(); + }} > diff --git a/frontend/src/components/MakerForm/MakerForm.tsx b/frontend/src/components/MakerForm/MakerForm.tsx index 1d383688..657ef04e 100644 --- a/frontend/src/components/MakerForm/MakerForm.tsx +++ b/frontend/src/components/MakerForm/MakerForm.tsx @@ -266,7 +266,6 @@ const MakerForm = ({ apiClient.post('/api/make/', body).then((data: object) => { setBadRequest(data.bad_request); if (data.id) { - history.push('/order/' + data.id); onOrderCreated(data.id); } setSubmittingRequest(false); diff --git a/frontend/src/components/SettingsForm/index.tsx b/frontend/src/components/SettingsForm/index.tsx index e25941ba..4417529e 100644 --- a/frontend/src/components/SettingsForm/index.tsx +++ b/frontend/src/components/SettingsForm/index.tsx @@ -24,6 +24,7 @@ import { SettingsOverscan, Link, } from '@mui/icons-material'; +import { systemClient } from '../../services/System'; interface SettingsFormProps { dense?: boolean; @@ -58,7 +59,10 @@ const SettingsForm = ({ setSettings({ ...settings, language })} + setLanguage={(language) => { + setSettings({ ...settings, language }); + systemClient.setCookie('settings_language', language); + }} /> @@ -103,9 +107,11 @@ const SettingsForm = ({ } - onChange={(e) => - setSettings({ ...settings, mode: e.target.checked ? 'dark' : 'light' }) - } + onChange={(e) => { + const mode = e.target.checked ? 'dark' : 'light'; + setSettings({ ...settings, mode }); + systemClient.setCookie('settings_mode', mode); + }} /> } /> @@ -120,7 +126,14 @@ const SettingsForm = ({ min={settings.frontend == 'basic' ? 12 : 10} max={settings.frontend == 'basic' ? 16 : 14} step={1} - onChange={(e) => setSettings({ ...settings, fontSize: e.target.value })} + onChange={(e) => { + const fontSize = e.target.value; + setSettings({ ...settings, fontSize }); + systemClient.setCookie( + `settings_fontsize_${settings.frontend}`, + fontSize.toString(), + ); + }} valueLabelDisplay='off' marks={fontSizes.map(({ label, value }) => ({ label: {t(label)}, @@ -137,7 +150,7 @@ const SettingsForm = ({ setSettings({ ...settings, network: value })} + onChange={(e, network) => setSettings({ ...settings, network })} > {t('Mainnet')} diff --git a/frontend/src/components/TradeBox/BondStatus.tsx b/frontend/src/components/TradeBox/BondStatus.tsx new file mode 100644 index 00000000..e8534262 --- /dev/null +++ b/frontend/src/components/TradeBox/BondStatus.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, Typography } from '@mui/material'; +import { Lock, LockOpen, Balance } from '@mui/icons-material'; + +interface BondStatusProps { + status: 'locked' | 'settled' | 'returned' | 'hide'; + isMaker: boolean; +} + +const BondStatus = ({ status, isMaker }: BondStatusProps): JSX.Element => { + const { t } = useTranslation(); + + let Icon = Lock; + if (status === 'returned') { + Icon = LockOpen; + } else if (status === 'settled') { + Icon = Balance; + } + + if (status === 'hide') { + return <>; + } else { + return ( + + +
+ + {t(`Your ${isMaker ? 'maker' : 'taker'} bond is ${status}`)} +
+
+
+ ); + } +}; + +export default BondStatus; diff --git a/frontend/src/components/TradeBox/Dialogs/ConfirmDispute.tsx b/frontend/src/components/TradeBox/Dialogs/ConfirmDispute.tsx new file mode 100644 index 00000000..c4964ee8 --- /dev/null +++ b/frontend/src/components/TradeBox/Dialogs/ConfirmDispute.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Dialog, + DialogTitle, + DialogActions, + DialogContent, + DialogContentText, + Button, +} from '@mui/material'; + +interface ConfirmDisputeDialogProps { + open: boolean; + onClose: () => void; + onAgreeClick: () => void; +} + +export const ConfirmDisputeDialog = ({ + open, + onClose, + onAgreeClick, +}: ConfirmDisputeDialogProps): JSX.Element => { + const { t } = useTranslation(); + + return ( + + {t('Do you want to open a dispute?')} + + + {t( + 'The RoboSats staff will examine the statements and evidence provided. You need to build a complete case, as the staff cannot read the chat. It is best to provide a burner contact method with your statement. The satoshis in the trade escrow will be sent to the dispute winner, while the dispute loser will lose the bond.', + )} + +
+ + {t( + 'Make sure to EXPORT the chat log. The staff might request your exported chat log JSON in order to solve discrepancies. It is your responsibility to store it.', + )} + +
+ + + + +
+ ); +}; + +export default ConfirmDisputeDialog; diff --git a/frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx b/frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx new file mode 100644 index 00000000..f6c57851 --- /dev/null +++ b/frontend/src/components/TradeBox/Dialogs/ConfirmFiatReceived.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Dialog, + DialogTitle, + DialogActions, + DialogContent, + DialogContentText, + Button, +} from '@mui/material'; +import { Order } from '../../../models'; +import currencies from '../../../../static/assets/currencies.json'; +import { pn } from '../../../utils'; +import { LoadingButton } from '@mui/lab'; + +interface ConfirmFiatReceivedDialogProps { + open: boolean; + loadingButton: boolean; + order: Order; + onClose: () => void; + onConfirmClick: () => void; +} + +export const ConfirmFiatReceivedDialog = ({ + open, + loadingButton, + onClose, + order, + onConfirmClick, +}: ConfirmFiatReceivedDialogProps): JSX.Element => { + const { t } = useTranslation(); + const currencyCode = currencies[order.currency.toString()]; + const amount = pn(parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4))); + + return ( + + + {t('Confirm you received {{amount}} {{currencyCode}}?', { currencyCode, amount })} + + + + {t( + 'Confirming that you received the fiat will finalize the trade. The satoshis in the escrow will be released to the buyer. Only confirm after the {{amount}} {{currencyCode}} have arrived to your account. In addition, if you have received the payment and do not confirm it, you risk losing your bond.', + { currencyCode, amount }, + )} + + + + + + {t('Confirm')} + + + + ); +}; + +export default ConfirmFiatReceivedDialog; diff --git a/frontend/src/components/TradeBox/Dialogs/index.ts b/frontend/src/components/TradeBox/Dialogs/index.ts new file mode 100644 index 00000000..1effc88f --- /dev/null +++ b/frontend/src/components/TradeBox/Dialogs/index.ts @@ -0,0 +1,2 @@ +export { ConfirmDisputeDialog } from './ConfirmDispute'; +export { ConfirmFiatReceivedDialog } from './ConfirmFiatReceived'; diff --git a/frontend/src/components/TradeBox/EncryptedChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/index.tsx index 53041e7f..515a0d82 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/index.tsx @@ -28,21 +28,13 @@ import CircularProgress from '@mui/material/CircularProgress'; import KeyIcon from '@mui/icons-material/Key'; import { ExportIcon } from '../../Icons'; import { useTheme } from '@mui/system'; +import { WebSocketsChatMessage } from '../../../models'; interface Props { orderId: number; userNick: string; } -interface EncryptedChatMessage { - userNick: string; - validSignature: boolean; - plainTextMessage: string; - encryptedMessage: string; - time: string; - index: number; -} - const EncryptedChat: React.FC = ({ orderId, userNick }: Props): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); @@ -58,7 +50,7 @@ const EncryptedChat: React.FC = ({ orderId, userNick }: Props): JSX.Eleme ); const [peerPubKey, setPeerPubKey] = useState(); const [token] = useState(systemClient.getCookie('robot_token') || ''); - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); const [serverMessages, setServerMessages] = useState([]); const [value, setValue] = useState(''); const [connection, setConnection] = useState(); diff --git a/frontend/src/components/TradeBox/Prompts/LockInvoice.tsx b/frontend/src/components/TradeBox/Prompts/LockInvoice.tsx new file mode 100644 index 00000000..aca7d667 --- /dev/null +++ b/frontend/src/components/TradeBox/Prompts/LockInvoice.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Grid, Link, Typography, TextField, Tooltip, useTheme } from '@mui/material'; +import { AccountBalanceWallet, ContentCopy } from '@mui/icons-material'; +import { NewTabIcon } from '../../Icons'; +import QRCode from 'react-qr-code'; +import { Order } from '../../../models'; +import { systemClient } from '../../../services/System'; +import currencies from '../../../../static/assets/currencies.json'; + +interface LockInvoicePromptProps { + order: Order; + concept: 'bond' | 'escrow'; +} + +export const LockInvoicePrompt = ({ order, concept }: LockInvoicePromptProps): JSX.Element => { + const { t } = useTranslation(); + const theme = useTheme(); + const currencyCode: string = currencies[`${order.currency}`]; + + const invoice = concept === 'bond' ? order.bond_invoice : order.escrow_invoice; + const helperText = + concept === 'bond' + ? t( + 'This is a hold invoice, it will freeze in your wallet. It will be charged only if you cancel or lose a dispute.', + ) + : t( + 'This is a hold invoice, it will freeze in your wallet. It will be released to the buyer once you confirm to have received the {{currencyCode}}.', + { currencyCode }, + ); + + const CompatibleWalletsButton = function () { + return ( + + ); + }; + + const depositHoursMinutes = function () { + const hours = Math.floor(order.escrow_duration / 3600); + const minutes = Math.floor((order.escrow_duration - hours * 3600) / 60); + const dict = { deposit_timer_hours: hours, deposit_timer_minutes: minutes }; + return dict; + }; + + const ExpirationWarning = function () { + return ( + + {t( + 'You risk losing your bond if you do not lock the collateral. Total time available is {{deposit_timer_hours}}h {{deposit_timer_minutes}}m.', + depositHoursMinutes(), + )} + + ); + }; + + return ( + + + {concept === 'bond' ? : } + + + + + { + systemClient.copyToClipboard(invoice); + }} + /> + + + + + + + + + + + + ); +}; + +export default LockInvoicePrompt; diff --git a/frontend/src/components/TradeBox/Prompts/TakerFound.tsx b/frontend/src/components/TradeBox/Prompts/TakerFound.tsx new file mode 100644 index 00000000..cce21870 --- /dev/null +++ b/frontend/src/components/TradeBox/Prompts/TakerFound.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Grid, Typography } from '@mui/material'; +import { Order } from '../../../models'; +import stepXofY from '../stepXofY'; + +interface TakerFoundPrompProps { + order: Order; +} + +export const TakerFoundPrompt = ({ order }: TakerFoundPrompProps): JSX.Element => { + const { t } = useTranslation(); + + const Title = function () { + return ( + + {t('A taker has been found!')} + {` ${stepXofY(order)}`} + + ); + }; + + return ( + + + + {t( + 'Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.', + )} + + + + ); +}; + +export default TakerFoundPrompt; diff --git a/frontend/src/components/TradeBox/Prompts/index.ts b/frontend/src/components/TradeBox/Prompts/index.ts new file mode 100644 index 00000000..042e08e1 --- /dev/null +++ b/frontend/src/components/TradeBox/Prompts/index.ts @@ -0,0 +1,2 @@ +export { LockInvoicePrompt } from './LockInvoice'; +export { TakerFoundPrompt } from './TakerFound'; diff --git a/frontend/src/components/TradeBox/Title/index.tsx b/frontend/src/components/TradeBox/Title/index.tsx new file mode 100644 index 00000000..402bf64d --- /dev/null +++ b/frontend/src/components/TradeBox/Title/index.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Typography, useTheme } from '@mui/material'; +import { Order } from '../../../models'; +import stepXofY from '../stepXofY'; +import currencies from '../../../../static/assets/currencies.json'; +import { pn } from '../../../utils'; + +interface TakerFoundPrompProps { + order: Order; +} + +export const Title = ({ order }: TakerFoundPrompProps): JSX.Element => { + const { t } = useTranslation(); + const theme = useTheme(); + const currencyCode: string = currencies[`${order.currency}`]; + + let text = ''; + + if (order.is_maker && order.status === 0) { + text = t('Lock {{amountSats}} Sats to PUBLISH order', { amountSats: pn(order.bond_satoshis) }); + } else if (order.is_taker && order.status === 3) { + text = t('Lock {{amountSats}} Sats to TAKE order', { amountSats: pn(order.bond_satoshis) }); + } else if (order.is_seller && [6, 7].includes(order.status)) { + text = t('Lock {{amountSats}} Sats as collateral', { amountSats: pn(order.escrow_satoshis) }); + } + + { + /* Maker and taker Bond request */ + } + // {this.props.data.is_maker & (this.props.data.status == 0) ? this.showQRInvoice() : ''} + // {this.props.data.is_taker & (this.props.data.status == 3) ? this.showQRInvoice() : ''} + + // {/* Waiting for taker and taker bond request */} + // {this.props.data.is_maker & (this.props.data.status == 2) ? this.showPausedOrder() : ''} + // {this.props.data.is_maker & (this.props.data.status == 1) ? this.showMakerWait() : ''} + // {this.props.data.is_maker & (this.props.data.status == 3) ? this.showTakerFound() : ''} + + // {/* Send Invoice (buyer) and deposit collateral (seller) */} + // {this.props.data.is_seller & + // (this.props.data.status == 6 || this.props.data.status == 7) + // ? this.showEscrowQRInvoice() + // : ''} + // {this.props.data.is_buyer & (this.props.data.status == 6 || this.props.data.status == 8) + // ? this.showInputInvoice() + // : ''} + // {this.props.data.is_buyer & (this.props.data.status == 7) + // ? this.showWaitingForEscrow() + // : ''} + // {this.props.data.is_seller & (this.props.data.status == 8) + // ? this.showWaitingForBuyerInvoice() + // : ''} + + // {/* In Chatroom */} + // {this.props.data.status == 9 || this.props.data.status == 10 ? this.showChat() : ''} + + // {/* Trade Finished */} + // {this.props.data.is_seller & [13, 14, 15].includes(this.props.data.status) + // ? this.showRateSelect() + // : ''} + // {this.props.data.is_buyer & (this.props.data.status == 14) ? this.showRateSelect() : ''} + + // {/* Trade Finished - Payment Routing Failed */} + // {this.props.data.is_buyer & (this.props.data.status == 13) + // ? this.showSendingPayment() + // : ''} + + // {/* Trade Finished - Payment Routing Failed */} + // {this.props.data.is_buyer & (this.props.data.status == 15) + // ? this.showRoutingFailed() + // : ''} + + // {/* Trade Finished - TODO Needs more planning */} + // {this.props.data.status == 11 ? this.showInDisputeStatement() : ''} + // {this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ''} + // {(this.props.data.status == 17) & this.props.data.is_taker || + // (this.props.data.status == 18) & this.props.data.is_maker + // ? this.showDisputeWinner() + // : ''} + // {(this.props.data.status == 18) & this.props.data.is_taker || + // (this.props.data.status == 17) & this.props.data.is_maker + // ? this.showDisputeLoser() + // : ''} + + // {/* Order has expired */} + // {this.props.data.status == 5 ? this.showOrderExpired() : ''} + + return ( + + {text} + {stepXofY(order)} + + ); +}; + +export default Title; diff --git a/frontend/src/components/TradeBox/stepXofY.ts b/frontend/src/components/TradeBox/stepXofY.ts new file mode 100644 index 00000000..98644d15 --- /dev/null +++ b/frontend/src/components/TradeBox/stepXofY.ts @@ -0,0 +1,47 @@ +import { Order } from '../../models'; + +const stepXofY = function (order: Order): string { + // set y value + let x: number | null = null; + let y: number | null = null; + + if (order.is_maker) { + y = 5; + } else if (order.is_taker) { + y = 4; + } + + // set x values + if (order.is_maker) { + if (order.status === 0) { + x = 1; + } else if ([1, 2, 3].includes(order.status)) { + x = 2; + } else if ([6, 7, 8].includes(order.status)) { + x = 3; + } else if (order.status === 9) { + x = 4; + } else if (order.status === 10) { + x = 5; + } + } else if (order.is_taker) { + if (order.status === 3) { + x = 1; + } else if ([6, 7, 8].includes(order.status)) { + x = 2; + } else if (order.status === 9) { + x = 3; + } else if (order.status === 10) { + x = 4; + } + } + + // Return "(x/y)" + if (x != null && y != null) { + return `(${x}/${y})`; + } else { + return ''; + } +}; + +export default stepXofY; diff --git a/frontend/src/components/TradeBox/wip_index.tsx b/frontend/src/components/TradeBox/wip_index.tsx new file mode 100644 index 00000000..f1409d93 --- /dev/null +++ b/frontend/src/components/TradeBox/wip_index.tsx @@ -0,0 +1,1605 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, Divider } from '@mui/material'; + +import { systemClient } from '../../services/System'; +import { apiClient } from '../../services/api'; +import { ConfirmDisputeDialog, ConfirmFiatReceivedDialog } from './Dialogs'; + +import Title from './Title'; +import { LockInvoicePrompt, TakerFoundPrompt } from './Prompts'; +import BondStatus from './BondStatus'; + +import { Order } from '../../models'; + +const audio = { + chat: new Audio(`/static/assets/sounds/chat-open.mp3`), + takerFound: new Audio(`/static/assets/sounds/taker-found.mp3`), + lockedInvoice: new Audio(`/static/assets/sounds/locked-invoice.mp3`), + successful: new Audio(`/static/assets/sounds/successful.mp3`), +}; + +interface loadingButtonsProps { + fiatSent: boolean; + fiatReceived: boolean; + submitInvoice: boolean; + submitAddress: boolean; + openDispute: boolean; +} + +const noLoadingButtons: loadingButtonsProps = { + fiatSent: false, + fiatReceived: false, + submitInvoice: false, + submitAddress: false, + openDispute: false, +}; + +interface OpenDialogProps { + confirmFiatReceived: boolean; + confirmDispute: boolean; +} + +const closeAll: OpenDialogProps = { confirmFiatReceived: false, confirmDispute: false }; + +interface OnchainFormProps { + address: string; + miningFee: number; + badAddress: string; +} + +interface LightningFormProps { + invoice: string; + routingBudget: number; + badInvoice: string; + useLnproxy: boolean; + lnproxyServer: string; + lnproxyBudget: number; + badLnproxy: string; +} + +const defaultOnchain: OnchainFormProps = { + address: '', + miningFee: 140, + badAddress: '', +}; + +const defaultLightning: LightningFormProps = { + invoice: '', + routingBudget: 0, + badInvoice: '', + useLnproxy: false, + lnproxyServer: '', + lnproxyBudget: 0, + badLnproxy: '', +}; + +interface TradeBoxProps { + order: Order; + setOrder: (state: Order) => void; +} + +const TradeBox = ({ order, setOrder }: TradeBoxProps): JSX.Element => { + const { t } = useTranslation(); + + // Buttons and Dialogs + const [loadingButtons, setLoadingButtons] = useState(noLoadingButtons); + const [open, setOpen] = useState(closeAll); + + // Forms + const [onchain, setOnchain] = useState(defaultOnchain); + const [lightning, setLightning] = useState(defaultLightning); + const [statement, setStatement] = useState(''); + + // Sounds + useEffect(() => { + if (order.status != oldOrder.status) { + if (order.status === 1) { + audio.lockedInvoice.play(); + } + } + }, [order.status]); + + const submitAction = function (action: string) { + apiClient + .post('/api/order/?order_id=' + order.id, { action }) + .then((data: Order | undefined) => { + setOrder({ ...order, ...data }); + setOpen(closeAll); + setLoadingButtons({ ...noLoadingButtons }); + }); + }; + + const onClickAgreeOpenDispute = function () { + setLoadingButtons({ ...noLoadingButtons, fiatReceived: true }); + submitAction('dispute'); + }; + + const onClickConfirmFiatReceived = function () { + setLoadingButtons({ ...noLoadingButtons, fiatReceived: true }); + submitAction('confirm'); + }; + + const Steps = [ + { + isMaker: { + title: '', + prompt: , + bondStatus: 'none', + }, + }, + ]; + + const StepConcent = Steps[order.status][isMaker]; + + // 0: 2000, // 'Waiting for maker bond' + // 1: 25000, // 'Public' + // 2: 90000, // 'Paused' + // 3: 2000, // 'Waiting for taker bond' + // 4: 999999, // 'Cancelled' + // 5: 999999, // 'Expired' + // 6: 6000, // 'Waiting for trade collateral and buyer invoice' + // 7: 8000, // 'Waiting only for seller trade collateral' + // 8: 8000, // 'Waiting only for buyer invoice' + // 9: 10000, // 'Sending fiat - In chatroom' + // 10: 10000, // 'Fiat sent - In chatroom' + // 11: 30000, // 'In dispute' + // 12: 999999, // 'Collaboratively cancelled' + // 13: 3000, // 'Sending satoshis to buyer' + // 14: 999999, // 'Sucessful trade' + // 15: 10000, // 'Failed lightning network routing' + // 16: 180000, // 'Wait for dispute resolution' + // 17: 180000, // 'Maker lost dispute' + // 18: 180000, // 'Taker lost dispute' + + return ( + + setOpen(closeAll)} + onAgreeClick={onClickAgreeOpenDispute} + /> + + + <Divider /> + <StepConcent.prompt /> + <Divider /> + <BondStatus status={StepConcent.bondStatus} isMaker={order.is_maker} /> + + <ConfirmFiatReceivedDialog + open={open.confirmFiatReceived} + order={order} + loadingButton={loadingButtons.fiatReceived} + onClose={() => setOpen(closeAll)} + onConfirmClick={onClickConfirmFiatReceived} + /> + </Box> + ); +}; + +export default TradeBox; + +// class TradeBox extends Component { + +// showTakerFound = () => { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// {/* Make bell sound when taker is found. SUPRESSED: It's annoying, not the right moment! Play only after taker locks bon */} +// {/* {this.Sound("taker-found")} */} +// <Grid item xs={12} align='center'> +// <Typography variant='subtitle1'> +// <b>{t('A taker has been found!')}</b> {' ' + this.stepXofY()} +// </Typography> +// </Grid> +// <Divider /> +// <Grid item xs={12} align='center'> +// <Typography variant='body2'> +// {t( +// 'Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.', +// )} +// </Typography> +// </Grid> +// // <BondStatus status={'locked'} isMaker={order.is_maker}/> +// </Grid> +// ); +// }; + +// depositHoursMinutes = () => { +// const hours = parseInt(this.props.data.escrow_duration / 3600); +// const minutes = parseInt((this.props.data.escrow_duration - hours * 3600) / 60); +// const dict = { deposit_timer_hours: hours, deposit_timer_minutes: minutes }; +// return dict; +// }; + +// handleClickPauseOrder = () => { +// this.props.completeSetState({ pauseLoading: true }); +// apiClient +// .post('/api/order/?order_id=' + this.props.data.id, { +// action: 'pause', +// }) +// .then((data) => this.props.getOrderDetails(data.id)); +// }; + +// showMakerWait = () => { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// {/* Make confirmation sound for HTLC received. */} +// {this.Sound('locked-invoice')} +// <Grid item xs={12} align='center'> +// <Typography variant='subtitle1'> +// <b> {t('Your order is public')} </b> {' ' + this.stepXofY()} +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2' align='left'> +// {t( +// 'Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{deposit_timer_hours}}h {{deposit_timer_minutes}}m to reply. If you do not reply, you risk losing your bond.', +// this.depositHoursMinutes(), +// )} +// </Typography> +// </ListItem> + +// <ListItem> +// <Typography variant='body2' align='left'> +// {t( +// 'If the order expires untaken, your bond will return to you (no action needed).', +// )} +// </Typography> +// </ListItem> + +// <Divider /> + +// <Grid container> +// <Grid item xs={10}> +// <ListItem> +// <ListItemIcon> +// <BookIcon /> +// </ListItemIcon> +// <ListItemText +// primary={this.props.data.num_similar_orders} +// secondary={t('Public orders for {{currencyCode}}', { +// currencyCode: this.props.data.currencyCode, +// })} +// /> +// </ListItem> +// </Grid> + +// <Grid item xs={2}> +// <div style={{ position: 'relative', top: '7px', right: '14px' }}> +// {this.props.pauseLoading ? ( +// <CircularProgress sx={{ width: '30px', height: '30px' }} /> +// ) : ( +// <Tooltip +// placement='top' +// enterTouchDelay={500} +// enterDelay={700} +// enterNextDelay={2000} +// title={t('Pause the public order')} +// > +// <Button color='primary' onClick={this.handleClickPauseOrder}> +// <PauseCircleIcon sx={{ width: '36px', height: '36px' }} /> +// </Button> +// </Tooltip> +// )} +// </div> +// </Grid> +// </Grid> + +// <Divider /> +// <ListItem> +// <ListItemIcon> +// <PercentIcon /> +// </ListItemIcon> +// <ListItemText +// primary={ +// t('Premium rank') + ' ' + parseInt(this.props.data.premium_percentile * 100) + '%' +// } +// secondary={t('Among public {{currencyCode}} orders (higher is cheaper)', { +// currencyCode: this.props.data.currencyCode, +// })} +// /> +// </ListItem> +// <Divider /> +// </List> +// </Grid> +// // <BondStatus status={'locked'} isMaker={order.is_maker}/> +// </Grid> +// ); +// }; + +// showPausedOrder = () => { +// const { t } = this.props; +// return ( +// <Grid container align='center' spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography variant='subtitle1'> +// <b> {t('Your order is paused')} </b> {' ' + this.stepXofY()} +// </Typography> +// </Grid> + +// <Grid item xs={12} align='center'> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2' align='left'> +// {t( +// 'Your public order has been paused. At the moment it cannot be seen or taken by other robots. You can choose to unpause it at any time.', +// )} +// </Typography> +// </ListItem> + +// <Grid item xs={12} align='center'> +// {this.props.pauseLoading ? ( +// <CircularProgress /> +// ) : ( +// <Button color='primary' onClick={this.handleClickPauseOrder}> +// <PlayCircleIcon sx={{ width: '36px', height: '36px' }} /> +// {t('Unpause Order')} +// </Button> +// )} +// </Grid> + +// <Divider /> +// </List> +// </Grid> +// // <BondStatus status={'locked'} isMaker={order.is_maker}/> +// </Grid> +// ); +// }; + +// handleInputInvoiceChanged = (e) => { +// this.setState({ +// invoice: e.target.value, +// badInvoice: false, +// }); +// }; + +// handleClickSubmitInvoiceButton = () => { +// this.setState({ badInvoice: false, loadingSubmitInvoice: true }); + +// apiClient +// .post('/api/order/?order_id=' + this.props.data.id, { +// action: 'update_invoice', +// invoice: this.state.invoice, +// }) +// .then( +// (data) => +// this.setState({ badInvoice: data.bad_invoice, loadingSubmitInvoice: false }) & +// this.props.completeSetState(data), +// ); +// }; + +// handleInputAddressChanged = (e) => { +// this.setState({ +// address: e.target.value, +// badAddress: false, +// }); +// }; + +// handleMiningFeeChanged = (e) => { +// let fee = e.target.value; +// if (fee > 50) { +// fee = 50; +// } + +// this.setState({ +// miningFee: fee, +// }); +// }; + +// handleClickSubmitAddressButton = () => { +// this.setState({ badInvoice: false, loadingSubmitAddress: true }); + +// apiClient +// .post('/api/order/?order_id=' + this.props.data.id, { +// action: 'update_address', +// address: this.state.address, +// mining_fee_rate: Math.max(1, this.state.miningFee), +// }) +// .then( +// (data) => +// this.setState({ badAddress: data.bad_address, loadingSubmitAddress: false }) & +// this.props.completeSetState(data), +// ); +// }; + +// handleInputDisputeChanged = (e) => { +// this.setState({ +// statement: e.target.value, +// badStatement: false, +// }); +// }; + +// handleClickSubmitStatementButton = () => { +// this.setState({ badInvoice: false }); + +// apiClient +// .post('/api/order/?order_id=' + this.props.data.id, { +// action: 'submit_statement', +// statement: this.state.statement, +// }) +// .then( +// (data) => +// this.setState({ badStatement: data.bad_statement }) & this.props.completeSetState(data), +// ); +// }; + +// handleScan = (data) => { +// if (data) { +// this.setState({ +// invoice: data, +// }); +// } +// }; + +// handleError = (err) => { +// console.error(err); +// }; + +// compatibleWalletsButton = () => { +// const { t } = this.props; + +// return ( +// <Button +// color='primary' +// component={Link} +// href={'https://learn.robosats.com/docs/wallets/'} +// target='_blank' +// align='center' +// > +// <AccountBalanceWalletIcon /> +// {t('See Compatible Wallets')} +// <NewTabIcon sx={{ width: 16, height: 16 }} /> +// </Button> +// ); +// }; + +// showInputInvoice() { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// {/* Make sound for Taker found or HTLC received. */} +// {this.props.data.is_maker ? this.Sound('taker-found') : this.Sound('locked-invoice')} +// <Typography color='primary' variant='subtitle1'> +// <b> +// {' '} +// {t('Submit payout info for {{amountSats}} Sats', { +// amountSats: pn(this.props.data.invoice_amount), +// })} +// </b>{' '} +// {' ' + this.stepXofY()} +// </Typography> +// </Grid> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2'> +// {t( +// 'Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC.', +// { +// amountFiat: pn( +// parseFloat( +// parseFloat(this.props.data.amount).toFixed( +// this.props.data.currency == 1000 ? 8 : 4, +// ), +// ), +// ), +// currencyCode: this.props.data.currencyCode, +// }, +// )} +// </Typography> +// </ListItem> +// </List> + +// <Grid item xs={12} align='center'> +// <ToggleButtonGroup size='small' value={this.state.receiveTab} exclusive> +// <ToggleButton +// value={0} +// disableRipple={true} +// onClick={() => this.setState({ receiveTab: 0 })} +// > +// <div +// style={{ +// display: 'flex', +// alignItems: 'center', +// justifyContent: 'center', +// flexWrap: 'wrap', +// }} +// > +// <BoltIcon /> {t('Lightning')} +// </div> +// </ToggleButton> +// <ToggleButton +// value={1} +// disabled={!this.props.data.swap_allowed} +// onClick={() => +// this.setState({ +// receiveTab: 1, +// miningFee: parseFloat(this.props.data.suggested_mining_fee_rate), +// }) +// } +// > +// <div +// style={{ +// display: 'flex', +// alignItems: 'center', +// justifyContent: 'center', +// flexWrap: 'wrap', +// }} +// > +// <LinkIcon /> {t('Onchain')} +// </div> +// </ToggleButton> +// </ToggleButtonGroup> +// </Grid> + +// {/* LIGHTNING PAYOUT TAB */} +// <div style={{ display: this.state.receiveTab == 0 ? '' : 'none' }}> +// <div style={{ height: 15 }} /> +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography variant='body2'> +// {t('Submit a valid invoice for {{amountSats}} Satoshis.', { +// amountSats: pn(this.props.data.invoice_amount), +// })} +// </Typography> +// </Grid> + +// <Grid item xs={12} align='center'> +// {this.compatibleWalletsButton()} +// </Grid> + +// <Grid item xs={12} align='center'> +// <TextField +// error={this.state.badInvoice} +// helperText={this.state.badInvoice ? t(this.state.badInvoice) : ''} +// label={t('Payout Lightning Invoice')} +// required +// value={this.state.invoice} +// inputProps={{ +// style: { textAlign: 'center' }, +// maxHeight: 200, +// }} +// multiline +// minRows={4} +// maxRows={8} +// onChange={this.handleInputInvoiceChanged} +// /> +// </Grid> +// <Grid item xs={12} align='center'> +// <LoadingButton +// loading={this.state.loadingSubmitInvoice} +// onClick={this.handleClickSubmitInvoiceButton} +// variant='contained' +// color='primary' +// > +// {t('Submit')} +// </LoadingButton> +// </Grid> +// </Grid> +// </div> + +// {/* ONCHAIN PAYOUT TAB */} +// <div style={{ display: this.state.receiveTab == 1 ? '' : 'none' }}> +// <List dense={true}> +// <ListItem> +// <Typography variant='body2'> +// <b>{t('EXPERIMENTAL: ')}</b> +// {t('RoboSats will do a swap and send the Sats to your onchain address.')} +// </Typography> +// </ListItem> + +// <Divider /> + +// <ListItem> +// <ListItemText +// primary={ +// pn( +// parseInt( +// (this.props.data.invoice_amount * this.props.data.swap_fee_rate) / 100, +// ), +// ) + +// ' Sats (' + +// this.props.data.swap_fee_rate + +// '%)' +// } +// secondary={t('Swap fee')} +// /> +// </ListItem> + +// <Divider /> + +// <ListItem> +// <ListItemText +// primary={ +// pn(parseInt(Math.max(1, this.state.miningFee) * 141)) + +// ' Sats (' + +// Math.max(1, this.state.miningFee) + +// ' Sats/vByte)' +// } +// secondary={t('Mining fee')} +// /> +// </ListItem> + +// <Divider /> + +// <ListItem> +// <ListItemText +// primary={ +// <b> +// {pn( +// parseInt( +// this.props.data.invoice_amount - +// Math.max(1, this.state.miningFee) * 141 - +// (this.props.data.invoice_amount * this.props.data.swap_fee_rate) / 100, +// ), +// ) + ' Sats'} +// </b> +// } +// secondary={t('Final amount you will receive')} +// /> +// </ListItem> +// </List> +// <TextField +// error={this.state.badAddress} +// helperText={this.state.badAddress ? t(this.state.badAddress) : ''} +// label={t('Bitcoin Address')} +// required +// value={this.state.invoice} +// sx={{ width: 170 }} +// inputProps={{ +// style: { textAlign: 'center' }, +// }} +// onChange={this.handleInputAddressChanged} +// /> +// <TextField +// error={this.state.miningFee < 1 || this.state.miningFee > 50} +// helperText={this.state.miningFee < 1 || this.state.miningFee > 50 ? 'Invalid' : ''} +// label={t('Mining Fee')} +// required +// sx={{ width: 110 }} +// value={this.state.miningFee} +// type='number' +// inputProps={{ +// max: 50, +// min: 1, +// style: { textAlign: 'center' }, +// }} +// onChange={this.handleMiningFeeChanged} +// /> +// <div style={{ height: 10 }} /> + +// <Grid item xs={12} align='center'> +// <LoadingButton +// loading={this.state.loadingSubmitAddress} +// onClick={this.handleClickSubmitAddressButton} +// variant='contained' +// color='primary' +// > +// {t('Submit')} +// </LoadingButton> +// </Grid> +// </div> +// <List> +// <Divider /> +// </List> + +// // <BondStatus status={'locked'} isMaker={order.is_maker}/> +// </Grid> +// ); +// } + +// // Asks the user for a dispute statement. +// showInDisputeStatement = () => { +// const { t } = this.props; +// if (this.props.data.statement_submitted) { +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography color='primary' variant='subtitle1'> +// <b> {t('We have received your statement')} </b> +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2'> +// {t( +// 'We are waiting for your trade counterpart statement. If you are hesitant about the state of the dispute or want to add more information, contact robosats@protonmail.com.', +// )} +// </Typography> +// </ListItem> +// <ListItem> +// <Typography variant='body2'> +// {t( +// 'Please, save the information needed to identify your order and your payments: order ID; payment hashes of the bonds or escrow (check on your lightning wallet); exact amount of satoshis; and robot nickname. You will have to identify yourself as the user involved in this trade via email (or other contact methods).', +// )} +// </Typography> +// </ListItem> +// <Divider /> +// </List> +// </Grid> +// // <BondStatus status={'settled'} isMaker={order.is_maker}/> +// </Grid> +// ); +// } else { +// return ( +// // TODO Option to upload files + +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography color='primary' variant='subtitle1'> +// <b> {t('A dispute has been opened')} </b> +// </Typography> +// </Grid> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2'> +// {t( +// 'Please, submit your statement. Be clear and specific about what happened and provide the necessary evidence. You MUST provide a contact method: burner email, XMPP or telegram username to follow up with the staff. Disputes are solved at the discretion of real robots (aka humans), so be as helpful as possible to ensure a fair outcome. Max 5000 chars.', +// )} +// </Typography> +// </ListItem> + +// <Grid item xs={12} align='center'> +// <TextField +// error={this.state.badStatement} +// helperText={this.state.badStatement ? this.state.badStatement : ''} +// label={t('Submit dispute statement')} +// required +// inputProps={{ +// style: { textAlign: 'center' }, +// }} +// multiline +// rows={4} +// onChange={this.handleInputDisputeChanged} +// /> +// </Grid> +// <Grid item xs={12} align='center'> +// <Button +// onClick={this.handleClickSubmitStatementButton} +// variant='contained' +// color='primary' +// > +// Submit +// </Button> +// </Grid> +// </List> +// // <BondStatus status={'settled'} isMaker={order.is_maker}/> +// </Grid> +// ); +// } +// }; + +// showWaitForDisputeResolution = () => { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography color='primary' variant='subtitle1'> +// <b> {t('We have the statements')} </b> +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2'> +// {t( +// 'Both statements have been received, wait for the staff to resolve the dispute. If you are hesitant about the state of the dispute or want to add more information, contact robosats@protonmail.com. If you did not provide a contact method, or are unsure whether you wrote it right, write us immediately.', +// )} +// </Typography> +// </ListItem> +// <ListItem> +// <Typography variant='body2'> +// {t( +// 'Please, save the information needed to identify your order and your payments: order ID; payment hashes of the bonds or escrow (check on your lightning wallet); exact amount of satoshis; and robot nickname. You will have to identify yourself as the user involved in this trade via email (or other contact methods).', +// )} +// </Typography> +// </ListItem> +// <Divider /> +// </List> +// </Grid> +// // <BondStatus status={'settled'} isMaker={order.is_maker}/> +// </Grid> +// ); +// }; + +// showDisputeWinner = () => { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography color='primary' variant='subtitle1'> +// <b> {t('You have won the dispute')} </b> +// </Typography> +// </Grid> +// <Grid item xs={12} align='left'> +// <Typography variant='body2'> +// {t( +// 'You can claim the dispute resolution amount (escrow and fidelity bond) from your profile rewards. If there is anything the staff can help with, do not hesitate to contact to robosats@protonmail.com (or via your provided burner contact method).', +// )} +// </Typography> +// </Grid> +// // <BondStatus status={'settled'} isMaker={order.is_maker}/> +// </Grid> +// ); +// }; + +// showDisputeLoser = () => { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography color='error' variant='subtitle1'> +// <b> {t('You have lost the dispute')} </b> +// </Typography> +// </Grid> +// <Grid item xs={12} align='left'> +// <Typography variant='body2'> +// {t( +// 'Unfortunately you have lost the dispute. If you think this is a mistake you can ask to re-open the case via email to robosats@protonmail.com. However, chances of it being investigated again are low.', +// )} +// </Typography> +// </Grid> +// // <BondStatus status={'settled'} isMaker={order.is_maker}/> +// </Grid> +// ); +// }; + +// showWaitingForEscrow() { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography variant='subtitle1'> +// <b>{t('Your info looks good!')}</b> {' ' + this.stepXofY()} +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2' align='left'> +// {t('We are waiting for the seller to lock the trade amount.')} +// </Typography> +// </ListItem> +// <ListItem> +// <Typography variant='body2' align='left'> +// {t( +// 'Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).', +// )} +// </Typography> +// </ListItem> +// <Divider /> +// </List> +// </Grid> +// // <BondStatus status={'locked'} isMaker={order.is_maker}/> +// </Grid> +// ); +// } + +// showWaitingForBuyerInvoice() { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// {/* Make confirmation sound for HTLC received. */} +// {this.Sound('locked-invoice')} +// <Grid item xs={12} align='center'> +// <Typography variant='subtitle1'> +// <b>{t('The trade collateral is locked!')}</b> {' ' + this.stepXofY()} +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <List dense={true}> +// <Divider /> +// <ListItem> +// <Typography variant='body2' align='left'> +// {t( +// 'We are waiting for the buyer to post a lightning invoice. Once he does, you will be able to directly communicate the fiat payment details.', +// )} +// </Typography> +// </ListItem> + +// <ListItem> +// <Typography variant='body2' align='left'> +// {t( +// 'Just hang on for a moment. If the buyer does not cooperate, you will get back the trade collateral and your bond automatically. In addition, you will receive a compensation (check the rewards in your profile).', +// )} +// </Typography> +// </ListItem> +// <Divider /> +// </List> +// </Grid> +// // <BondStatus status={'locked'} isMaker={order.is_maker}/> +// </Grid> +// ); +// } + +// handleClickConfirmButton = () => { +// apiClient +// .post('/api/order/?order_id=' + this.props.data.id, { +// action: 'confirm', +// }) +// .then((data) => { +// this.props.completeSetState(data), +// this.setState({ loadingButtonFiatSent: false, loadingButtonFiatReceived: false }); +// }); +// }; + +// handleRatingUserChange = (e) => { +// apiClient +// .post('/api/order/?order_id=' + this.props.data.id, { +// action: 'rate_user', +// rating: e.target.value, +// }) +// .then((data) => this.props.completeSetState(data)); +// }; + +// handleRatingRobosatsChange = (e) => { +// if (this.state.rating_platform != null) { +// return null; +// } +// this.setState({ rating_platform: e.target.value }); + +// apiClient +// .post('/api/order/?order_id=' + this.props.data.id, { +// action: 'rate_platform', +// rating: e.target.value, +// }) +// .then((data) => this.props.completeSetState(data)); +// }; + +// showFiatSentButton() { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Button +// loading={this.state.loadingButtonFiatSent} +// defaultValue='confirm' +// variant='contained' +// color='secondary' +// onClick={() => { +// this.setState({ loadingButtonFiatSent: true }); +// this.handleClickConfirmButton(); +// }} +// > +// {t('Confirm {{amount}} {{currencyCode}} sent', { +// currencyCode: this.props.data.currencyCode, +// amount: pn( +// parseFloat( +// parseFloat(this.props.data.amount).toFixed( +// this.props.data.currency == 1000 ? 8 : 4, +// ), +// ), +// ), +// })} +// </Button> +// </Grid> +// </Grid> +// ); +// } + +// showFiatReceivedButton() { +// const { t } = this.props; +// return ( +// <Grid item xs={12} align='center'> +// <LoadingButton +// loading={this.state.loadingButtonFiatReceived} +// defaultValue='confirm' +// variant='contained' +// color='secondary' +// onClick={this.handleClickOpenConfirmFiatReceived} +// > +// {t('Confirm {{amount}} {{currencyCode}} received', { +// currencyCode: this.props.data.currencyCode, +// amount: pn( +// parseFloat( +// parseFloat(this.props.data.amount).toFixed( +// this.props.data.currency == 1000 ? 8 : 4, +// ), +// ), +// ), +// })} +// </LoadingButton> +// </Grid> +// ); +// } + +// disputeCountdownRenderer = ({ hours, minutes }) => { +// return ( +// <span> +// {hours}h {zeroPad(minutes)}m{' '} +// </span> +// ); +// }; + +// showOpenDisputeButton() { +// const { t } = this.props; +// const now = Date.now(); +// const expires_at = new Date(this.props.data.expires_at); +// // open dispute button enables 12h before expiry +// expires_at.setHours(expires_at.getHours() - 12); +// return ( +// <Tooltip +// placement='top' +// componentsProps={{ +// tooltip: { sx: { position: 'relative', top: 42 } }, +// }} +// disableHoverListener={now > expires_at} +// disableTouchListener={now > expires_at} +// enterTouchDelay={0} +// title={ +// <Trans i18nKey='open_dispute'> +// To open a dispute you need to wait +// <Countdown date={expires_at} renderer={this.disputeCountdownRenderer} /> +// </Trans> +// } +// > +// <Grid item xs={12} align='center'> +// <Button +// disabled={now < expires_at} +// color='inherit' +// onClick={this.handleClickOpenConfirmDispute} +// > +// {t('Open Dispute')} +// </Button> +// </Grid> +// </Tooltip> +// ); +// } + +// handleRenewOrderButtonPressed = () => { +// this.setState({ renewLoading: true }); +// const body = { +// type: this.props.data.type, +// currency: this.props.data.currency, +// amount: this.props.data.has_range ? null : this.props.data.amount, +// has_range: this.props.data.has_range, +// min_amount: this.props.data.min_amount, +// max_amount: this.props.data.max_amount, +// payment_method: this.props.data.payment_method, +// is_explicit: this.props.data.is_explicit, +// premium: this.props.data.is_explicit ? null : this.props.data.premium, +// satoshis: this.props.data.is_explicit ? this.props.data.satoshis : null, +// public_duration: this.props.data.public_duration, +// escrow_duration: this.props.data.escrow_duration, +// bond_size: this.props.data.bond_size, +// bondless_taker: this.props.data.bondless_taker, +// }; +// apiClient +// .post('/api/make/', body) +// .then( +// (data) => +// this.setState({ badRequest: data.bad_request }) & +// (data.id +// ? this.props.push('/order/' + data.id) & this.props.getOrderDetails(data.id) +// : ''), +// ); +// }; + +// showOrderExpired = () => { +// const { t } = this.props; +// const show_renew = this.props.data.is_maker; + +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography variant='subtitle1'> +// <b>{t('The order has expired')}</b> +// </Typography> +// </Grid> + +// <Grid item xs={12} align='center'> +// <Typography variant='body2'>{t(this.props.data.expiry_message)}</Typography> +// </Grid> +// {show_renew ? ( +// <Grid item xs={12} align='center'> +// {this.state.renewLoading ? ( +// <CircularProgress /> +// ) : ( +// <Button +// variant='contained' +// color='primary' +// onClick={this.handleRenewOrderButtonPressed} +// > +// {t('Renew Order')} +// </Button> +// )} +// </Grid> +// ) : null} +// </Grid> +// ); +// }; + +// showChat = () => { +// const { t } = this.props; +// // In Chatroom - No fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) +// if (this.props.data.is_buyer & (this.props.data.status == 9)) { +// var showSendButton = true; +// var showReveiceButton = false; +// var showDisputeButton = true; +// } +// if (this.props.data.is_seller & (this.props.data.status == 9)) { +// var showSendButton = false; +// var showReveiceButton = false; +// var showDisputeButton = true; +// } + +// // In Chatroom - Fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) +// if (this.props.data.is_buyer & (this.props.data.status == 10)) { +// var showSendButton = false; +// var showReveiceButton = false; +// var showDisputeButton = true; +// } +// if (this.props.data.is_seller & (this.props.data.status == 10)) { +// var showSendButton = false; +// var showReveiceButton = true; +// var showDisputeButton = true; +// } + +// return ( +// <Grid container spacing={1}> +// {/* Make confirmation sound for Chat Open. */} +// {this.Sound('locked-invoice')} +// <Grid item xs={12} align='center'> +// <Typography variant='subtitle1'> +// <b> +// {' '} +// {this.props.data.is_seller ? t('Chat with the buyer') : t('Chat with the seller')} +// </b>{' '} +// {' ' + this.stepXofY()} +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// {this.props.data.is_seller ? ( +// <Typography variant='body2' align='center'> +// {this.props.data.status == 9 +// ? t( +// 'Say hi! Be helpful and concise. Let them know how to send you {{amount}} {{currencyCode}}.', +// { +// currencyCode: this.props.data.currencyCode, +// amount: pn( +// parseFloat( +// parseFloat(this.props.data.amount).toFixed( +// this.props.data.currency == 1000 ? 8 : 4, +// ), +// ), +// ), +// }, +// ) +// : t("The buyer has sent the fiat. Click 'Confirm Received' once you receive it.")} +// </Typography> +// ) : ( +// <Typography variant='body2' align='center'> +// {this.props.data.status == 9 +// ? t( +// "Say hi! Ask for payment details and click 'Confirm Sent' as soon as the payment is sent.", +// ) +// : t('Wait for the seller to confirm he has received the payment.')} +// </Typography> +// )} +// </Grid> + +// <EncryptedChat orderId={this.props.data.id} userNick={this.props.data.ur_nick} /> +// <Grid item xs={12} align='center'> +// {showDisputeButton ? this.showOpenDisputeButton() : ''} +// {showSendButton ? this.showFiatSentButton() : ''} +// {showReveiceButton ? this.showFiatReceivedButton() : ''} +// </Grid> +// // <BondStatus status={'locked'} isMaker={order.is_maker}/> +// </Grid> +// ); +// }; + +// showRateSelect() { +// const { t } = this.props; +// const show_renew = this.props.data.is_maker; + +// return ( +// <Grid container spacing={1}> +// {/* Make confirmation sound for Chat Open. */} +// {this.Sound('successful')} +// <Grid item xs={12} align='center'> +// <Typography component='h6' variant='h6'> +// <div +// style={{ +// display: 'flex', +// alignItems: 'center', +// flexWrap: 'wrap', +// justifyContent: 'center', +// }} +// > +// <BoltIcon sx={{ width: 25, height: 37 }} color='warning' /> +// {t('Trade finished!')} +// <BoltIcon sx={{ width: 25, height: 37 }} color='warning' /> +// </div> +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <Typography variant='body2' align='center'> +// <Trans i18nKey='rate_robosats'> +// What do you think of <b>RoboSats</b>? +// </Trans> +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <Rating +// name='size-large' +// defaultValue={0} +// size='large' +// onChange={this.handleRatingRobosatsChange} +// /> +// </Grid> +// {this.state.rating_platform == 5 ? ( +// <Grid item xs={12} align='center'> +// <div +// style={{ +// display: 'flex', +// alignItems: 'center', +// flexWrap: 'wrap', +// justifyContent: 'center', +// }} +// > +// <Typography variant='body2' align='center'> +// <b>{t('Thank you! RoboSats loves you too')}</b>{' '} +// </Typography> +// <FavoriteIcon color='error' /> +// </div> +// <Typography variant='body2' align='center'> +// {t( +// 'RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!', +// )} +// </Typography> +// </Grid> +// ) : null} +// {(this.state.rating_platform != 5) & (this.state.rating_platform != null) ? ( +// <Grid item xs={12} align='center'> +// <Typography variant='body2' align='center'> +// <b>{t('Thank you for using Robosats!')}</b> +// </Typography> +// <Typography variant='body2' align='center'> +// <Trans i18nKey='let_us_know_hot_to_improve'> +// Let us know how the platform could improve ( +// <Link target='_blank' href='https://t.me/robosats'> +// Telegram +// </Link>{' '} +// /{' '} +// <Link target='_blank' href='https://github.com/Reckless-Satoshi/robosats/issues'> +// Github +// </Link> +// ) +// </Trans> +// </Typography> +// </Grid> +// ) : null} + +// {/* SHOW TXID IF USER RECEIVES ONCHAIN */} +// {this.props.data.txid ? ( +// <Grid item xs={12} align='left'> +// <Alert severity='success'> +// <AlertTitle> +// {t('Your TXID')} +// <Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}> +// <IconButton +// color='inherit' +// onClick={() => { +// systemClient.copyToClipboard(this.props.data.txid); +// }} +// > +// <ContentCopy sx={{ width: 16, height: 16 }} /> +// </IconButton> +// </Tooltip> +// </AlertTitle> +// <Typography +// variant='body2' +// align='center' +// sx={{ wordWrap: 'break-word', width: 220 }} +// > +// <Link +// target='_blank' +// href={ +// 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/' + +// (this.props.data.network == 'testnet' ? 'testnet/' : '') + +// 'tx/' + +// this.props.data.txid +// } +// > +// {this.props.data.txid} +// </Link> +// </Typography> +// </Alert> +// </Grid> +// ) : null} + +// <Grid item container spacing={3}> +// <Grid item xs={show_renew ? 6 : 12} align='center'> +// <Button +// color='primary' +// variant='outlined' +// onClick={() => { +// this.props.push('/'); +// }} +// > +// <RocketLaunchIcon /> +// {t('Start Again')} +// </Button> +// </Grid> + +// {show_renew ? ( +// <Grid item xs={6} align='center'> +// {this.state.renewLoading ? ( +// <CircularProgress /> +// ) : ( +// <Button +// color='primary' +// variant='outlined' +// onClick={this.handleRenewOrderButtonPressed} +// > +// <RefreshIcon /> +// {t('Renew Order')} +// </Button> +// )} +// </Grid> +// ) : null} +// </Grid> + +// <TradeSummary +// isMaker={this.props.data.is_maker} +// makerNick={this.props.data.maker_nick} +// takerNick={this.props.data.taker_nick} +// currencyCode={this.props.data.currencyCode} +// makerSummary={this.props.data.maker_summary} +// takerSummary={this.props.data.taker_summary} +// platformSummary={this.props.data.platform_summary} +// orderId={this.props.data.orderId} +// /> +// </Grid> +// ); +// } + +// showSendingPayment() { +// const { t } = this.props; +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography component='h6' variant='h6'> +// {t('Attempting Lightning Payment')} +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <Typography variant='body2' align='center'> +// {t( +// 'RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must be online in order to receive payments.', +// )} +// </Typography> +// <br /> +// <Grid item xs={12} align='center'> +// <CircularProgress /> +// </Grid> +// </Grid> +// </Grid> +// ); +// } + +// // Countdown Renderer callback with condition +// countdownRenderer = ({ minutes, seconds, completed }) => { +// const { t } = this.props; +// if (completed) { +// // Render a completed state +// return ( +// <div align='center'> +// <span> {t('Retrying!')} </span> +// <br /> +// <CircularProgress /> +// </div> +// ); +// } else { +// return ( +// <span> +// {zeroPad(minutes)}m {zeroPad(seconds)}s{' '} +// </span> +// ); +// } +// }; + +// failureReason = () => { +// const { t } = this.props; +// return ( +// <Grid item xs={12} align='center'> +// <Typography variant='body2' align='center'> +// <b>{t('Failure reason:')}</b> +// </Typography> +// <Typography variant='body2' align='center'> +// {t(this.props.data.failure_reason)} +// </Typography> +// </Grid> +// ); +// }; + +// showRoutingFailed = () => { +// const { t } = this.props; +// if (this.props.data.invoice_expired) { +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography component='h6' variant='h6'> +// {t('Lightning Routing Failed')} +// </Typography> +// </Grid> + +// {this.props.data.failure_reason ? this.failureReason() : null} + +// <Grid item xs={12} align='center'> +// <Typography variant='body2' align='center'> +// {t( +// 'Your invoice has expired or more than 3 payment attempts have been made. Submit a new invoice.', +// )} +// </Typography> +// </Grid> + +// <Grid item xs={12} align='center'> +// {this.compatibleWalletsButton()} +// </Grid> + +// <Grid item xs={12} align='center'> +// <Typography color='primary' variant='subtitle1'> +// <b> +// {' '} +// {t('Submit an invoice for {{amountSats}} Sats', { +// amountSats: pn(this.props.data.invoice_amount), +// })} +// </b> +// </Typography> +// </Grid> +// <Grid item xs={12} align='center'> +// <TextField +// error={this.state.badInvoice} +// helperText={this.state.badInvoice ? t(this.state.badInvoice) : ''} +// label={t('Payout Lightning Invoice')} +// required +// inputProps={{ +// style: { textAlign: 'center' }, +// }} +// multiline +// minRows={4} +// maxRows={8} +// onChange={this.handleInputInvoiceChanged} +// /> +// </Grid> +// <Grid item xs={12} align='center'> +// <LoadingButton +// loading={this.state.loadingSubmitInvoice} +// onClick={this.handleClickSubmitInvoiceButton} +// variant='contained' +// color='primary' +// > +// {t('Submit')} +// </LoadingButton> +// </Grid> +// // <BondStatus status={'returned'} isMaker={order.is_maker}/> +// </Grid> +// ); +// } else { +// return ( +// <Grid container spacing={1}> +// <Grid item xs={12} align='center'> +// <Typography component='h6' variant='h6'> +// {t('Lightning Routing Failed')} +// </Typography> +// </Grid> + +// {this.props.data.failure_reason ? this.failureReason() : null} + +// <Grid item xs={12} align='center'> +// <Typography variant='body2' align='center'> +// {t( +// 'RoboSats will try to pay your invoice 3 times with a one minute pause in between. If it keeps failing, you will be able to submit a new invoice. Check whether you have enough inbound liquidity. Remember that lightning nodes must be online in order to receive payments.', +// )} +// </Typography> +// <List> +// <Divider /> +// <ListItemText secondary={t('Next attempt in')}> +// <Countdown +// date={new Date(this.props.data.next_retry_time)} +// renderer={this.countdownRenderer} +// /> +// </ListItemText> +// </List> +// </Grid> +// // <BondStatus status={'returned'} isMaker={order.is_maker}/> +// </Grid> +// ); +// } +// }; + +// render() { +// const { t } = this.props; +// return ( +// <Grid container spacing={1} style={{ width: this.props.width }}> +// {this.ConfirmDisputeDialog()} +// {this.ConfirmFiatReceivedDialog()} +// <Grid item xs={12} align='center'> +// <MediaQuery minWidth={920}> +// <Typography component='h5' variant='h5'> +// {t('Contract Box')} +// </Typography> +// </MediaQuery> +// <Paper elevation={12} style={{ padding: 8 }}> +// {/* Maker and taker Bond request */} +// {this.props.data.is_maker & (this.props.data.status == 0) ? this.showQRInvoice() : ''} +// {this.props.data.is_taker & (this.props.data.status == 3) ? this.showQRInvoice() : ''} + +// {/* Waiting for taker and taker bond request */} +// {this.props.data.is_maker & (this.props.data.status == 2) ? this.showPausedOrder() : ''} +// {this.props.data.is_maker & (this.props.data.status == 1) ? this.showMakerWait() : ''} +// {this.props.data.is_maker & (this.props.data.status == 3) ? this.showTakerFound() : ''} + +// {/* Send Invoice (buyer) and deposit collateral (seller) */} +// {this.props.data.is_seller & +// (this.props.data.status == 6 || this.props.data.status == 7) +// ? this.showEscrowQRInvoice() +// : ''} +// {this.props.data.is_buyer & (this.props.data.status == 6 || this.props.data.status == 8) +// ? this.showInputInvoice() +// : ''} +// {this.props.data.is_buyer & (this.props.data.status == 7) +// ? this.showWaitingForEscrow() +// : ''} +// {this.props.data.is_seller & (this.props.data.status == 8) +// ? this.showWaitingForBuyerInvoice() +// : ''} + +// {/* In Chatroom */} +// {this.props.data.status == 9 || this.props.data.status == 10 ? this.showChat() : ''} + +// {/* Trade Finished */} +// {this.props.data.is_seller & [13, 14, 15].includes(this.props.data.status) +// ? this.showRateSelect() +// : ''} +// {this.props.data.is_buyer & (this.props.data.status == 14) ? this.showRateSelect() : ''} + +// {/* Trade Finished - Payment Routing Failed */} +// {this.props.data.is_buyer & (this.props.data.status == 13) +// ? this.showSendingPayment() +// : ''} + +// {/* Trade Finished - Payment Routing Failed */} +// {this.props.data.is_buyer & (this.props.data.status == 15) +// ? this.showRoutingFailed() +// : ''} + +// {/* Trade Finished - TODO Needs more planning */} +// {this.props.data.status == 11 ? this.showInDisputeStatement() : ''} +// {this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ''} +// {(this.props.data.status == 17) & this.props.data.is_taker || +// (this.props.data.status == 18) & this.props.data.is_maker +// ? this.showDisputeWinner() +// : ''} +// {(this.props.data.status == 18) & this.props.data.is_taker || +// (this.props.data.status == 17) & this.props.data.is_maker +// ? this.showDisputeLoser() +// : ''} + +// {/* Order has expired */} +// {this.props.data.status == 5 ? this.showOrderExpired() : ''} +// {/* TODO */} +// {/* */} +// {/* */} +// </Paper> +// </Grid> +// </Grid> +// ); +// } +// } + +// export default withTranslation()(TradeBox); diff --git a/frontend/src/components/UnsafeAlert.js b/frontend/src/components/UnsafeAlert.js index fc582945..5a481cf1 100644 --- a/frontend/src/components/UnsafeAlert.js +++ b/frontend/src/components/UnsafeAlert.js @@ -9,21 +9,10 @@ class UnsafeAlert extends Component { super(props); this.state = { show: true, - isSelfhosted: this.isSelfhosted(), }; } - isSelfhosted() { - const http = new XMLHttpRequest(); - try { - http.open('HEAD', `${location.protocol}//${getHost()}/selfhosted`, false); - http.send(); - return http.status === 200; - } catch { - return false; - } - } - + // To do. Read from Coordinators state Obj. safe_urls = [ 'robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion', 'robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion', @@ -32,6 +21,26 @@ class UnsafeAlert extends Component { 'r7r4sckft6ptmk4r2jajiuqbowqyxiwsle4iyg4fijtoordc6z7a.b32.i2p', ]; + checkClient() { + const http = new XMLHttpRequest(); + const unsafeClient = !this.safe_urls.includes(getHost()); + try { + http.open('HEAD', `${location.protocol}//${getHost()}/selfhosted`, false); + http.send(); + this.props.setSettings({ + ...this.props.settings, + unsafeClient, + selfhostedClient: http.status === 200, + }); + } catch { + this.props.setSettings({ ...this.props.settings, unsafeClient, selfhostedClient: false }); + } + } + + componentDidMount() { + this.checkClient(); + } + render() { const { t } = this.props; @@ -41,7 +50,7 @@ class UnsafeAlert extends Component { } // Show selfhosted notice - if (this.state.isSelfhosted) { + else if (this.props.settings.selfhostedClient) { return ( <div> <Paper elevation={6} className='alertUnsafe'> @@ -65,7 +74,7 @@ class UnsafeAlert extends Component { } // Show unsafe alert - if (!window.NativeRobosats && !this.safe_urls.includes(getHost())) { + else if (this.props.settings.unsafeClient) { return ( <div> <MediaQuery minWidth={800}> diff --git a/frontend/src/models/Book.model.ts b/frontend/src/models/Book.model.ts index 5c05c30d..ca781870 100644 --- a/frontend/src/models/Book.model.ts +++ b/frontend/src/models/Book.model.ts @@ -1,4 +1,4 @@ -export interface Order { +export interface PublicOrder { id: number; created_at: Date; expires_at: Date; @@ -24,8 +24,8 @@ export interface Order { } export interface Book { - orders: Order[]; + orders: PublicOrder[]; loading: boolean; } -export default Order; +export default PublicOrder; diff --git a/frontend/src/models/Chat.model.ts b/frontend/src/models/Chat.model.ts new file mode 100644 index 00000000..9d5f9ddb --- /dev/null +++ b/frontend/src/models/Chat.model.ts @@ -0,0 +1,23 @@ +export interface WebSocketsChatMessage { + userNick: string; + validSignature: boolean; + plainTextMessage: string; + encryptedMessage: string; + time: Date; + index: number; +} + +export interface APIChatMessage { + nick: string; + time: Date; + message: string; + index: number; +} + +export interface APIChat { + peer_pubkey: string; + peer_connected: boolean; + messages: APIChatMessage[]; +} + +export default APIChat; diff --git a/frontend/src/models/Order.model.ts b/frontend/src/models/Order.model.ts new file mode 100644 index 00000000..4819091c --- /dev/null +++ b/frontend/src/models/Order.model.ts @@ -0,0 +1,100 @@ +import { APIChat } from '.'; + +export interface TradeRobotSummary { + sent_fiat: number; + received_sats: number; + is_swap: boolean; + received_onchain_sats: number; + mining_fee_sats: number; + swap_fee_sats: number; + swap_fee_percent: number; + sent_sats: number; + received_fiat: number; + trade_fee_sats: number; +} + +export interface TradeCoordinatorSummary { + contract_timestamp: Date; + contract_total_time: number; + routing_fee_sats: number; + trade_revenue_sats: number; +} + +export interface Order { + id: number; + status: number; + created_at: Date; + expires_at: Date; + type: number; + currency: number; + amount: string; + has_range: boolean; + min_amount: string; + max_amount: string; + payment_method: string; + is_explicit: true; + premium: number; + satoshis: number; + bondless_taker: true; + maker: number; + taker: number; + escrow_duration: number; + total_secs_exp: number; + penalty: Date; + is_maker: boolean; + is_taker: boolean; + is_participant: boolean; + maker_status: 'Active' | 'Seen recently' | 'Inactive'; + taker_status: 'Active' | 'Seen recently' | 'Inactive'; + price_now: number; + premium_percentile: number; + num_similar_orders: number; + tg_enabled: boolean; // deprecated + tg_token: string; + tg_bot_name: string; + is_buyer: boolean; + is_seller: boolean; + maker_nick: string; + taker_nick: string; + status_message: string; + is_fiat_sent: boolean; + is_disputed: boolean; + ur_nick: string; + maker_locked: boolean; + taker_locked: boolean; + escrow_locked: boolean; + trade_satoshis: number; + bond_invoice: string; + bond_satoshis: number; + escrow_invoice: string; + escrow_satoshis: number; + invoice_amount: number; + swap_allowed: boolean; + swap_failure_reason: string; + suggested_mining_fee_rate: number; + swap_fee_rate: number; + pending_cancel: boolean; + asked_for_cancel: boolean; + statement_submitted: boolean; + retries: number; + next_retry_time: Date; + failure_reason: string; + invoice_expired: boolean; + public_duration: number; + bond_size: string; + trade_fee_percent: number; + bond_size_sats: number; + bond_size_percent: number; + chat: APIChat; + maker_summary: TradeRobotSummary; + taker_summary: TradeRobotSummary; + platform_summary: TradeCoordinatorSummary; + expiry_reason: number; + expiry_message: string; + num_satoshis: number; + sent_satoshis: number; + txid: string; + network: 'mainnet' | 'testnet'; +} + +export default Order; diff --git a/frontend/src/models/Settings.default.basic.ts b/frontend/src/models/Settings.default.basic.ts index e640115c..005df634 100644 --- a/frontend/src/models/Settings.default.basic.ts +++ b/frontend/src/models/Settings.default.basic.ts @@ -1,8 +1,13 @@ -import { baseSettings, Settings } from './Settings.model'; +import { systemClient } from '../services/System'; +import BaseSettings from './Settings.model'; -export const defaultSettings: Settings = { - ...baseSettings, - frontend: 'basic', -}; +class Settings extends BaseSettings { + constructor() { + super(); + const fontSizeCookie = systemClient.getCookie('settings_fontsize_basic'); + this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 14; + } + public frontend: 'basic' | 'pro' = 'basic'; +} -export default defaultSettings; +export default Settings; diff --git a/frontend/src/models/Settings.default.pro.ts b/frontend/src/models/Settings.default.pro.ts index d7c9829b..835082fb 100644 --- a/frontend/src/models/Settings.default.pro.ts +++ b/frontend/src/models/Settings.default.pro.ts @@ -1,9 +1,13 @@ -import { baseSettings, Settings } from './Settings.model'; +import { systemClient } from '../services/System'; +import BaseSettings from './Settings.model'; -export const defaultSettings: Settings = { - ...baseSettings, - fontSize: 12, - frontend: 'pro', -}; +class Settings extends BaseSettings { + constructor() { + super(); + const fontSizeCookie = systemClient.getCookie('settings_fontsize_pro'); + this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 12; + } + public frontend: 'basic' | 'pro' = 'pro'; +} -export default defaultSettings; +export default Settings; diff --git a/frontend/src/models/Settings.model.ts b/frontend/src/models/Settings.model.ts index 147980b0..2b8ed817 100644 --- a/frontend/src/models/Settings.model.ts +++ b/frontend/src/models/Settings.model.ts @@ -1,4 +1,5 @@ import i18n from '../i18n/Web'; +import { systemClient } from '../services/System'; import type Coordinator from './Coordinator.model'; export type Language = @@ -19,28 +20,34 @@ export type Language = | 'zh-SI' | 'zh-TR'; -export interface Settings { - frontend: 'basic' | 'pro'; - mode: 'light' | 'dark'; - fontSize: number; - language: Language; - freezeViewports: boolean; - network: 'mainnet' | 'testnet' | undefined; - coordinator: Coordinator | undefined; +class BaseSettings { + constructor() { + const modeCookie: 'light' | 'dark' | '' = systemClient.getCookie('settings_mode'); + this.mode = + modeCookie !== '' + ? modeCookie + : window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + + const languageCookie = systemClient.getCookie('settings_language'); + this.language = + languageCookie !== '' + ? languageCookie + : i18n.resolvedLanguage == null + ? 'en' + : i18n.resolvedLanguage.substring(0, 2); + } + + public frontend: 'basic' | 'pro' = 'basic'; + public mode: 'light' | 'dark' = 'light'; + public fontSize: number = 14; + public language?: Language; + public freezeViewports: boolean = false; + public network: 'mainnet' | 'testnet' | undefined = 'mainnet'; + public coordinator: Coordinator | undefined = undefined; + public unsafeClient: boolean = false; + public hostedClient: boolean = false; } -export const baseSettings: Settings = { - frontend: 'basic', - mode: - window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light', - fontSize: 14, - language: - i18n.resolvedLanguage == null ? 'en' : (i18n.resolvedLanguage.substring(0, 2) as Language), - freezeViewports: false, - network: undefined, - coordinator: undefined, -}; - -export default Settings; +export default BaseSettings; diff --git a/frontend/src/models/index.ts b/frontend/src/models/index.ts index 0266021d..c41dfae2 100644 --- a/frontend/src/models/index.ts +++ b/frontend/src/models/index.ts @@ -1,17 +1,18 @@ import Robot from './Robot.model'; +import Settings from './Settings.default.basic'; +export { Robot, Settings }; export type { LimitList } from './Limit.model'; export type { Limit } from './Limit.model'; -export type { Order } from './Book.model'; +export type { Maker } from './Maker.model'; +export type { Order } from './Order.model'; +export type { PublicOrder } from './Book.model'; export type { Book } from './Book.model'; export type { Info } from './Info.model'; -export type { Settings } from './Settings.model'; export type { Language } from './Settings.model'; export type { Favorites } from './Favorites.model'; export type { Coordinator } from './Coordinator.model'; -export type { Maker } from './Maker.model'; -export { Robot }; +export type { APIChat, WebSocketsChatMessage, APIChatMessage } from './Chat.model'; export { defaultMaker } from './Maker.model'; -export { defaultSettings } from './Settings.default.basic'; export { defaultInfo } from './Info.model'; diff --git a/frontend/src/services/System/SystemNativeClient/index.ts b/frontend/src/services/System/SystemNativeClient/index.ts index 15a149a7..c27770dc 100644 --- a/frontend/src/services/System/SystemNativeClient/index.ts +++ b/frontend/src/services/System/SystemNativeClient/index.ts @@ -22,8 +22,9 @@ class SystemNativeClient implements SystemClient { }); }; - public getCookie: (key: string) => string | undefined = (key) => { - return window.NativeRobosats?.cookies[key]; + public getCookie: (key: string) => string = (key) => { + const cookie = window.NativeRobosats?.cookies[key]; + return cookie === null || cookie === undefined ? '' : cookie; }; public setCookie: (key: string, value: string) => void = (key, value) => { diff --git a/frontend/src/services/System/SystemWebClient/index.ts b/frontend/src/services/System/SystemWebClient/index.ts index 2720f724..3aab1a3c 100644 --- a/frontend/src/services/System/SystemWebClient/index.ts +++ b/frontend/src/services/System/SystemWebClient/index.ts @@ -27,7 +27,7 @@ class SystemWebClient implements SystemClient { } }; - public getCookie: (key: string) => string | undefined = (key) => { + public getCookie: (key: string) => string = (key) => { let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); diff --git a/mobile/App.tsx b/mobile/App.tsx index a6858371..fb8016c5 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -47,6 +47,9 @@ const App = () => { EncryptedStorage.removeItem('csrftoken'); loadCookie('robot_token'); loadCookie('pub_key'); + loadCookie('settings_fontsize_basic'); + loadCookie('settings_language'); + loadCookie('settings_mode'); loadCookie('enc_priv_key').then(() => injectMessageResolve(reponseId)); };