Refactor contexts and models (#921)

* Add SVG icons for map pins

* Add federation basis and new coordinator form (#793)

* Add new coordinator entry issue form

* Add Federation basis

* Fix eslint errors from F2F and fix languages

* Redo eslint @typescript-eslint/strict-boolean-expressions

* Robot Page working

* Contexts Working

* Garage Working

* CurrentOrder working

* Federation model working

---------

Co-authored-by: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Co-authored-by: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com>
This commit is contained in:
KoalaSat 2023-10-27 10:01:59 +00:00 committed by Reckless_Satoshi
parent f6b0d34264
commit 293c0b604d
195 changed files with 9492 additions and 5059 deletions

View File

@ -8,7 +8,7 @@ on:
- main - main
paths: paths:
- frontend - frontend
pull_request: pull_request_target:
branches: branches:
- main - main
paths: paths:
@ -41,7 +41,5 @@ jobs:
with: with:
prettier: true prettier: true
prettier_dir: frontend prettier_dir: frontend
eslint: true
## Disabled due to error eslint_dir: frontend
# eslint: true
# eslint_dir: frontend

View File

@ -61,6 +61,7 @@ services:
volumes: volumes:
- ./nodeapp/:/usr/src/robosats/ - ./nodeapp/:/usr/src/robosats/
- ./nodeapp/nginx.conf:/etc/nginx/nginx.conf - ./nodeapp/nginx.conf:/etc/nginx/nginx.conf
- ./nodeapp/coordinators/:/etc/nginx/conf.d/
- ./frontend/static:/usr/src/robosats/static - ./frontend/static:/usr/src/robosats/static
clean-orders: clean-orders:

View File

@ -92,9 +92,15 @@ Each RoboSats coordinator has a profile in the RoboSats app. The profile contain
- Large Limits - Can host orders with large limits. - Large Limits - Can host orders with large limits.
- DevFund donator - Donates to the DevFund the default amount or more. - DevFund donator - Donates to the DevFund the default amount or more.
<<<<<<< HEAD
Some of these badges can be objectively measured and awarded. Other badges rely on the subjectivity of the development team. These will be generously awarded and only taken away after a warning. Some of these badges can be objectively measured and awarded. Other badges rely on the subjectivity of the development team. These will be generously awarded and only taken away after a warning.
We also envision more badges in the future, for example milestones by number of trades coordinated (200, 1K, 5K, 25K, 100K, etc). We also envision more badges in the future, for example milestones by number of trades coordinated (200, 1K, 5K, 25K, 100K, etc).
=======
Some of these badges can be objectively measured and therefore awarded. Others badges rely on the subjectivity by the development team. These will be generously awarded and only taken away after a warning.
We also envision more badges in the future. For example milestones of number of trades coordinated (200, 1K, 5K, 25K, 100K, etc).
>>>>>>> 2254fa60 (Add federation basis and new coordinator form (#793))
## Timeline ## Timeline

View File

@ -19,12 +19,21 @@
"sourceType": "module", "sourceType": "module",
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"ignorePatterns": ["index.js", "**/PaymentMethods/Icons/code/code.js"],
"plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"], "plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
"rules": { "rules": {
"react-hooks/rules-of-hooks": "error", "react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn", "react-hooks/exhaustive-deps": "off",
"react/prop-types": "off", "react/prop-types": "off",
"react/react-in-jsx-scope": "off" "react/react-in-jsx-scope": "off",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variableLike",
"format": ["camelCase", "snake_case", "PascalCase", "UPPER_CASE"],
"leadingUnderscore": "allow"
}
]
}, },
"settings": { "settings": {
"import/resolver": { "import/resolver": {

View File

@ -1,4 +1,4 @@
import React, { StrictMode, Suspense } from 'react'; import React, { StrictMode, Suspense, useState } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import Main from './basic/Main'; import Main from './basic/Main';
import { CssBaseline, ThemeProvider } from '@mui/material'; import { CssBaseline, ThemeProvider } from '@mui/material';
@ -11,20 +11,29 @@ import i18n from './i18n/Web';
import { systemClient } from './services/System'; import { systemClient } from './services/System';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import { GarageContext, useGarageStore } from './contexts/GarageContext';
import { FederationContext, useFederationStore } from './contexts/FederationContext';
const App = (): JSX.Element => { const App = (): JSX.Element => {
const store = useAppStore(); const appStore = useAppStore();
const garageStore = useGarageStore();
const federationStore = useFederationStore();
return ( return (
<StrictMode> <StrictMode>
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback='loading'> <Suspense fallback='loading'>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<AppContext.Provider value={store}> <AppContext.Provider value={appStore}>
<ThemeProvider theme={store.theme}> <GarageContext.Provider value={garageStore}>
<CssBaseline /> <FederationContext.Provider value={federationStore}>
{window.NativeRobosats === undefined ? <HostAlert /> : <TorConnectionBadge />} <ThemeProvider theme={appStore.theme}>
<Main /> <CssBaseline />
</ThemeProvider> {window.NativeRobosats === undefined ? <HostAlert /> : <TorConnectionBadge />}
<Main />
</ThemeProvider>
</FederationContext.Provider>
</GarageContext.Provider>
</AppContext.Provider> </AppContext.Provider>
</I18nextProvider> </I18nextProvider>
</Suspense> </Suspense>
@ -33,7 +42,7 @@ const App = (): JSX.Element => {
); );
}; };
const loadApp = () => { const loadApp = (): void => {
// waits until the environment is ready for the Android WebView app // waits until the environment is ready for the Android WebView app
if (systemClient.loading) { if (systemClient.loading) {
setTimeout(loadApp, 200); setTimeout(loadApp, 200);

View File

@ -12,10 +12,13 @@ import BookTable from '../../components/BookTable';
import { BarChart, FormatListBulleted, Map } from '@mui/icons-material'; import { BarChart, FormatListBulleted, Map } from '@mui/icons-material';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import MapChart from '../../components/Charts/MapChart'; import MapChart from '../../components/Charts/MapChart';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
const BookPage = (): JSX.Element => { const BookPage = (): JSX.Element => {
const { robot, fetchBook, windowSize, setDelay, setOrder } = const { windowSize } = useContext<UseAppStoreType>(AppContext);
useContext<UseAppStoreType>(AppContext); const { setDelay } = useContext<UseFederationStoreType>(FederationContext);
const { garage, clearOrder } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [view, setView] = useState<'list' | 'depth' | 'map'>('list'); const [view, setView] = useState<'list' | 'depth' | 'map'>('list');
@ -27,25 +30,17 @@ const BookPage = (): JSX.Element => {
const maxBookTableWidth = 85; const maxBookTableWidth = 85;
const chartWidthEm = width - maxBookTableWidth; const chartWidthEm = width - maxBookTableWidth;
useEffect(() => { const onOrderClicked = function (id: number, shortAlias: string): void {
fetchBook(); if (garage.getRobot().avatarLoaded) {
}, []); clearOrder();
setDelay(10000);
const onViewOrder = function () { navigate(`/order/${shortAlias}/${id}`);
setOrder(undefined);
setDelay(10000);
};
const onOrderClicked = function (id: number) {
if (robot.avatarLoaded) {
navigate('/order/' + id);
onViewOrder();
} else { } else {
setOpenNoRobot(true); setOpenNoRobot(true);
} }
}; };
const NavButtons = function () { const NavButtons = function (): JSX.Element {
return ( return (
<ButtonGroup variant='contained' color='inherit'> <ButtonGroup variant='contained' color='inherit'>
<Button <Button
@ -60,13 +55,25 @@ const BookPage = (): JSX.Element => {
<></> <></>
) : ( ) : (
<> <>
<Button onClick={() => setView('list')}> <Button
onClick={() => {
setView('list');
}}
>
<FormatListBulleted /> {t('List')} <FormatListBulleted /> {t('List')}
</Button> </Button>
<Button onClick={() => setView('depth')}> <Button
onClick={() => {
setView('depth');
}}
>
<BarChart /> {t('Chart')} <BarChart /> {t('Chart')}
</Button> </Button>
<Button onClick={() => setView('map')}> <Button
onClick={() => {
setView('map');
}}
>
<Map /> {t('Map')} <Map /> {t('Map')}
</Button> </Button>
</> </>
@ -82,7 +89,9 @@ const BookPage = (): JSX.Element => {
onClose={() => { onClose={() => {
setOpenNoRobot(false); setOpenNoRobot(false);
}} }}
onClickGenerateRobot={() => navigate('/robot')} onClickGenerateRobot={() => {
navigate('/robot');
}}
/> />
{openMaker ? ( {openMaker ? (
<Dialog <Dialog
@ -94,9 +103,11 @@ const BookPage = (): JSX.Element => {
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}> <Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
<MakerForm <MakerForm
onOrderCreated={(id) => { onOrderCreated={(id) => {
navigate('/order/' + id); navigate(`/order/${id}`);
}}
onClickGenerateRobot={() => {
navigate('/robot');
}} }}
onClickGenerateRobot={() => navigate('/robot')}
/> />
</Box> </Box>
</Dialog> </Dialog>

View File

@ -1,20 +1,15 @@
import React, { useContext } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { MemoryRouter, BrowserRouter, Routes, Route } from 'react-router-dom'; import { MemoryRouter, BrowserRouter, Routes, Route } from 'react-router-dom';
import { Box, Slide, Typography, styled } from '@mui/material'; import { Box, Slide, Typography, styled } from '@mui/material';
import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext';
import RobotPage from './RobotPage'; import { RobotPage, MakerPage, BookPage, OrderPage, SettingsPage, NavBar, MainDialogs } from './';
import MakerPage from './MakerPage';
import BookPage from './BookPage';
import OrderPage from './OrderPage';
import SettingsPage from './SettingsPage';
import NavBar from './NavBar';
import MainDialogs from './MainDialogs';
import RobotAvatar from '../components/RobotAvatar'; import RobotAvatar from '../components/RobotAvatar';
import Notifications from '../components/Notifications';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Notifications from '../components/Notifications'; import { FederationContext, UseFederationStoreType } from '../contexts/FederationContext';
import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext'; import { GarageContext, UseGarageStoreType } from '../contexts/GarageContext';
const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter; const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter;
@ -35,39 +30,31 @@ const MainBox = styled(Box)<MainBoxProps>((props) => ({
const Main: React.FC = () => { const Main: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const { settings, page, slideDirection, setOpen, windowSize, navbarHeight, hostUrl, origin } =
settings, useContext<UseAppStoreType>(AppContext);
robot, const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
setRobot, const { garage } = useContext<UseGarageStoreType>(GarageContext);
baseUrl, const [avatarBaseUrl, setAvatarBaseUrl] = useState<string>(hostUrl);
order,
page, useEffect(() => {
slideDirection, setAvatarBaseUrl(federation.getCoordinator(sortedCoordinators[0]).getBaseUrl());
setOpen, }, [settings.network, settings.selfhostedClient, federation, sortedCoordinators]);
windowSize,
navbarHeight,
} = useContext<UseAppStoreType>(AppContext);
return ( return (
<Router> <Router>
<RobotAvatar <RobotAvatar
style={{ display: 'none' }} style={{ display: 'none' }}
nickname={robot.nickname} nickname={garage.getRobot().nickname}
baseUrl={baseUrl} baseUrl={federation.getCoordinator(sortedCoordinators[0]).getBaseUrl()}
onLoad={() => { onLoad={() => garage.updateRobot({ avatarLoaded: true })}
setRobot((robot) => {
return { ...robot, avatarLoaded: true };
});
}}
/> />
<Notifications <Notifications
order={order}
page={page} page={page}
openProfile={() => { openProfile={() => {
setOpen({ ...closeAll, profile: true }); setOpen({ ...closeAll, profile: true });
}} }}
rewards={robot.earnedRewards} rewards={garage.getRobot().earnedRewards}
windowWidth={windowSize.width} windowWidth={windowSize?.width}
/> />
{settings.network === 'testnet' ? ( {settings.network === 'testnet' ? (
<TestnetTypography color='secondary' align='center'> <TestnetTypography color='secondary' align='center'>
@ -87,10 +74,10 @@ const Main: React.FC = () => {
<Slide <Slide
direction={page === 'robot' ? slideDirection.in : slideDirection.out} direction={page === 'robot' ? slideDirection.in : slideDirection.out}
in={page === 'robot'} in={page === 'robot'}
appear={slideDirection.in != undefined} appear={slideDirection.in !== undefined}
> >
<div> <div>
<RobotPage /> <RobotPage avatarBaseUrl={avatarBaseUrl} />
</div> </div>
</Slide> </Slide>
} }
@ -105,7 +92,7 @@ const Main: React.FC = () => {
<Slide <Slide
direction={page === 'offers' ? slideDirection.in : slideDirection.out} direction={page === 'offers' ? slideDirection.in : slideDirection.out}
in={page === 'offers'} in={page === 'offers'}
appear={slideDirection.in != undefined} appear={slideDirection.in !== undefined}
> >
<div> <div>
<BookPage /> <BookPage />
@ -120,7 +107,7 @@ const Main: React.FC = () => {
<Slide <Slide
direction={page === 'create' ? slideDirection.in : slideDirection.out} direction={page === 'create' ? slideDirection.in : slideDirection.out}
in={page === 'create'} in={page === 'create'}
appear={slideDirection.in != undefined} appear={slideDirection.in !== undefined}
> >
<div> <div>
<MakerPage /> <MakerPage />
@ -130,12 +117,12 @@ const Main: React.FC = () => {
/> />
<Route <Route
path='/order/:orderId' path='/order/:shortAlias/:orderId'
element={ element={
<Slide <Slide
direction={page === 'order' ? slideDirection.in : slideDirection.out} direction={page === 'order' ? slideDirection.in : slideDirection.out}
in={page === 'order'} in={page === 'order'}
appear={slideDirection.in != undefined} appear={slideDirection.in !== undefined}
> >
<div> <div>
<OrderPage /> <OrderPage />
@ -150,7 +137,7 @@ const Main: React.FC = () => {
<Slide <Slide
direction={page === 'settings' ? slideDirection.in : slideDirection.out} direction={page === 'settings' ? slideDirection.in : slideDirection.out}
in={page === 'settings'} in={page === 'settings'}
appear={slideDirection.in != undefined} appear={slideDirection.in !== undefined}
> >
<div> <div>
<SettingsPage /> <SettingsPage />
@ -160,7 +147,7 @@ const Main: React.FC = () => {
/> />
</Routes> </Routes>
</MainBox> </MainBox>
<NavBar width={windowSize.width} height={navbarHeight} /> <NavBar />
<MainDialogs /> <MainDialogs />
</Router> </Router>
); );

View File

@ -1,16 +1,17 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { import {
CommunityDialog, CommunityDialog,
CoordinatorSummaryDialog, ExchangeDialog,
InfoDialog, CoordinatorDialog,
AboutDialog,
LearnDialog, LearnDialog,
ProfileDialog, ProfileDialog,
StatsDialog, ClientDialog,
UpdateClientDialog, UpdateDialog,
NoticeDialog,
} from '../../components/Dialogs'; } from '../../components/Dialogs';
import { pn } from '../../utils'; import { pn } from '../../utils';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
export interface OpenDialogs { export interface OpenDialogs {
more: boolean; more: boolean;
@ -18,100 +19,96 @@ export interface OpenDialogs {
community: boolean; community: boolean;
info: boolean; info: boolean;
coordinator: boolean; coordinator: boolean;
stats: boolean; exchange: boolean;
client: boolean;
update: boolean; update: boolean;
profile: boolean; profile: boolean;
notice: boolean; notice: boolean;
} }
const MainDialogs = (): JSX.Element => { const MainDialogs = (): JSX.Element => {
const { open, setOpen, info, limits, robot, setRobot, setCurrentOrder, baseUrl } = const { open, setOpen, settings, clientVersion, hostUrl } =
useContext<UseAppStoreType>(AppContext); useContext<UseAppStoreType>(AppContext);
const { federation, focusedCoordinator, coordinatorUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const [maxAmount, setMaxAmount] = useState<string>('...loading...'); const [maxAmount, setMaxAmount] = useState<string>('...loading...');
useEffect(() => { useEffect(() => {
if (limits.list[1000]) { if (focusedCoordinator) {
setMaxAmount(pn(limits.list[1000].max_amount * 100000000)); const limits = federation.getCoordinator(focusedCoordinator).limits;
if (limits[1000] !== undefined) {
setMaxAmount(pn(limits[1000].max_amount * 100000000));
}
} }
}, [limits.list]); }, [coordinatorUpdatedAt]);
useEffect(() => {
if (info.openUpdateClient) {
setOpen((open) => {
return { ...open, update: true };
});
}
}, [info]);
useEffect(() => {
if (!info.loading && info.notice_severity !== 'none' && info.notice_message !== '') {
setOpen((open) => {
return { ...open, notice: true };
});
}
}, [info]);
return ( return (
<> <>
<UpdateClientDialog <UpdateDialog
open={open.update} coordinatorVersion={federation.exchange.info.version}
coordinatorVersion={info.coordinatorVersion} clientVersion={clientVersion.semver}
clientVersion={info.clientVersion}
onClose={() => { onClose={() => {
setOpen({ ...open, update: false }); setOpen((open) => {
return { ...open, update: false };
});
}} }}
/> />
<NoticeDialog <AboutDialog
open={open.notice}
severity={info.notice_severity}
message={info.notice_message}
onClose={() => {
setOpen({ ...open, notice: false });
}}
/>
<InfoDialog
open={open.info} open={open.info}
maxAmount={maxAmount} maxAmount={maxAmount}
onClose={() => { onClose={() => {
setOpen({ ...open, info: false }); setOpen((open) => {
return { ...open, info: false };
});
}} }}
/> />
<LearnDialog <LearnDialog
open={open.learn} open={open.learn}
onClose={() => { onClose={() => {
setOpen({ ...open, learn: false }); setOpen((open) => {
return { ...open, learn: false };
});
}} }}
/> />
<CommunityDialog <CommunityDialog
open={open.community} open={open.community}
onClose={() => { onClose={() => {
setOpen({ ...open, community: false }); setOpen((open) => {
return { ...open, community: false };
});
}} }}
/> />
<CoordinatorSummaryDialog <ExchangeDialog
open={open.coordinator} open={open.exchange}
onClose={() => { onClose={() => {
setOpen({ ...open, coordinator: false }); setOpen((open) => {
return { ...open, exchange: false };
});
}} }}
info={info}
/> />
<StatsDialog <ClientDialog
open={open.stats} open={open.client}
onClose={() => { onClose={() => {
setOpen({ ...open, stats: false }); setOpen((open) => {
return { ...open, client: false };
});
}} }}
info={info}
/> />
<ProfileDialog <ProfileDialog
open={open.profile} open={open.profile}
baseUrl={baseUrl} baseUrl={hostUrl}
onClose={() => { onClose={() => {
setOpen({ ...open, profile: false }); setOpen({ ...open, profile: false });
}} }}
robot={robot} />
setRobot={setRobot} <CoordinatorDialog
setCurrentOrder={setCurrentOrder} open={open.coordinator}
network={settings.network}
onClose={() => {
setOpen(closeAll);
}}
coordinator={focusedCoordinator ? federation.getCoordinator(focusedCoordinator) : null}
/> />
</> </>
); );

View File

@ -2,7 +2,6 @@ import React, { useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Grid, Paper, Collapse, Typography } from '@mui/material'; import { Grid, Paper, Collapse, Typography } from '@mui/material';
import { filterOrders } from '../../utils'; import { filterOrders } from '../../utils';
import MakerForm from '../../components/MakerForm'; import MakerForm from '../../components/MakerForm';
@ -10,10 +9,13 @@ import BookTable from '../../components/BookTable';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { NoRobotDialog } from '../../components/Dialogs'; import { NoRobotDialog } from '../../components/Dialogs';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
const MakerPage = (): JSX.Element => { const MakerPage = (): JSX.Element => {
const { robot, book, fav, maker, windowSize, navbarHeight, setOrder, setDelay } = const { fav, windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
useContext<UseAppStoreType>(AppContext); const { setDelay, federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, maker } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -23,7 +25,7 @@ const MakerPage = (): JSX.Element => {
const matches = useMemo(() => { const matches = useMemo(() => {
return filterOrders({ return filterOrders({
orders: book.orders, orders: federation.book,
baseFilter: { baseFilter: {
currency: fav.currency === 0 ? 1 : fav.currency, currency: fav.currency === 0 ? 1 : fav.currency,
type: fav.type, type: fav.type,
@ -39,7 +41,7 @@ const MakerPage = (): JSX.Element => {
}, },
}); });
}, [ }, [
book.orders, federation.book,
fav, fav,
maker.premium, maker.premium,
maker.amount, maker.amount,
@ -48,14 +50,14 @@ const MakerPage = (): JSX.Element => {
maker.paymentMethods, maker.paymentMethods,
]); ]);
const onViewOrder = function () { const onViewOrder = function (): void {
setOrder(undefined); garage.updateOrder(null);
setDelay(10000); setDelay(10000);
}; };
const onOrderClicked = function (id: number) { const onOrderClicked = function (id: number): void {
if (robot.avatarLoaded) { if (garage.getRobot().avatarLoaded) {
navigate('/order/' + id); navigate(`/order/${id}`);
onViewOrder(); onViewOrder();
} else { } else {
setOpenNoRobot(true); setOpenNoRobot(true);
@ -69,7 +71,9 @@ const MakerPage = (): JSX.Element => {
onClose={() => { onClose={() => {
setOpenNoRobot(false); setOpenNoRobot(false);
}} }}
onClickGenerateRobot={() => navigate('/robot')} onClickGenerateRobot={() => {
navigate('/robot');
}}
/> />
<Grid item> <Grid item>
<Collapse in={matches.length > 0 && showMatches}> <Collapse in={matches.length > 0 && showMatches}>
@ -103,8 +107,8 @@ const MakerPage = (): JSX.Element => {
}} }}
> >
<MakerForm <MakerForm
onOrderCreated={(id) => { onOrderCreated={(shortAlias, id) => {
navigate('/order/' + id); navigate(`/order/${shortAlias}/${id}`);
}} }}
disableRequest={matches.length > 0 && !showMatches} disableRequest={matches.length > 0 && !showMatches}
collapseAll={showMatches} collapseAll={showMatches}
@ -115,7 +119,9 @@ const MakerPage = (): JSX.Element => {
setShowMatches(false); setShowMatches(false);
}} }}
submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'} submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'}
onClickGenerateRobot={() => navigate('/robot')} onClickGenerateRobot={() => {
navigate('/robot');
}}
/> />
</Paper> </Paper>
</Grid> </Grid>

View File

@ -1,9 +1,8 @@
import React from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useTheme, styled, Grid, IconButton } from '@mui/material'; import { useTheme, styled, Grid, IconButton } from '@mui/material';
import Tooltip, { type TooltipProps, tooltipClasses } from '@mui/material/Tooltip'; import Tooltip, { type TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import { closeAll } from '../../contexts/AppContext'; import { closeAll, type UseAppStoreType, AppContext } from '../../contexts/AppContext';
import { type OpenDialogs } from '../MainDialogs';
import { BubbleChart, Info, People, PriceChange, School } from '@mui/icons-material'; import { BubbleChart, Info, People, PriceChange, School } from '@mui/icons-material';
@ -20,13 +19,13 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
})); }));
interface MoreTooltipProps { interface MoreTooltipProps {
open: OpenDialogs;
setOpen: (state: OpenDialogs) => void;
children: JSX.Element; children: JSX.Element;
} }
const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element => { const MoreTooltip = ({ children }: MoreTooltipProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const { open, setOpen } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme(); const theme = useTheme();
return ( return (
<StyledTooltip <StyledTooltip
@ -89,15 +88,13 @@ const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element
</Grid> </Grid>
<Grid item sx={{ position: 'relative', right: '0.4em' }}> <Grid item sx={{ position: 'relative', right: '0.4em' }}>
<Tooltip enterTouchDelay={250} placement='left' title={t('Coordinator summary')}> <Tooltip enterTouchDelay={250} placement='left' title={t('Exchange summary')}>
<IconButton <IconButton
sx={{ sx={{
color: open.coordinator color: open.exchange ? theme.palette.primary.main : theme.palette.text.secondary,
? theme.palette.primary.main
: theme.palette.text.secondary,
}} }}
onClick={() => { onClick={() => {
setOpen({ ...closeAll, coordinator: !open.coordinator }); setOpen({ ...closeAll, exchange: !open.exchange });
}} }}
> >
<PriceChange /> <PriceChange />
@ -106,13 +103,13 @@ const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element
</Grid> </Grid>
<Grid item sx={{ position: 'relative', right: '0.4em' }}> <Grid item sx={{ position: 'relative', right: '0.4em' }}>
<Tooltip enterTouchDelay={250} placement='left' title={t('Stats for nerds')}> <Tooltip enterTouchDelay={250} placement='left' title={t('client for nerds')}>
<IconButton <IconButton
sx={{ sx={{
color: open.stats ? theme.palette.primary.main : theme.palette.text.secondary, color: open.client ? theme.palette.primary.main : theme.palette.text.secondary,
}} }}
onClick={() => { onClick={() => {
setOpen({ ...closeAll, stats: !open.stats }); setOpen({ ...closeAll, client: !open.client });
}} }}
> >
<BubbleChart /> <BubbleChart />

View File

@ -4,7 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { Tabs, Tab, Paper, useTheme } from '@mui/material'; import { Tabs, Tab, Paper, useTheme } from '@mui/material';
import MoreTooltip from './MoreTooltip'; import MoreTooltip from './MoreTooltip';
import { type Page } from '.'; import { type Page, isPage } from '.';
import { import {
SettingsApplications, SettingsApplications,
@ -16,33 +16,37 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar'; import RobotAvatar from '../../components/RobotAvatar';
import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
interface NavBarProps { const NavBar = (): JSX.Element => {
width: number; const theme = useTheme();
height: number; const { t } = useTranslation();
}
const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
const { const {
robot,
page, page,
settings,
setPage, setPage,
settings,
setSlideDirection, setSlideDirection,
open, open,
setOpen, setOpen,
currentOrder, windowSize,
baseUrl, navbarHeight,
hostUrl,
} = useContext<UseAppStoreType>(AppContext); } = useContext<UseAppStoreType>(AppContext);
const { currentOrder } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const smallBar = width < 50; const smallBar = windowSize?.width < 50;
const color = settings.network === 'mainnet' ? 'primary' : 'secondary';
const tabSx = smallBar const tabSx = smallBar
? { position: 'relative', bottom: robot.avatarLoaded ? '0.9em' : '0.13em', minWidth: '1em' } ? {
position: 'relative',
bottom: garage.getRobot().avatarLoaded ? '0.9em' : '0.13em',
minWidth: '1em',
}
: { position: 'relative', bottom: '1em', minWidth: '2em' }; : { position: 'relative', bottom: '1em', minWidth: '2em' };
const pagesPosition = { const pagesPosition = {
@ -55,17 +59,17 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
useEffect(() => { useEffect(() => {
// change tab (page) into the current route // change tab (page) into the current route
const pathPage: Page = location.pathname.split('/')[1]; const pathPage: Page | string = location.pathname.split('/')[1];
if (pathPage == 'index.html') { if (pathPage === 'index.html') {
navigate('/robot'); navigate('/robot');
setPage('robot'); setPage('robot');
} }
if (pathPage) { if (isPage(pathPage)) {
setPage(pathPage); setPage(pathPage);
} }
}, [location]); }, [location, navigate, setPage]);
const handleSlideDirection = function (oldPage: Page, newPage: Page) { const handleSlideDirection = function (oldPage: Page, newPage: Page): void {
const oldPos: number = pagesPosition[oldPage]; const oldPos: number = pagesPosition[oldPage];
const newPos: number = pagesPosition[newPage]; const newPos: number = pagesPosition[newPage];
setSlideDirection( setSlideDirection(
@ -73,13 +77,14 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
); );
}; };
const changePage = function (mouseEvent: any, newPage: Page) { const changePage = function (mouseEvent: any, newPage: Page): void {
if (newPage === 'none') { if (newPage === 'none') {
return null; return;
} else { } else {
handleSlideDirection(page, newPage); handleSlideDirection(page, newPage);
setPage(newPage); setPage(newPage);
const param = newPage === 'order' ? currentOrder ?? '' : ''; const param =
newPage === 'order' ? `${String(currentOrder.shortAlias)}/${String(currentOrder.id)}` : '';
setTimeout(() => { setTimeout(() => {
navigate(`/${newPage}/${param}`); navigate(`/${newPage}/${param}`);
}, theme.transitions.duration.leavingScreen * 3); }, theme.transitions.duration.leavingScreen * 3);
@ -88,35 +93,41 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
useEffect(() => { useEffect(() => {
setOpen(closeAll); setOpen(closeAll);
}, [page]); }, [page, setOpen]);
return ( return (
<Paper <Paper
elevation={6} elevation={6}
sx={{ height: `${height}em`, width: `100%`, position: 'fixed', bottom: 0, borderRadius: 0 }} sx={{
height: `${navbarHeight}em`,
width: `100%`,
position: 'fixed',
bottom: 0,
borderRadius: 0,
}}
> >
<Tabs <Tabs
TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }} TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }}
variant='fullWidth' variant='fullWidth'
value={page} value={page}
indicatorColor={settings.network === 'mainnet' ? 'primary' : 'secondary'} indicatorColor={color}
textColor={settings.network === 'mainnet' ? 'primary' : 'secondary'} textColor={color}
onChange={changePage} onChange={changePage}
> >
<Tab <Tab
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }} sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
value='none' value='none'
disabled={robot.nickname === null} disabled={garage.getRobot().nickname === null}
onClick={() => { onClick={() => {
setOpen({ ...closeAll, profile: !open.profile }); setOpen({ ...closeAll, profile: !open.profile });
}} }}
icon={ icon={
robot.nickname && robot.avatarLoaded ? ( garage.getRobot().nickname && garage.getRobot().avatarLoaded ? (
<RobotAvatar <RobotAvatar
style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }} style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }}
avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'} avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'}
nickname={robot.nickname} nickname={garage.getRobot().nickname}
baseUrl={baseUrl} baseUrl={hostUrl}
/> />
) : ( ) : (
<></> <></>
@ -150,7 +161,7 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
sx={tabSx} sx={tabSx}
label={smallBar ? undefined : t('Order')} label={smallBar ? undefined : t('Order')}
value='order' value='order'
disabled={!robot.avatarLoaded || currentOrder == undefined} disabled={!garage.getRobot().avatarLoaded || currentOrder.id == null}
icon={<Assignment />} icon={<Assignment />}
iconPosition='start' iconPosition='start'
/> />
@ -166,11 +177,13 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
sx={tabSx} sx={tabSx}
label={smallBar ? undefined : t('More')} label={smallBar ? undefined : t('More')}
value='none' value='none'
onClick={(e) => { onClick={() => {
open.more ? null : setOpen({ ...open, more: true }); setOpen((open) => {
return { ...open, more: !open.more };
});
}} }}
icon={ icon={
<MoreTooltip open={open} setOpen={setOpen} closeAll={closeAll}> <MoreTooltip>
<MoreHoriz /> <MoreHoriz />
</MoreTooltip> </MoreTooltip>
} }

View File

@ -2,3 +2,7 @@ import NavBar from './NavBar';
export type Page = 'robot' | 'order' | 'create' | 'offers' | 'settings' | 'none'; export type Page = 'robot' | 'order' | 'create' | 'offers' | 'settings' | 'none';
export default NavBar; export default NavBar;
export function isPage(page: string): page is Page {
return ['robot', 'order', 'create', 'offers', 'settings', 'none'].includes(page);
}

View File

@ -8,41 +8,54 @@ import OrderDetails from '../../components/OrderDetails';
import { apiClient } from '../../services/api'; import { apiClient } from '../../services/api';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
const OrderPage = (): JSX.Element => { const OrderPage = (): JSX.Element => {
const { const { windowSize, setOpen, settings, navbarHeight, hostUrl, origin } =
windowSize, useContext<UseAppStoreType>(AppContext);
info, const { setFocusedCoordinator, federation, currentOrder, setCurrentOrder, focusedCoordinator } =
order, useContext<UseFederationStoreType>(FederationContext);
robot, const { garage, badOrder, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
settings,
setOrder,
clearOrder,
currentOrder,
setCurrentOrder,
badOrder,
setBadOrder,
baseUrl,
navbarHeight,
} = useContext<UseAppStoreType>(AppContext);
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const params = useParams(); const params = useParams();
const doublePageWidth: number = 50; const doublePageWidth: number = 50;
const maxHeight: number = (windowSize.height - navbarHeight) * 0.85 - 3; const maxHeight: number = (windowSize?.height - navbarHeight) * 0.85 - 3;
const [tab, setTab] = useState<'order' | 'contract'>('contract'); const [tab, setTab] = useState<'order' | 'contract'>('contract');
const [baseUrl, setBaseUrl] = useState<string>(hostUrl);
useEffect(() => { useEffect(() => {
if (currentOrder != params.orderId) { const newOrder = {
clearOrder(); shortAlias: params.shortAlias ?? '',
setCurrentOrder(Number(params.orderId)); id: Number(params.orderId) ?? null,
} order: null,
}, [params.orderId]); };
const renewOrder = function () { const { url, basePath } = federation
if (order != undefined) { .getCoordinator(newOrder.shortAlias)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
setBaseUrl(`${url}${basePath}`);
if (currentOrder.id !== newOrder.id || currentOrder.shortAlias !== newOrder.shortAlias) {
setCurrentOrder(newOrder);
}
}, [params]);
const onClickCoordinator = function (): void {
if (currentOrder.shortAlias) {
setFocusedCoordinator(currentOrder.shortAlias);
}
setOpen((open) => {
return { ...open, coordinator: true };
});
};
const renewOrder = function (): void {
const order = currentOrder.order;
if (order !== null && focusedCoordinator) {
const body = { const body = {
type: order.type, type: order.type,
currency: order.currency, currency: order.currency,
@ -60,32 +73,38 @@ const OrderPage = (): JSX.Element => {
latitude: order.latitude, latitude: order.latitude,
longitude: order.longitude, longitude: order.longitude,
}; };
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post(baseUrl, '/api/make/', body, { tokenSHA256: robot.tokenSHA256 }) .post(url, '/api/make/', body, { tokenSHA256: garage.getRobot().tokenSHA256 })
.then((data: any) => { .then((data: any) => {
if (data.bad_request) { if (data.bad_request !== undefined) {
setBadOrder(data.bad_request); setBadOrder(data.bad_request);
} else if (data.id) { } else if (data.id !== undefined) {
navigate('/order/' + data.id); navigate(`/order/${String(currentOrder.shortAlias)}/${String(data.id)}`);
} }
})
.catch(() => {
setBadOrder('Request error');
}); });
} }
}; };
const startAgain = () => { const startAgain = (): void => {
navigate('/robot'); navigate('/robot');
}; };
return ( return (
<Box> <Box>
{order == undefined && badOrder == undefined ? <CircularProgress /> : null} {currentOrder.order === null && badOrder === undefined && <CircularProgress />}
{badOrder != undefined ? ( {badOrder !== undefined ? (
<Typography align='center' variant='subtitle2' color='secondary'> <Typography align='center' variant='subtitle2' color='secondary'>
{t(badOrder)} {t(badOrder)}
</Typography> </Typography>
) : null} ) : null}
{order != undefined && badOrder == undefined ? ( {currentOrder.order !== null && badOrder === undefined ? (
order.is_participant ? ( currentOrder.order.is_participant ? (
windowSize.width > doublePageWidth ? ( windowSize.width > doublePageWidth ? (
// DOUBLE PAPER VIEW // DOUBLE PAPER VIEW
<Grid <Grid
@ -106,12 +125,12 @@ const OrderPage = (): JSX.Element => {
}} }}
> >
<OrderDetails <OrderDetails
order={order} coordinator={federation.getCoordinator(String(currentOrder.shortAlias))}
setOrder={setOrder} onClickCoordinator={onClickCoordinator}
baseUrl={baseUrl} baseUrl={baseUrl}
info={info} onClickGenerateRobot={() => {
hasRobot={robot.avatarLoaded} navigate('/robot');
onClickGenerateRobot={() => navigate('/robot')} }}
/> />
</Paper> </Paper>
</Grid> </Grid>
@ -125,10 +144,8 @@ const OrderPage = (): JSX.Element => {
}} }}
> >
<TradeBox <TradeBox
order={order} robot={garage.getRobot()}
robot={robot}
settings={settings} settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder} setBadOrder={setBadOrder}
baseUrl={baseUrl} baseUrl={baseUrl}
onRenewOrder={renewOrder} onRenewOrder={renewOrder}
@ -160,22 +177,20 @@ const OrderPage = (): JSX.Element => {
overflow: 'auto', overflow: 'auto',
}} }}
> >
<div style={{ display: tab == 'order' ? '' : 'none' }}> <div style={{ display: tab === 'order' ? '' : 'none' }}>
<OrderDetails <OrderDetails
order={order} coordinator={federation.getCoordinator(String(currentOrder.shortAlias))}
setOrder={setOrder} onClickCoordinator={onClickCoordinator}
baseUrl={baseUrl} baseUrl={baseUrl}
info={info} onClickGenerateRobot={() => {
hasRobot={robot.avatarLoaded} navigate('/robot');
onClickGenerateRobot={() => navigate('/robot')} }}
/> />
</div> </div>
<div style={{ display: tab == 'contract' ? '' : 'none' }}> <div style={{ display: tab === 'contract' ? '' : 'none' }}>
<TradeBox <TradeBox
order={order} robot={garage.getRobot()}
robot={robot}
settings={settings} settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder} setBadOrder={setBadOrder}
baseUrl={baseUrl} baseUrl={baseUrl}
onRenewOrder={renewOrder} onRenewOrder={renewOrder}
@ -195,12 +210,12 @@ const OrderPage = (): JSX.Element => {
}} }}
> >
<OrderDetails <OrderDetails
order={order} coordinator={federation.getCoordinator(String(currentOrder.shortAlias))}
setOrder={setOrder} onClickCoordinator={onClickCoordinator}
baseUrl={baseUrl} baseUrl={hostUrl}
info={info} onClickGenerateRobot={() => {
hasRobot={robot.avatarLoaded} navigate('/robot');
onClickGenerateRobot={() => navigate('/robot')} }}
/> />
</Paper> </Paper>
) )

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { import {
@ -21,6 +21,8 @@ import RobotAvatar from '../../components/RobotAvatar';
import TokenInput from './TokenInput'; import TokenInput from './TokenInput';
import { genBase62Token } from '../../utils'; import { genBase62Token } from '../../utils';
import { NewTabIcon } from '../../components/Icons'; import { NewTabIcon } from '../../components/Icons';
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
interface OnboardingProps { interface OnboardingProps {
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void; setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
@ -35,22 +37,22 @@ interface OnboardingProps {
const Onboarding = ({ const Onboarding = ({
setView, setView,
robot,
inputToken, inputToken,
setInputToken, setInputToken,
setRobot,
badToken, badToken,
getGenerateRobot, getGenerateRobot,
baseUrl,
}: OnboardingProps): JSX.Element => { }: OnboardingProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const [step, setStep] = useState<'1' | '2' | '3'>('1'); const [step, setStep] = useState<'1' | '2' | '3'>('1');
const [generatedToken, setGeneratedToken] = useState<boolean>(false); const [generatedToken, setGeneratedToken] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const generateToken = () => { const generateToken = (): void => {
setGeneratedToken(true); setGeneratedToken(true);
setInputToken(genBase62Token(36)); setInputToken(genBase62Token(36));
setLoading(true); setLoading(true);
@ -63,7 +65,7 @@ const Onboarding = ({
<Box> <Box>
<Accordion expanded={step === '1'} disableGutters={true}> <Accordion expanded={step === '1'} disableGutters={true}>
<AccordionSummary> <AccordionSummary>
<Typography variant='h5' color={step == '1' ? 'text.primary' : 'text.disabled'}> <Typography variant='h5' color={step === '1' ? 'text.primary' : 'text.disabled'}>
{t('1. Generate a token')} {t('1. Generate a token')}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
@ -101,9 +103,7 @@ const Onboarding = ({
autoFocusTarget='copyButton' autoFocusTarget='copyButton'
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}
setRobot={setRobot}
badToken={badToken} badToken={badToken}
robot={robot}
onPressEnter={() => null} onPressEnter={() => null}
/> />
</Grid> </Grid>
@ -122,7 +122,7 @@ const Onboarding = ({
onClick={() => { onClick={() => {
setStep('2'); setStep('2');
getGenerateRobot(inputToken); getGenerateRobot(inputToken);
setRobot({ ...robot, nickname: undefined }); garage.updateRobot({ nickname: undefined });
}} }}
variant='contained' variant='contained'
size='large' size='large'
@ -141,7 +141,7 @@ const Onboarding = ({
<Accordion expanded={step === '2'} disableGutters={true}> <Accordion expanded={step === '2'} disableGutters={true}>
<AccordionSummary> <AccordionSummary>
<Typography variant='h5' color={step == '2' ? 'text.primary' : 'text.disabled'}> <Typography variant='h5' color={step === '2' ? 'text.primary' : 'text.disabled'}>
{t('2. Meet your robot identity')} {t('2. Meet your robot identity')}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
@ -149,7 +149,7 @@ const Onboarding = ({
<Grid container direction='column' alignItems='center' spacing={1}> <Grid container direction='column' alignItems='center' spacing={1}>
<Grid item> <Grid item>
<Typography> <Typography>
{robot.avatarLoaded && robot.nickname ? ( {garage.getRobot().avatarLoaded && Boolean(garage.getRobot().nickname) ? (
t('This is your trading avatar') t('This is your trading avatar')
) : ( ) : (
<> <>
@ -162,7 +162,7 @@ const Onboarding = ({
<Grid item sx={{ width: '13.5em' }}> <Grid item sx={{ width: '13.5em' }}>
<RobotAvatar <RobotAvatar
nickname={robot.nickname} nickname={garage.getRobot().nickname}
smooth={true} smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }} style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating' placeholderType='generating'
@ -174,11 +174,11 @@ const Onboarding = ({
width: '12.4em', width: '12.4em',
}} }}
tooltipPosition='top' tooltipPosition='top'
baseUrl={baseUrl} baseUrl={hostUrl}
/> />
</Grid> </Grid>
{robot.avatarLoaded && robot.nickname ? ( {garage.getRobot().avatarLoaded && Boolean(garage.getRobot().nickname) ? (
<Grid item> <Grid item>
<Typography align='center'>{t('Hi! My name is')}</Typography> <Typography align='center'>{t('Hi! My name is')}</Typography>
<Typography component='h5' variant='h5'> <Typography component='h5' variant='h5'>
@ -197,7 +197,7 @@ const Onboarding = ({
width: '1.5em', width: '1.5em',
}} }}
/> />
<b>{robot.nickname}</b> <b>{garage.getRobot().nickname}</b>
<Bolt <Bolt
sx={{ sx={{
color: '#fcba03', color: '#fcba03',
@ -210,7 +210,9 @@ const Onboarding = ({
</Grid> </Grid>
) : null} ) : null}
<Grid item> <Grid item>
<Collapse in={!!(robot.avatarLoaded && robot.nickname)}> <Collapse
in={!!(garage.getRobot().avatarLoaded && Boolean(garage.getRobot().nickname))}
>
<Button <Button
onClick={() => { onClick={() => {
setStep('3'); setStep('3');
@ -229,7 +231,7 @@ const Onboarding = ({
<Accordion expanded={step === '3'} disableGutters={true}> <Accordion expanded={step === '3'} disableGutters={true}>
<AccordionSummary> <AccordionSummary>
<Typography variant='h5' color={step == '3' ? 'text.primary' : 'text.disabled'}> <Typography variant='h5' color={step === '3' ? 'text.primary' : 'text.disabled'}>
{t('3. Browse or create an order')} {t('3. Browse or create an order')}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>

View File

@ -6,8 +6,6 @@ import TokenInput from './TokenInput';
import Key from '@mui/icons-material/Key'; import Key from '@mui/icons-material/Key';
interface RecoveryProps { interface RecoveryProps {
robot: Robot;
setRobot: (state: Robot) => void;
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void; setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
inputToken: string; inputToken: string;
badToken: string; badToken: string;
@ -16,8 +14,6 @@ interface RecoveryProps {
} }
const Recovery = ({ const Recovery = ({
robot,
setRobot,
inputToken, inputToken,
badToken, badToken,
setView, setView,
@ -26,7 +22,7 @@ const Recovery = ({
}: RecoveryProps): JSX.Element => { }: RecoveryProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const onClickRecover = () => { const onClickRecover = (): void => {
getGenerateRobot(inputToken); getGenerateRobot(inputToken);
setView('profile'); setView('profile');
}; };
@ -48,15 +44,18 @@ const Recovery = ({
showCopy={false} showCopy={false}
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}
setRobot={setRobot}
label={t('Paste token here')} label={t('Paste token here')}
robot={robot}
onPressEnter={onClickRecover} onPressEnter={onClickRecover}
badToken={badToken} badToken={badToken}
/> />
</Grid> </Grid>
<Grid item> <Grid item>
<Button variant='contained' size='large' disabled={!!badToken} onClick={onClickRecover}> <Button
variant='contained'
size='large'
disabled={Boolean(badToken)}
onClick={onClickRecover}
>
<Key /> <div style={{ width: '0.5em' }} /> <Key /> <div style={{ width: '0.5em' }} />
{t('Recover')} {t('Recover')}
</Button> </Button>

View File

@ -12,6 +12,7 @@ import {
Box, Box,
useTheme, useTheme,
Tooltip, Tooltip,
type SelectChangeEvent,
} from '@mui/material'; } from '@mui/material';
import { Bolt, Add, DeleteSweep, Logout, Download } from '@mui/icons-material'; import { Bolt, Add, DeleteSweep, Logout, Download } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar'; import RobotAvatar from '../../components/RobotAvatar';
@ -20,6 +21,8 @@ import { type Slot, type Robot } from '../../models';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { genBase62Token } from '../../utils'; import { genBase62Token } from '../../utils';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
interface RobotProfileProps { interface RobotProfileProps {
robot: Robot; robot: Robot;
@ -29,23 +32,22 @@ interface RobotProfileProps {
inputToken: string; inputToken: string;
logoutRobot: () => void; logoutRobot: () => void;
setInputToken: (state: string) => void; setInputToken: (state: string) => void;
baseUrl: string;
width: number; width: number;
baseUrl: string;
} }
const RobotProfile = ({ const RobotProfile = ({
robot,
setRobot,
inputToken, inputToken,
getGenerateRobot, getGenerateRobot,
setInputToken, setInputToken,
logoutRobot, logoutRobot,
setView, setView,
baseUrl,
width, width,
}: RobotProfileProps): JSX.Element => { }: RobotProfileProps): JSX.Element => {
const { currentSlot, garage, setCurrentSlot, windowSize } = const { windowSize, hostUrl } = useContext<UseAppStoreType>(AppContext);
useContext<UseAppStoreType>(AppContext); const { currentOrder } = useContext<UseFederationStoreType>(FederationContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
@ -53,19 +55,18 @@ const RobotProfile = ({
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
if (robot.nickname && robot.avatarLoaded) { if (garage.getRobot().nickname && garage.getRobot().avatarLoaded) {
setLoading(false); setLoading(false);
} }
}, [robot]); }, [robotUpdatedAt]);
const handleAddRobot = () => { const handleAddRobot = (): void => {
getGenerateRobot(genBase62Token(36), garage.slots.length); getGenerateRobot(genBase62Token(36));
setLoading(true); setLoading(true);
}; };
const handleChangeSlot = (e) => { const handleChangeSlot = (e: SelectChangeEvent<number | 'loading'>): void => {
const slot = e.target.value; garage.currentSlot = Number(e.target.value);
getGenerateRobot(garage.slots[slot].robot.token, slot);
setLoading(true); setLoading(true);
}; };
@ -80,7 +81,7 @@ const RobotProfile = ({
sx={{ width: '100%' }} sx={{ width: '100%' }}
> >
<Grid item sx={{ height: '2.3em', position: 'relative' }}> <Grid item sx={{ height: '2.3em', position: 'relative' }}>
{robot.avatarLoaded && robot.nickname ? ( {garage.getRobot().avatarLoaded && garage.getRobot().nickname ? (
<Typography align='center' component='h5' variant='h5'> <Typography align='center' component='h5' variant='h5'>
<div <div
style={{ style={{
@ -99,7 +100,7 @@ const RobotProfile = ({
}} }}
/> />
)} )}
<b>{robot.nickname}</b> <b>{garage.getRobot().nickname}</b>
{width < 19 ? null : ( {width < 19 ? null : (
<Bolt <Bolt
sx={{ sx={{
@ -121,7 +122,7 @@ const RobotProfile = ({
<Grid item sx={{ width: `13.5em` }}> <Grid item sx={{ width: `13.5em` }}>
<RobotAvatar <RobotAvatar
nickname={robot.nickname} nickname={garage.getRobot().nickname}
smooth={true} smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }} style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating' placeholderType='generating'
@ -134,9 +135,9 @@ const RobotProfile = ({
}} }}
tooltip={t('This is your trading avatar')} tooltip={t('This is your trading avatar')}
tooltipPosition='top' tooltipPosition='top'
baseUrl={baseUrl} baseUrl={hostUrl}
/> />
{robot.found && !robot.lastOrderId ? ( {garage.getRobot().found && Number(garage.getRobot().lastOrderId) > 0 ? (
<Typography align='center' variant='h6'> <Typography align='center' variant='h6'>
{t('Welcome back!')} {t('Welcome back!')}
</Typography> </Typography>
@ -145,27 +146,31 @@ const RobotProfile = ({
)} )}
</Grid> </Grid>
{robot.activeOrderId && robot.avatarLoaded && robot.nickname ? ( {Boolean(garage.getRobot().activeOrderId) &&
garage.getRobot().avatarLoaded &&
Boolean(garage.getRobot().nickname) ? (
<Grid item> <Grid item>
<Button <Button
onClick={() => { onClick={() => {
navigate(`/order/${robot.activeOrderId}`); navigate(`/order/${String(currentOrder.shortAlias)}/${String(currentOrder.id)}`);
}} }}
> >
{t('Active order #{{orderID}}', { orderID: robot.activeOrderId })} {t('Active order #{{orderID}}', { orderID: garage.getRobot().activeOrderId })}
</Button> </Button>
</Grid> </Grid>
) : null} ) : null}
{robot.lastOrderId && robot.avatarLoaded && robot.nickname ? ( {Boolean(garage.getRobot().lastOrderId) &&
garage.getRobot().avatarLoaded &&
Boolean(garage.getRobot().nickname) ? (
<Grid item container direction='column' alignItems='center'> <Grid item container direction='column' alignItems='center'>
<Grid item> <Grid item>
<Button <Button
onClick={() => { onClick={() => {
navigate(`/order/${robot.lastOrderId}`); navigate(`/order/${String(currentOrder.shortAlias)}/${String(currentOrder.id)}`);
}} }}
> >
{t('Last order #{{orderID}}', { orderID: robot.lastOrderId })} {t('Last order #{{orderID}}', { orderID: garage.getRobot().lastOrderId })}
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>
@ -221,8 +226,6 @@ const RobotProfile = ({
editable={false} editable={false}
label={t('Store your token safely')} label={t('Store your token safely')}
setInputToken={setInputToken} setInputToken={setInputToken}
setRobot={setRobot}
robot={robot}
onPressEnter={() => null} onPressEnter={() => null}
/> />
</Grid> </Grid>
@ -246,7 +249,7 @@ const RobotProfile = ({
inputProps={{ inputProps={{
style: { textAlign: 'center' }, style: { textAlign: 'center' },
}} }}
value={loading ? 'loading' : currentSlot} value={loading ? 'loading' : garage.currentSlot}
onChange={handleChangeSlot} onChange={handleChangeSlot}
> >
{loading ? ( {loading ? (
@ -271,7 +274,7 @@ const RobotProfile = ({
smooth={true} smooth={true}
style={{ width: '2.6em', height: '2.6em' }} style={{ width: '2.6em', height: '2.6em' }}
placeholderType='loading' placeholderType='loading'
baseUrl={baseUrl} baseUrl={hostUrl}
small={true} small={true}
/> />
</Grid> </Grid>
@ -314,7 +317,7 @@ const RobotProfile = ({
color='primary' color='primary'
onClick={() => { onClick={() => {
garage.delete(); garage.delete();
setCurrentSlot(0); garage.currentSlot = 0;
logoutRobot(); logoutRobot();
setView('welcome'); setView('welcome');
}} }}

View File

@ -1,16 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IconButton, LinearProgress, TextField, Tooltip } from '@mui/material'; import { IconButton, LinearProgress, TextField, Tooltip } from '@mui/material';
import { type Robot } from '../../models';
import { ContentCopy } from '@mui/icons-material'; import { ContentCopy } from '@mui/icons-material';
import { systemClient } from '../../services/System'; import { systemClient } from '../../services/System';
import { UseGarageStoreType, GarageContext } from '../../contexts/GarageContext';
interface TokenInputProps { interface TokenInputProps {
robot: Robot;
editable?: boolean; editable?: boolean;
fullWidth?: boolean; fullWidth?: boolean;
loading?: boolean; loading?: boolean;
setRobot: (state: Robot) => void;
inputToken: string; inputToken: string;
autoFocusTarget?: 'textfield' | 'copyButton' | 'none'; autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
onPressEnter: () => void; onPressEnter: () => void;
@ -21,11 +19,9 @@ interface TokenInputProps {
} }
const TokenInput = ({ const TokenInput = ({
robot,
editable = true, editable = true,
showCopy = true, showCopy = true,
label, label,
setRobot,
fullWidth = true, fullWidth = true,
onPressEnter, onPressEnter,
autoFocusTarget = 'textfield', autoFocusTarget = 'textfield',
@ -35,6 +31,7 @@ const TokenInput = ({
setInputToken, setInputToken,
}: TokenInputProps): JSX.Element => { }: TokenInputProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const [showCopied, setShowCopied] = useState<boolean>(false); const [showCopied, setShowCopied] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
@ -46,12 +43,12 @@ const TokenInput = ({
} else { } else {
return ( return (
<TextField <TextField
error={inputToken.length > 20 ? !!badToken : false} error={inputToken.length > 20 ? Boolean(badToken) : false}
disabled={!editable} disabled={!editable}
required={true} required={true}
label={label || undefined} label={label ?? ''}
value={inputToken} value={inputToken}
autoFocus={autoFocusTarget == 'textfield'} autoFocus={autoFocusTarget === 'textfield'}
fullWidth={fullWidth} fullWidth={fullWidth}
sx={{ borderColor: 'primary' }} sx={{ borderColor: 'primary' }}
variant={editable ? 'outlined' : 'filled'} variant={editable ? 'outlined' : 'filled'}
@ -69,17 +66,15 @@ const TokenInput = ({
endAdornment: showCopy ? ( endAdornment: showCopy ? (
<Tooltip open={showCopied} title={t('Copied!')}> <Tooltip open={showCopied} title={t('Copied!')}>
<IconButton <IconButton
autoFocus={autoFocusTarget == 'copyButton'} autoFocus={autoFocusTarget === 'copyButton'}
color={robot.copiedToken ? 'inherit' : 'primary'} color={garage.getRobot().copiedToken ? 'inherit' : 'primary'}
onClick={() => { onClick={() => {
systemClient.copyToClipboard(inputToken); systemClient.copyToClipboard(inputToken);
setShowCopied(true); setShowCopied(true);
setTimeout(() => { setTimeout(() => {
setShowCopied(false); setShowCopied(false);
}, 1000); }, 1000);
setRobot((robot) => { garage.updateRobot({ copiedToken: true });
return { ...robot, copiedToken: true };
});
}} }}
> >
<ContentCopy sx={{ width: '1em', height: '1em' }} /> <ContentCopy sx={{ width: '1em', height: '1em' }} />

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Button, Grid, Typography, useTheme } from '@mui/material'; import { Box, Button, Grid, Typography, useTheme } from '@mui/material';
import { RoboSatsTextIcon } from '../../components/Icons'; import { RoboSatsTextIcon } from '../../components/Icons';

View File

@ -21,13 +21,20 @@ import { TorIcon } from '../../components/Icons';
import { genKey } from '../../pgp'; import { genKey } from '../../pgp';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { validateTokenEntropy } from '../../utils'; import { validateTokenEntropy } from '../../utils';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
const RobotPage = (): JSX.Element => { interface RobotPageProps {
const { robot, setRobot, fetchRobot, torStatus, windowSize, baseUrl, settings } = avatarBaseUrl: string;
useContext<UseAppStoreType>(AppContext); }
const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
const { torStatus, windowSize, settings, page } = useContext<UseAppStoreType>(AppContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
const url_token = settings.selfhostedClient ? params.token : null; const urlToken = settings.selfhostedClient ? params.token : null;
const width = Math.min(windowSize.width * 0.8, 28); const width = Math.min(windowSize.width * 0.8, 28);
const maxHeight = windowSize.height * 0.85 - 3; const maxHeight = windowSize.height * 0.85 - 3;
const theme = useTheme(); const theme = useTheme();
@ -35,21 +42,21 @@ const RobotPage = (): JSX.Element => {
const [badToken, setBadToken] = useState<string>(''); const [badToken, setBadToken] = useState<string>('');
const [inputToken, setInputToken] = useState<string>(''); const [inputToken, setInputToken] = useState<string>('');
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>( const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
robot.token ? 'profile' : 'welcome', garage.getRobot().token !== undefined ? 'profile' : 'welcome',
); );
useEffect(() => { useEffect(() => {
if (robot.token) { const token = urlToken ?? garage.getRobot().token;
setInputToken(robot.token); if (token !== undefined) {
setInputToken(token);
} }
const token = url_token ?? robot.token; if (garage.getRobot().nickname !== undefined && token !== undefined) {
if (robot.nickname == null && token) { if (window.NativeRobosats === undefined || torStatus === '"Done"') {
if (window.NativeRobosats === undefined || torStatus == '"Done"') {
getGenerateRobot(token); getGenerateRobot(token);
setView('profile'); setView('profile');
} }
} }
}, [torStatus]); }, [torStatus, page]);
useEffect(() => { useEffect(() => {
if (inputToken.length < 20) { if (inputToken.length < 20) {
@ -61,26 +68,28 @@ const RobotPage = (): JSX.Element => {
} }
}, [inputToken]); }, [inputToken]);
const getGenerateRobot = (token: string, slot?: number) => { const getGenerateRobot = (token: string): void => {
setInputToken(token); setInputToken(token);
genKey(token).then(function (key) { genKey(token)
fetchRobot({ .then((key) => {
newKeys: { const slot = garage.createRobot({
token,
pubKey: key.publicKeyArmored, pubKey: key.publicKeyArmored,
encPrivKey: key.encryptedPrivateKeyArmored, encPrivKey: key.encryptedPrivateKeyArmored,
}, });
newToken: token, federation.fetchRobot(garage, slot);
slot, })
.catch((error) => {
console.error('Error:', error);
}); });
});
}; };
const logoutRobot = () => { const logoutRobot = (): void => {
setInputToken(''); setInputToken('');
setRobot(new Robot()); garage.deleteSlot(garage.currentSlot);
}; };
if (!(window.NativeRobosats === undefined) && !(torStatus == 'DONE' || torStatus == '"Done"')) { if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) {
return ( return (
<Paper <Paper
elevation={12} elevation={12}
@ -146,35 +155,29 @@ const RobotPage = (): JSX.Element => {
{view === 'onboarding' ? ( {view === 'onboarding' ? (
<Onboarding <Onboarding
setView={setView} setView={setView}
robot={robot}
setRobot={setRobot}
badToken={badToken} badToken={badToken}
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot} getGenerateRobot={getGenerateRobot}
baseUrl={baseUrl} avatarBaseUrl={avatarBaseUrl}
/> />
) : null} ) : null}
{view === 'profile' ? ( {view === 'profile' ? (
<RobotProfile <RobotProfile
setView={setView} setView={setView}
robot={robot}
setRobot={setRobot}
logoutRobot={logoutRobot} logoutRobot={logoutRobot}
width={width} width={width}
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot} getGenerateRobot={getGenerateRobot}
baseUrl={baseUrl} avatarBaseUrl={avatarBaseUrl}
/> />
) : null} ) : null}
{view === 'recovery' ? ( {view === 'recovery' ? (
<Recovery <Recovery
setView={setView} setView={setView}
robot={robot}
setRobot={setRobot}
badToken={badToken} badToken={badToken}
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}

View File

@ -1,10 +1,13 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Grid, Paper } from '@mui/material'; import { Grid, Paper } from '@mui/material';
import SettingsForm from '../../components/SettingsForm'; import SettingsForm from '../../components/SettingsForm';
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import FederationTable from '../../components/FederationTable';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
const SettingsPage = (): JSX.Element => { const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext); const { windowSize, navbarHeight, settings, setOpen, open, hostUrl } =
useContext<UseAppStoreType>(AppContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3; const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
return ( return (
@ -12,7 +15,7 @@ const SettingsPage = (): JSX.Element => {
elevation={12} elevation={12}
sx={{ sx={{
padding: '0.6em', padding: '0.6em',
width: '21em', width: '20.5em',
maxHeight: `${maxHeight}em`, maxHeight: `${maxHeight}em`,
overflow: 'auto', overflow: 'auto',
overflowX: 'clip', overflowX: 'clip',
@ -22,6 +25,16 @@ const SettingsPage = (): JSX.Element => {
<Grid item> <Grid item>
<SettingsForm showNetwork={!(window.NativeRobosats === undefined)} /> <SettingsForm showNetwork={!(window.NativeRobosats === undefined)} />
</Grid> </Grid>
<Grid item>
<FederationTable
openCoordinator={() => {
setOpen({ ...open, coordinator: true });
}}
baseUrl={hostUrl}
maxHeight={14}
network={settings.network}
/>
</Grid>
</Grid> </Grid>
</Paper> </Paper>
); );

View File

@ -0,0 +1,7 @@
export { default as BookPage } from './BookPage';
export { default as MainDialogs } from './MainDialogs';
export { default as MakerPage } from './MakerPage';
export { default as NavBar } from './NavBar';
export { default as OrderPage } from './OrderPage';
export { default as RobotPage } from './RobotPage';
export { default as SettingsPage } from './SettingsPage';

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Typography, Typography,
@ -26,7 +26,7 @@ interface BookControlProps {
fav: Favorites; fav: Favorites;
setFav: (state: Favorites) => void; setFav: (state: Favorites) => void;
paymentMethod: string[]; paymentMethod: string[];
setPaymentMethods: () => void; setPaymentMethods: (state: string[]) => void;
} }
const BookControl = ({ const BookControl = ({
@ -43,22 +43,22 @@ const BookControl = ({
const typeZeroText = fav.mode === 'fiat' ? t('Buy') : t('Swap In'); const typeZeroText = fav.mode === 'fiat' ? t('Buy') : t('Swap In');
const typeOneText = fav.mode === 'fiat' ? t('Sell') : t('Swap Out'); const typeOneText = fav.mode === 'fiat' ? t('Sell') : t('Swap Out');
const small = const small =
(typeZeroText.length + typeOneText.length) * 0.7 + (fav.mode == 'fiat' ? 16 : 7.5); (typeZeroText.length + typeOneText.length) * 0.7 + (fav.mode === 'fiat' ? 16 : 7.5);
const medium = small + 13; const medium = small + 13;
const large = medium + (t('and use').length + t('pay with').length) * 0.6 + 5; const large = medium + (t('and use').length + t('pay with').length) * 0.6 + 5;
return [typeZeroText, typeOneText, small, medium, large]; return [typeZeroText, typeOneText, small, medium, large];
}, [i18n.language, fav.mode]); }, [i18n.language, fav.mode]);
const handleCurrencyChange = function (e) { const handleCurrencyChange = function (e: React.ChangeEvent<HTMLInputElement>): void {
const currency = e.target.value; const currency = Number(e.target.value);
setFav({ ...fav, currency, mode: currency === 1000 ? 'swap' : 'fiat' }); setFav({ ...fav, currency, mode: currency === 1000 ? 'swap' : 'fiat' });
}; };
const handleTypeChange = function (mouseEvent, val) { const handleTypeChange = function (mouseEvent: React.MouseEvent, val: number): void {
setFav({ ...fav, type: val }); setFav({ ...fav, type: val });
}; };
const handleModeChange = function (mouseEvent, val) { const handleModeChange = function (mouseEvent: React.MouseEvent, val: number): void {
const mode = fav.mode === 'fiat' ? 'swap' : 'fiat'; const mode = fav.mode === 'fiat' ? 'swap' : 'fiat';
const currency = fav.mode === 'fiat' ? 1000 : 0; const currency = fav.mode === 'fiat' ? 1000 : 0;
setFav({ ...fav, mode, currency }); setFav({ ...fav, mode, currency });
@ -197,7 +197,7 @@ const BookControl = ({
{width > large ? ( {width > large ? (
<Grid item sx={{ position: 'relative', top: '0.5em' }}> <Grid item sx={{ position: 'relative', top: '0.5em' }}>
<Typography variant='caption' color='text.secondary'> <Typography variant='caption' color='text.secondary'>
{fav.currency == 1000 ? t(fav.type === 0 ? 'to' : 'from') : t('pay with')} {fav.currency === 1000 ? t(fav.type === 0 ? 'to' : 'from') : t('pay with')}
</Typography> </Typography>
</Grid> </Grid>
) : null} ) : null}
@ -218,10 +218,10 @@ const BookControl = ({
listBoxProps={{ sx: { width: '13em' } }} listBoxProps={{ sx: { width: '13em' } }}
onAutocompleteChange={setPaymentMethods} onAutocompleteChange={setPaymentMethods}
value={paymentMethod} value={paymentMethod}
optionsType={fav.currency == 1000 ? 'swap' : 'fiat'} optionsType={fav.currency === 1000 ? 'swap' : 'fiat'}
error={false} error={false}
helperText={''} helperText={''}
label={fav.currency == 1000 ? t('DESTINATION') : t('METHOD')} label={fav.currency === 1000 ? t('DESTINATION') : t('METHOD')}
tooltipTitle='' tooltipTitle=''
listHeaderText='' listHeaderText=''
addNewButtonText='' addNewButtonText=''
@ -247,7 +247,7 @@ const BookControl = ({
label={t('Select Payment Method')} label={t('Select Payment Method')}
required={true} required={true}
renderValue={(value) => renderValue={(value) =>
value == 'ANY' ? ( value === 'ANY' ? (
<CheckBoxOutlineBlankIcon style={{ position: 'relative', top: '0.1em' }} /> <CheckBoxOutlineBlankIcon style={{ position: 'relative', top: '0.1em' }} />
) : ( ) : (
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
@ -258,9 +258,9 @@ const BookControl = ({
inputProps={{ inputProps={{
style: { textAlign: 'center' }, style: { textAlign: 'center' },
}} }}
value={paymentMethod[0] ? paymentMethod[0] : 'ANY'} value={paymentMethod[0] ?? 'ANY'}
onChange={(e) => { onChange={(e) => {
setPaymentMethods(e.target.value == 'ANY' ? [] : [e.target.value]); setPaymentMethods(e.target.value === 'ANY' ? [] : [e.target.value]);
}} }}
> >
<MenuItem value={'ANY'}> <MenuItem value={'ANY'}>

View File

@ -22,6 +22,8 @@ import {
type GridColumnVisibilityModel, type GridColumnVisibilityModel,
GridPagination, GridPagination,
type GridPaginationModel, type GridPaginationModel,
type GridColDef,
type GridValidRowModel,
} from '@mui/x-data-grid'; } from '@mui/x-data-grid';
import currencyDict from '../../../static/assets/currencies.json'; import currencyDict from '../../../static/assets/currencies.json';
import { type PublicOrder } from '../../models'; import { type PublicOrder } from '../../models';
@ -35,6 +37,7 @@ import RobotAvatar from '../RobotAvatar';
// Icons // Icons
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material'; import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
const ClickThroughDataGrid = styled(DataGrid)({ const ClickThroughDataGrid = styled(DataGrid)({
'& .MuiDataGrid-overlayWrapperInner': { '& .MuiDataGrid-overlayWrapperInner': {
@ -62,15 +65,15 @@ const ClickThroughDataGrid = styled(DataGrid)({
}, },
}); });
const premiumColor = function (baseColor: string, accentColor: string, point: number) { const premiumColor = function (baseColor: string, accentColor: string, point: number): string {
const baseRGB = hexToRgb(baseColor); const baseRGB = hexToRgb(baseColor);
const accentRGB = hexToRgb(accentColor); const accentRGB = hexToRgb(accentColor);
const redDiff = accentRGB[0] - baseRGB[0]; const redDiff = accentRGB[0] - baseRGB[0];
const red = baseRGB[0] + redDiff * point; const red = Number(baseRGB[0]) + redDiff * point;
const greenDiff = accentRGB[1] - baseRGB[1]; const greenDiff = accentRGB[1] - baseRGB[1];
const green = baseRGB[1] + greenDiff * point; const green = Number(baseRGB[1]) + greenDiff * point;
const blueDiff = accentRGB[2] - baseRGB[2]; const blueDiff = accentRGB[2] - baseRGB[2];
const blue = baseRGB[2] + blueDiff * point; const blue = Number(baseRGB[2]) + blueDiff * point;
return `rgb(${Math.round(red)}, ${Math.round(green)}, ${Math.round(blue)}, ${0.7 + point * 0.3})`; return `rgb(${Math.round(red)}, ${Math.round(green)}, ${Math.round(blue)}, ${0.7 + point * 0.3})`;
}; };
@ -86,7 +89,7 @@ interface BookTableProps {
showControls?: boolean; showControls?: boolean;
showFooter?: boolean; showFooter?: boolean;
showNoResults?: boolean; showNoResults?: boolean;
onOrderClicked?: (id: number) => void; onOrderClicked?: (id: number, shortAlias: string) => void;
} }
const BookTable = ({ const BookTable = ({
@ -103,11 +106,18 @@ const BookTable = ({
showNoResults = true, showNoResults = true,
onOrderClicked = () => null, onOrderClicked = () => null,
}: BookTableProps): JSX.Element => { }: BookTableProps): JSX.Element => {
const { book, fetchBook, fav, setFav, baseUrl } = useContext<UseAppStoreType>(AppContext); const { fav, setFav, settings, setOpen, hostUrl, origin } =
useContext<UseAppStoreType>(AppContext);
const { federation, setFocusedCoordinator, coordinatorUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const orders = orderList ?? book.orders; const orders = orderList ?? federation.book;
const loadingProgress = useMemo(() => {
return (federation.exchange.onlineCoordinators / federation.exchange.totalCoordinators) * 100;
}, [coordinatorUpdatedAt]);
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({ const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
pageSize: 0, pageSize: 0,
page: 0, page: 0,
@ -133,10 +143,10 @@ const BookTable = ({
useEffect(() => { useEffect(() => {
setPaginationModel({ setPaginationModel({
pageSize: book.loading && orders.length == 0 ? 0 : defaultPageSize, pageSize: federation.loading && orders.length === 0 ? 0 : defaultPageSize,
page: paginationModel.page, page: paginationModel.page,
}); });
}, [book.loading, orders, defaultPageSize]); }, [coordinatorUpdatedAt, orders, defaultPageSize]);
const localeText = useMemo(() => { const localeText = useMemo(() => {
return { return {
@ -196,8 +206,17 @@ const BookTable = ({
headerName: t('Robot'), headerName: t('Robot'),
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
const { url, basePath } = federation
.getCoordinator(params.row.coordinatorShortAlias)
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
return ( return (
<ListItemButton style={{ cursor: 'pointer', position: 'relative', left: '-1.3em' }}> <ListItemButton
style={{ cursor: 'pointer', position: 'relative', left: '-1.3em' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
<ListItemAvatar> <ListItemAvatar>
<RobotAvatar <RobotAvatar
nickname={params.row.maker_nick} nickname={params.row.maker_nick}
@ -207,7 +226,7 @@ const BookTable = ({
orderType={params.row.type} orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)} statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)} tooltip={t(params.row.maker_status)}
baseUrl={baseUrl} baseUrl={url + basePath}
small={true} small={true}
/> />
</ListItemAvatar> </ListItemAvatar>
@ -224,22 +243,65 @@ const BookTable = ({
headerName: t('Robot'), headerName: t('Robot'),
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
const { url, basePath } = federation
.getCoordinator(params.row.coordinatorShortAlias)
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
return ( return (
<div style={{ position: 'relative', left: '-1.64em' }}> <div
<ListItemButton style={{ cursor: 'pointer' }}> style={{ position: 'relative', left: '-0.34em', cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
<RobotAvatar
nickname={params.row.maker_nick}
smooth={true}
flipHorizontally={true}
style={{ width: '3.215em', height: '3.215em' }}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
baseUrl={url + basePath}
/>
</div>
);
},
};
}, []);
const onClickCoordinator = function (shortAlias: string): void {
setFocusedCoordinator(shortAlias);
setOpen((open) => {
return { ...open, coordinator: true };
});
};
const coordinatorObj = useCallback((width: number) => {
return {
field: 'coordinatorShortAlias',
headerName: t('Host'),
width: width * fontSize,
renderCell: (params: any) => {
return (
<ListItemButton
style={{ cursor: 'pointer', position: 'relative', left: '-1.54em' }}
onClick={() => {
onClickCoordinator(params.row.coordinatorShortAlias);
}}
>
<ListItemAvatar>
<RobotAvatar <RobotAvatar
nickname={params.row.maker_nick} nickname={params.row.coordinatorShortAlias}
coordinator={true}
style={{ width: '3.215em', height: '3.215em' }}
smooth={true} smooth={true}
flipHorizontally={true} flipHorizontally={true}
style={{ width: '3.215em', height: '3.215em' }} baseUrl={hostUrl}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
baseUrl={baseUrl}
small={true} small={true}
/> />
</ListItemButton> </ListItemAvatar>
</div> </ListItemButton>
); );
}, },
}; };
@ -250,10 +312,20 @@ const BookTable = ({
field: 'type', field: 'type',
headerName: t('Is'), headerName: t('Is'),
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => renderCell: (params: any) => {
params.row.type return (
? t(fav.mode === 'fiat' ? 'Seller' : 'Swapping Out') <div
: t(fav.mode === 'fiat' ? 'Buyer' : 'Swapping In'), style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
{params.row.type === 1
? t(fav.mode === 'fiat' ? 'Seller' : 'Swapping Out')
: t(fav.mode === 'fiat' ? 'Buyer' : 'Swapping In')}
</div>
);
},
}; };
}, []); }, []);
@ -270,7 +342,12 @@ const BookTable = ({
const maxAmount = const maxAmount =
fav.mode === 'swap' ? params.row.max_amount * 100000 : params.row.max_amount; fav.mode === 'swap' ? params.row.max_amount * 100000 : params.row.max_amount;
return ( return (
<div style={{ cursor: 'pointer' }}> <div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
{amountToString(amount, params.row.has_range, minAmount, maxAmount) + {amountToString(amount, params.row.has_range, minAmount, maxAmount) +
(fav.mode === 'swap' ? 'K Sats' : '')} (fav.mode === 'swap' ? 'K Sats' : '')}
</div> </div>
@ -285,7 +362,7 @@ const BookTable = ({
headerName: t('Currency'), headerName: t('Currency'),
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
const currencyCode = currencyDict[params.row.currency.toString()]; const currencyCode = String(currencyDict[params.row.currency.toString()]);
return ( return (
<div <div
style={{ style={{
@ -294,6 +371,9 @@ const BookTable = ({
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap', flexWrap: 'wrap',
}} }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
> >
{currencyCode} {currencyCode}
<div style={{ width: '0.3em' }} /> <div style={{ width: '0.3em' }} />
@ -311,7 +391,12 @@ const BookTable = ({
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
return ( return (
<div style={{ cursor: 'pointer' }}> <div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
<PaymentStringAsIcons <PaymentStringAsIcons
othersText={t('Others')} othersText={t('Others')}
verbose={true} verbose={true}
@ -337,6 +422,9 @@ const BookTable = ({
left: '-4px', left: '-4px',
cursor: 'pointer', cursor: 'pointer',
}} }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
> >
<PaymentStringAsIcons <PaymentStringAsIcons
othersText={t('Others')} othersText={t('Others')}
@ -356,9 +444,16 @@ const BookTable = ({
type: 'number', type: 'number',
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
const currencyCode = currencyDict[params.row.currency.toString()]; const currencyCode = String(currencyDict[params.row.currency.toString()]);
return ( return (
<div style={{ cursor: 'pointer' }}>{`${pn(params.row.price)} ${currencyCode}/BTC`}</div> <div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
{`${pn(params.row.price)} ${currencyCode}/BTC`}
</div>
); );
}, },
}; };
@ -377,10 +472,11 @@ const BookTable = ({
type: 'number', type: 'number',
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
const currencyCode = currencyDict[params.row.currency.toString()]; const currencyCode = String(currencyDict[params.row.currency.toString()]);
let fontColor = `rgb(0,0,0)`; let fontColor = `rgb(0,0,0)`;
let premiumPoint = 0;
if (params.row.type === 0) { if (params.row.type === 0) {
var premiumPoint = params.row.premium / buyOutstandingPremium; premiumPoint = params.row.premium / buyOutstandingPremium;
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint; premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
fontColor = premiumColor( fontColor = premiumColor(
theme.palette.text.primary, theme.palette.text.primary,
@ -388,7 +484,7 @@ const BookTable = ({
premiumPoint, premiumPoint,
); );
} else { } else {
var premiumPoint = (sellStandardPremium - params.row.premium) / sellStandardPremium; premiumPoint = (sellStandardPremium - params.row.premium) / sellStandardPremium;
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint; premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
fontColor = premiumColor( fontColor = premiumColor(
theme.palette.text.primary, theme.palette.text.primary,
@ -401,11 +497,16 @@ const BookTable = ({
<Tooltip <Tooltip
placement='left' placement='left'
enterTouchDelay={0} enterTouchDelay={0}
title={pn(params.row.price) + ' ' + currencyCode + '/BTC'} title={`${pn(params.row.price)} ${currencyCode}/BTC`}
> >
<div style={{ cursor: 'pointer' }}> <div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
<Typography variant='inherit' color={fontColor} sx={{ fontWeight }}> <Typography variant='inherit' color={fontColor} sx={{ fontWeight }}>
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'} {`${parseFloat(parseFloat(params.row.premium).toFixed(4))}%`}
</Typography> </Typography>
</div> </div>
</Tooltip> </Tooltip>
@ -425,7 +526,16 @@ const BookTable = ({
renderCell: (params: any) => { renderCell: (params: any) => {
const hours = Math.round(params.row.escrow_duration / 3600); const hours = Math.round(params.row.escrow_duration / 3600);
const minutes = Math.round((params.row.escrow_duration - hours * 3600) / 60); const minutes = Math.round((params.row.escrow_duration - hours * 3600) / 60);
return <div style={{ cursor: 'pointer' }}>{hours > 0 ? `${hours}h` : `${minutes}m`}</div>; return (
<div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
{hours > 0 ? `${hours}h` : `${minutes}m`}
</div>
);
}, },
}; };
}, []); }, []);
@ -443,7 +553,12 @@ const BookTable = ({
const hours = Math.round(timeToExpiry / (3600 * 1000)); const hours = Math.round(timeToExpiry / (3600 * 1000));
const minutes = Math.round((timeToExpiry - hours * (3600 * 1000)) / 60000); const minutes = Math.round((timeToExpiry - hours * (3600 * 1000)) / 60000);
return ( return (
<Box sx={{ position: 'relative', display: 'inline-flex', left: '0.3em' }}> <Box
sx={{ position: 'relative', display: 'inline-flex', left: '0.3em' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
<CircularProgress <CircularProgress
value={percent} value={percent}
color={percent < 15 ? 'error' : percent < 30 ? 'warning' : 'success'} color={percent < 15 ? 'error' : percent < 30 ? 'warning' : 'success'}
@ -481,7 +596,12 @@ const BookTable = ({
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
return ( return (
<div style={{ cursor: 'pointer' }}> <div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
{params.row.satoshis_now > 1000000 {params.row.satoshis_now > 1000000
? `${pn(Math.round(params.row.satoshis_now / 10000) / 100)} M` ? `${pn(Math.round(params.row.satoshis_now / 10000) / 100)} M`
: `${pn(Math.round(params.row.satoshis_now / 1000))} K`} : `${pn(Math.round(params.row.satoshis_now / 1000))} K`}
@ -498,9 +618,14 @@ const BookTable = ({
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
return ( return (
<div style={{ cursor: 'pointer' }}> <div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
<Typography variant='caption' color='text.secondary'> <Typography variant='caption' color='text.secondary'>
{`#${params.row.id}`} {`#${String(params.row.id)}`}
</Typography> </Typography>
</div> </div>
); );
@ -515,7 +640,14 @@ const BookTable = ({
type: 'number', type: 'number',
width: width * fontSize, width: width * fontSize,
renderCell: (params: any) => { renderCell: (params: any) => {
return <div style={{ cursor: 'pointer' }}>{`${Number(params.row.bond_size)}%`}</div>; return (
<div
style={{ cursor: 'pointer' }}
onClick={() => {
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>{`${Number(params.row.bond_size)}%`}</div>
);
}, },
}; };
}, []); }, []);
@ -524,7 +656,7 @@ const BookTable = ({
return { return {
amount: { amount: {
priority: 1, priority: 1,
order: 4, order: 5,
normal: { normal: {
width: fav.mode === 'swap' ? 9.5 : 6.5, width: fav.mode === 'swap' ? 9.5 : 6.5,
object: amountObj, object: amountObj,
@ -532,7 +664,7 @@ const BookTable = ({
}, },
currency: { currency: {
priority: 2, priority: 2,
order: 5, order: 6,
normal: { normal: {
width: fav.mode === 'swap' ? 0 : 5.9, width: fav.mode === 'swap' ? 0 : 5.9,
object: currencyObj, object: currencyObj,
@ -540,7 +672,7 @@ const BookTable = ({
}, },
premium: { premium: {
priority: 3, priority: 3,
order: 11, order: 12,
normal: { normal: {
width: 6, width: 6,
object: premiumObj, object: premiumObj,
@ -548,7 +680,7 @@ const BookTable = ({
}, },
payment_method: { payment_method: {
priority: 4, priority: 4,
order: 6, order: 7,
normal: { normal: {
width: 12.85, width: 12.85,
object: paymentObj, object: paymentObj,
@ -570,9 +702,17 @@ const BookTable = ({
object: robotSmallObj, object: robotSmallObj,
}, },
}, },
coordinatorShortAlias: {
priority: 5,
order: 3,
normal: {
width: 4.1,
object: coordinatorObj,
},
},
price: { price: {
priority: 6, priority: 6,
order: 10, order: 11,
normal: { normal: {
width: 10, width: 10,
object: priceObj, object: priceObj,
@ -580,7 +720,7 @@ const BookTable = ({
}, },
expires_at: { expires_at: {
priority: 7, priority: 7,
order: 7, order: 8,
normal: { normal: {
width: 5, width: 5,
object: expiryObj, object: expiryObj,
@ -588,7 +728,7 @@ const BookTable = ({
}, },
escrow_duration: { escrow_duration: {
priority: 8, priority: 8,
order: 8, order: 9,
normal: { normal: {
width: 4.8, width: 4.8,
object: timerObj, object: timerObj,
@ -596,7 +736,7 @@ const BookTable = ({
}, },
satoshis_now: { satoshis_now: {
priority: 9, priority: 9,
order: 9, order: 10,
normal: { normal: {
width: 6, width: 6,
object: satoshisObj, object: satoshisObj,
@ -612,7 +752,7 @@ const BookTable = ({
}, },
bond_size: { bond_size: {
priority: 11, priority: 11,
order: 10, order: 11,
normal: { normal: {
width: 4.2, width: 4.2,
object: bondObj, object: bondObj,
@ -620,7 +760,7 @@ const BookTable = ({
}, },
id: { id: {
priority: 12, priority: 12,
order: 12, order: 13,
normal: { normal: {
width: 4.8, width: 4.8,
object: idObj, object: idObj,
@ -629,7 +769,10 @@ const BookTable = ({
}; };
}, [fav.mode]); }, [fav.mode]);
const filteredColumns = function (maxWidth: number) { const filteredColumns = function (maxWidth: number): {
columns: Array<GridColDef<GridValidRowModel>>;
width: number;
} {
const useSmall = maxWidth < 70; const useSmall = maxWidth < 70;
const selectedColumns: object[] = []; const selectedColumns: object[] = [];
const columnVisibilityModel: GridColumnVisibilityModel = {}; const columnVisibilityModel: GridColumnVisibilityModel = {};
@ -641,8 +784,10 @@ const BookTable = ({
continue; continue;
} }
const colWidth = useSmall && value.small ? value.small.width : value.normal.width; const colWidth = Number(
const colObject = useSmall && value.small ? value.small.object : value.normal.object; useSmall && Boolean(value.small) ? value.small.width : value.normal.width,
);
const colObject = useSmall && Boolean(value.small) ? value.small.object : value.normal.object;
if (width + colWidth < maxWidth || selectedColumns.length < 2) { if (width + colWidth < maxWidth || selectedColumns.length < 2) {
width = width + colWidth; width = width + colWidth;
@ -659,19 +804,19 @@ const BookTable = ({
return first[1] - second[1]; return first[1] - second[1];
}); });
const columns = selectedColumns.map(function (item) { const columns: Array<GridColDef<GridValidRowModel>> = selectedColumns.map(function (item) {
return item[0]; return item[0];
}); });
setColumnVisibilityModel(columnVisibilityModel); setColumnVisibilityModel(columnVisibilityModel);
return [columns, width * 0.875 + 0.15]; return { columns, width: width * 0.875 + 0.15 };
}; };
const [columns, width] = useMemo(() => { const { columns, width } = useMemo(() => {
return filteredColumns(fullscreen ? fullWidth : maxWidth); return filteredColumns(fullscreen ? fullWidth : maxWidth);
}, [maxWidth, fullscreen, fullWidth, fav.mode]); }, [maxWidth, fullscreen, fullWidth, fav.mode]);
const Footer = function () { const Footer = function (): JSX.Element {
return ( return (
<Grid container alignItems='center' direction='row' justifyContent='space-between'> <Grid container alignItems='center' direction='row' justifyContent='space-between'>
<Grid item> <Grid item>
@ -686,11 +831,7 @@ const BookTable = ({
</IconButton> </IconButton>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<IconButton <IconButton onClick={() => federation.update()}>
onClick={() => {
fetchBook();
}}
>
<Refresh /> <Refresh />
</IconButton> </IconButton>
</Grid> </Grid>
@ -712,7 +853,7 @@ const BookTable = ({
Toolbar?: (props: any) => JSX.Element; Toolbar?: (props: any) => JSX.Element;
} }
const NoResultsOverlay = function () { const NoResultsOverlay = function (): JSX.Element {
return ( return (
<Grid <Grid
container container
@ -723,14 +864,14 @@ const BookTable = ({
> >
<Grid item> <Grid item>
<Typography align='center' component='h5' variant='h5'> <Typography align='center' component='h5' variant='h5'>
{fav.type == 0 {fav.type === 0
? t('No orders found to sell BTC for {{currencyCode}}', { ? t('No orders found to sell BTC for {{currencyCode}}', {
currencyCode: currencyCode:
fav.currency == 0 ? t('ANY') : currencyDict[fav.currency.toString()], fav.currency === 0 ? t('ANY') : currencyDict[fav.currency.toString()],
}) })
: t('No orders found to buy BTC for {{currencyCode}}', { : t('No orders found to buy BTC for {{currencyCode}}', {
currencyCode: currencyCode:
fav.currency == 0 ? t('ANY') : currencyDict[fav.currency.toString()], fav.currency === 0 ? t('ANY') : currencyDict[fav.currency.toString()],
})} })}
</Typography> </Typography>
</Grid> </Grid>
@ -786,7 +927,8 @@ const BookTable = ({
rowHeight={3.714 * theme.typography.fontSize} rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize} headerHeight={3.25 * theme.typography.fontSize}
rows={filteredOrders} rows={filteredOrders}
loading={book.loading} getRowId={(params: PublicOrder) => `${String(params.coordinatorShortAlias)}/${params.id}`}
loading={federation.loading}
columns={columns} columns={columns}
columnVisibilityModel={columnVisibilityModel} columnVisibilityModel={columnVisibilityModel}
onColumnVisibilityModelChange={(newColumnVisibilityModel) => { onColumnVisibilityModelChange={(newColumnVisibilityModel) => {
@ -802,15 +944,16 @@ const BookTable = ({
paymentMethod: paymentMethods, paymentMethod: paymentMethods,
setPaymentMethods, setPaymentMethods,
}, },
loadingOverlay: {
variant: 'determinate',
value: loadingProgress,
},
}} }}
paginationModel={paginationModel} paginationModel={paginationModel}
pageSizeOptions={width < 22 ? [] : [0, defaultPageSize, defaultPageSize * 2, 50, 100]} pageSizeOptions={width < 22 ? [] : [0, defaultPageSize, defaultPageSize * 2, 50, 100]}
onPaginationModelChange={(newPaginationModel) => { onPaginationModelChange={(newPaginationModel) => {
setPaginationModel(newPaginationModel); setPaginationModel(newPaginationModel);
}} }}
onRowClick={(params: any) => {
onOrderClicked(params.row.id);
}}
/> />
</Paper> </Paper>
); );
@ -823,7 +966,7 @@ const BookTable = ({
rowHeight={3.714 * theme.typography.fontSize} rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize} headerHeight={3.25 * theme.typography.fontSize}
rows={filteredOrders} rows={filteredOrders}
loading={book.loading} loading={federation.loading}
columns={columns} columns={columns}
hideFooter={!showFooter} hideFooter={!showFooter}
components={gridComponents} components={gridComponents}
@ -845,9 +988,6 @@ const BookTable = ({
onPaginationModelChange={(newPaginationModel) => { onPaginationModelChange={(newPaginationModel) => {
setPaginationModel(newPaginationModel); setPaginationModel(newPaginationModel);
}} }}
onRowClick={(params: any) => {
onOrderClicked(params.row.id);
}}
/> />
</Paper> </Paper>
</Dialog> </Dialog>

View File

@ -24,15 +24,16 @@ import { type PublicOrder, type Order } from '../../../models';
import { matchMedian } from '../../../utils'; import { matchMedian } from '../../../utils';
import currencyDict from '../../../../static/assets/currencies.json'; import currencyDict from '../../../../static/assets/currencies.json';
import getNivoScheme from '../NivoScheme'; import getNivoScheme from '../NivoScheme';
import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext';
import OrderTooltip from '../helpers/OrderTooltip'; import OrderTooltip from '../helpers/OrderTooltip';
import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext';
import { FederationContext, UseFederationStoreType } from '../../../contexts/FederationContext';
interface DepthChartProps { interface DepthChartProps {
maxWidth: number; maxWidth: number;
maxHeight: number; maxHeight: number;
fillContainer?: boolean; fillContainer?: boolean;
elevation?: number; elevation?: number;
onOrderClicked?: (id: number) => void; onOrderClicked?: (id: number, shortAlias: string) => void;
} }
const DepthChart: React.FC<DepthChartProps> = ({ const DepthChart: React.FC<DepthChartProps> = ({
@ -42,10 +43,12 @@ const DepthChart: React.FC<DepthChartProps> = ({
elevation = 6, elevation = 6,
onOrderClicked = () => null, onOrderClicked = () => null,
}) => { }) => {
const { book, fav, info, limits, baseUrl } = useContext<UseAppStoreType>(AppContext); const { fav } = useContext<UseAppStoreType>(AppContext);
const { federation, coordinatorUpdatedAt, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const [enrichedOrders, setEnrichedOrders] = useState<Order[]>([]); const [enrichedOrders, setEnrichedOrders] = useState<PublicOrder[]>([]);
const [series, setSeries] = useState<Serie[]>([]); const [series, setSeries] = useState<Serie[]>([]);
const [rangeSteps, setRangeSteps] = useState<number>(8); const [rangeSteps, setRangeSteps] = useState<number>(8);
const [xRange, setXRange] = useState<number>(8); const [xRange, setXRange] = useState<number>(8);
@ -61,18 +64,21 @@ const DepthChart: React.FC<DepthChartProps> = ({
}, [fav.currency]); }, [fav.currency]);
useEffect(() => { useEffect(() => {
if (Object.keys(limits.list).length > 0) { if (federation.book.length > 0) {
const enriched = book.orders.map((order) => { const enriched = federation.book.map((order) => {
// We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate // We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate
// for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a // for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a
// simple rule of three // simple rule of three
order.base_amount = if (order.coordinatorShortAlias) {
(order.price * limits.list[currencyCode].price) / limits.list[order.currency].price; const limits = federation.getCoordinator(order.coordinatorShortAlias).limits;
order.base_amount =
(order.price * limits[currencyCode].price) / limits[order.currency].price;
}
return order; return order;
}); });
setEnrichedOrders(enriched); setEnrichedOrders(enriched);
} }
}, [limits.list, book.orders, currencyCode]); }, [coordinatorUpdatedAt, currencyCode]);
useEffect(() => { useEffect(() => {
if (enrichedOrders.length > 0) { if (enrichedOrders.length > 0) {
@ -82,10 +88,10 @@ const DepthChart: React.FC<DepthChartProps> = ({
useEffect(() => { useEffect(() => {
if (xType === 'base_amount') { if (xType === 'base_amount') {
const prices: number[] = enrichedOrders.map((order) => order?.base_amount || 0); const prices: number[] = enrichedOrders.map((order) => order?.base_amount ?? 0);
const medianValue = ~~matchMedian(prices); const medianValue = ~~matchMedian(prices);
const maxValue = prices.sort((a, b) => b - a).slice(0, 1)[0] || 1500; const maxValue = prices.sort((a, b) => b - a).slice(0, 1)[0] ?? 1500;
const maxRange = maxValue - medianValue; const maxRange = maxValue - medianValue;
const rangeSteps = maxRange / 10; const rangeSteps = maxRange / 10;
@ -93,35 +99,35 @@ const DepthChart: React.FC<DepthChartProps> = ({
setXRange(maxRange); setXRange(maxRange);
setRangeSteps(rangeSteps); setRangeSteps(rangeSteps);
} else { } else {
if (info.last_day_nonkyc_btc_premium === undefined) { if (federation.exchange.info?.last_day_nonkyc_btc_premium === undefined) {
const premiums: number[] = enrichedOrders.map((order) => order?.premium || 0); const premiums: number[] = enrichedOrders.map((order) => order?.premium ?? 0);
setCenter(~~matchMedian(premiums)); setCenter(~~matchMedian(premiums));
} else { } else {
setCenter(info.last_day_nonkyc_btc_premium); setCenter(federation.exchange.info?.last_day_nonkyc_btc_premium);
} }
setXRange(8); setXRange(8);
setRangeSteps(0.5); setRangeSteps(0.5);
} }
}, [enrichedOrders, xType, info.last_day_nonkyc_btc_premium, currencyCode]); }, [enrichedOrders, xType, federationUpdatedAt, currencyCode]);
const generateSeries: () => void = () => { const generateSeries: () => void = () => {
const sortedOrders: PublicOrder[] = const sortedOrders: PublicOrder[] =
xType === 'base_amount' xType === 'base_amount'
? enrichedOrders.sort( ? enrichedOrders.sort(
(order1, order2) => (order1?.base_amount || 0) - (order2?.base_amount || 0), (order1, order2) => (order1?.base_amount ?? 0) - (order2?.base_amount ?? 0),
) )
: enrichedOrders.sort((order1, order2) => order1.premium - order2.premium); : enrichedOrders.sort((order1, order2) => order1.premium - order2.premium);
const sortedBuyOrders: PublicOrder[] = sortedOrders const sortedBuyOrders: PublicOrder[] = sortedOrders
.filter((order) => order.type == 0) .filter((order) => order.type === 0)
.reverse(); .reverse();
const sortedSellOrders: PublicOrder[] = sortedOrders.filter((order) => order.type == 1); const sortedSellOrders: PublicOrder[] = sortedOrders.filter((order) => order.type === 1);
const buySerie: Datum[] = generateSerie(sortedBuyOrders); const buySerie: Datum[] = generateSerie(sortedBuyOrders);
const sellSerie: Datum[] = generateSerie(sortedSellOrders); const sellSerie: Datum[] = generateSerie(sortedSellOrders);
const maxX: number = (center || 0) + xRange; const maxX: number = (center ?? 0) + xRange;
const minX: number = (center || 0) - xRange; const minX: number = (center ?? 0) - xRange;
setSeries([ setSeries([
{ {
@ -136,7 +142,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
}; };
const generateSerie = (orders: PublicOrder[]): Datum[] => { const generateSerie = (orders: PublicOrder[]): Datum[] => {
if (center == undefined) { if (center === undefined) {
return []; return [];
} }
@ -144,7 +150,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
let serie: Datum[] = []; let serie: Datum[] = [];
orders.forEach((order) => { orders.forEach((order) => {
const lastSumOrders = sumOrders; const lastSumOrders = sumOrders;
sumOrders += (order.satoshis_now || 0) / 100000000; sumOrders += (order.satoshis_now ?? 0) / 100000000;
const datum: Datum[] = [ const datum: Datum[] = [
{ {
// Vertical Line // Vertical Line
@ -169,7 +175,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
}; };
const closeSerie = (serie: Datum[], limitBottom: number, limitTop: number): Datum[] => { const closeSerie = (serie: Datum[], limitBottom: number, limitTop: number): Datum[] => {
if (serie.length == 0) { if (serie.length === 0) {
return []; return [];
} }
@ -197,11 +203,11 @@ const DepthChart: React.FC<DepthChartProps> = ({
d={props.lineGenerator([ d={props.lineGenerator([
{ {
y: 0, y: 0,
x: props.xScale(center || 0), x: props.xScale(center ?? 0),
}, },
{ {
y: props.innerHeight, y: props.innerHeight,
x: props.xScale(center || 0), x: props.xScale(center ?? 0),
}, },
])} ])}
fill='none' fill='none'
@ -225,7 +231,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
}; };
const formatAxisY = (value: number): string => `${value}BTC`; const formatAxisY = (value: number): string => `${value}BTC`;
const handleOnClick: PointMouseHandler = (point: Point) => { const handleOnClick: PointMouseHandler = (point: Point) => {
onOrderClicked(point.data?.order?.id); onOrderClicked(point.data?.order?.id, point.data?.order?.coordinatorShortAlias);
}; };
const em = theme.typography.fontSize; const em = theme.typography.fontSize;
@ -239,7 +245,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
} }
> >
<Paper variant='outlined' style={{ width: '100%', height: '100%' }}> <Paper variant='outlined' style={{ width: '100%', height: '100%' }}>
{center == undefined || enrichedOrders.length < 1 ? ( {center === undefined || enrichedOrders.length < 1 ? (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@ -299,7 +305,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
<Grid item> <Grid item>
<Box justifyContent='center'> <Box justifyContent='center'>
{xType === 'base_amount' {xType === 'base_amount'
? `${center} ${currencyDict[currencyCode]}` ? `${center} ${String(currencyDict[currencyCode])}`
: `${center}%`} : `${center}%`}
</Box> </Box>
</Grid> </Grid>

View File

@ -12,9 +12,10 @@ import {
Tooltip, Tooltip,
} from '@mui/material'; } from '@mui/material';
import Map from '../../Map'; import Map from '../../Map';
import { AppContext, UseAppStoreType } from '../../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../../contexts/AppContext';
import { PhotoSizeSelectActual } from '@mui/icons-material'; import { PhotoSizeSelectActual } from '@mui/icons-material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FederationContext, UseFederationStoreType } from '../../../contexts/FederationContext';
interface MapChartProps { interface MapChartProps {
maxWidth: number; maxWidth: number;
@ -32,7 +33,7 @@ const MapChart: React.FC<MapChartProps> = ({
onOrderClicked = () => {}, onOrderClicked = () => {},
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { book } = useContext<UseAppStoreType>(AppContext); const { federation } = useContext<UseFederationStoreType>(FederationContext);
const [useTiles, setUseTiles] = useState<boolean>(false); const [useTiles, setUseTiles] = useState<boolean>(false);
const [acceptedTilesWarning, setAcceptedTilesWarning] = useState<boolean>(false); const [acceptedTilesWarning, setAcceptedTilesWarning] = useState<boolean>(false);
const [openWarningDialog, setOpenWarningDialog] = useState<boolean>(false); const [openWarningDialog, setOpenWarningDialog] = useState<boolean>(false);
@ -81,7 +82,7 @@ const MapChart: React.FC<MapChartProps> = ({
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Paper variant='outlined' style={{ width: '100%', height: '100%', justifyContent: 'center' }}> <Paper variant='outlined' style={{ width: '100%', height: '100%', justifyContent: 'center' }}>
{false ? ( {federation.book.length < 1 ? (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@ -127,7 +128,7 @@ const MapChart: React.FC<MapChartProps> = ({
</Tooltip> </Tooltip>
</Grid> </Grid>
<div style={{ height: `${height - 3.1}em` }}> <div style={{ height: `${height - 3.1}em` }}>
<Map useTiles={useTiles} orders={book.orders} onOrderClicked={onOrderClicked} /> <Map useTiles={useTiles} orders={federation.book} onOrderClicked={onOrderClicked} />
</div> </div>
</> </>
)} )}

View File

@ -6,17 +6,24 @@ import { amountToString, statusBadgeColor } from '../../../../utils';
import currencyDict from '../../../../../static/assets/currencies.json'; import currencyDict from '../../../../../static/assets/currencies.json';
import { PaymentStringAsIcons } from '../../../PaymentMethods'; import { PaymentStringAsIcons } from '../../../PaymentMethods';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AppContext, UseAppStoreType } from '../../../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../../../contexts/AppContext';
import { FederationContext, UseFederationStoreType } from '../../../../contexts/FederationContext';
interface OrderTooltipProps { interface OrderTooltipProps {
order: PublicOrder; order: PublicOrder;
} }
const OrderTooltip: React.FC<OrderTooltipProps> = ({ order }) => { const OrderTooltip: React.FC<OrderTooltipProps> = ({ order }) => {
const { baseUrl } = useContext<UseAppStoreType>(AppContext); const { settings, origin } = useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation(); const { t } = useTranslation();
return order ? ( const coordinatorAlias = order?.coordinatorShortAlias;
const network = settings.network;
const coordinator = federation.getCoordinator(coordinatorAlias);
const baseUrl = coordinator?.[network]?.[origin] ?? '';
return order?.id && baseUrl !== '' ? (
<Paper elevation={12} style={{ padding: 10, width: 250 }}> <Paper elevation={12} style={{ padding: 10, width: 250 }}>
<Grid container justifyContent='space-between'> <Grid container justifyContent='space-between'>
<Grid item xs={3}> <Grid item xs={3}>

View File

@ -22,7 +22,7 @@ interface Props {
onClose: () => void; onClose: () => void;
} }
const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => { const AboutDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -263,4 +263,4 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
); );
}; };
export default InfoDialog; export default AboutDialog;

View File

@ -23,7 +23,7 @@ import ContentCopy from '@mui/icons-material/ContentCopy';
import ForumIcon from '@mui/icons-material/Forum'; import ForumIcon from '@mui/icons-material/Forum';
import { ExportIcon, NewTabIcon } from '../Icons'; import { ExportIcon, NewTabIcon } from '../Icons';
function CredentialTextfield(props) { function CredentialTextfield(props): JSX.Element {
return ( return (
<Grid item align='center' xs={12}> <Grid item align='center' xs={12}>
<Tooltip placement='top' enterTouchDelay={200} enterDelay={200} title={props.tooltipTitle}> <Tooltip placement='top' enterTouchDelay={200} enterDelay={200} title={props.tooltipTitle}>
@ -58,9 +58,9 @@ interface Props {
onClose: () => void; onClose: () => void;
orderId: number; orderId: number;
messages: array; messages: array;
own_pub_key: string; ownPubKey: string;
own_enc_priv_key: string; ownEncPrivKey: string;
peer_pub_key: string; peerPubKey: string;
passphrase: string; passphrase: string;
onClickBack: () => void; onClickBack: () => void;
} }
@ -70,9 +70,9 @@ const AuditPGPDialog = ({
onClose, onClose,
orderId, orderId,
messages, messages,
own_pub_key, ownPubKey,
own_enc_priv_key, ownEncPrivKey,
peer_pub_key, peerPubKey,
passphrase, passphrase,
onClickBack, onClickBack,
}: Props): JSX.Element => { }: Props): JSX.Element => {
@ -104,7 +104,7 @@ const AuditPGPDialog = ({
'Your PGP public key. Your peer uses it to encrypt messages only you can read.', 'Your PGP public key. Your peer uses it to encrypt messages only you can read.',
)} )}
label={t('Your public key')} label={t('Your public key')}
value={own_pub_key} value={ownPubKey}
copiedTitle={t('Copied!')} copiedTitle={t('Copied!')}
/> />
@ -113,7 +113,7 @@ const AuditPGPDialog = ({
'Your peer PGP public key. You use it to encrypt messages only he can read and to verify your peer signed the incoming messages.', 'Your peer PGP public key. You use it to encrypt messages only he can read and to verify your peer signed the incoming messages.',
)} )}
label={t('Peer public key')} label={t('Peer public key')}
value={peer_pub_key} value={peerPubKey}
copiedTitle={t('Copied!')} copiedTitle={t('Copied!')}
/> />
@ -122,7 +122,7 @@ const AuditPGPDialog = ({
'Your encrypted private key. You use it to decrypt the messages that your peer encrypted for you. You also use it to sign the messages you send.', 'Your encrypted private key. You use it to decrypt the messages that your peer encrypted for you. You also use it to sign the messages you send.',
)} )}
label={t('Your encrypted private key')} label={t('Your encrypted private key')}
value={own_enc_priv_key} value={ownEncPrivKey}
copiedTitle={t('Copied!')} copiedTitle={t('Copied!')}
/> />
@ -149,10 +149,10 @@ const AuditPGPDialog = ({
color='primary' color='primary'
variant='contained' variant='contained'
onClick={() => { onClick={() => {
saveAsJson('keys_' + orderId + '.json', { saveAsJson(`keys_${orderId}.json`, {
own_public_key: own_pub_key, own_public_key: ownPubKey,
peer_public_key: peer_pub_key, peer_public_key: peerPubKey,
encrypted_private_key: own_enc_priv_key, encrypted_private_key: ownEncPrivKey,
passphrase, passphrase,
}); });
}} }}
@ -181,7 +181,7 @@ const AuditPGPDialog = ({
color='primary' color='primary'
variant='contained' variant='contained'
onClick={() => { onClick={() => {
saveAsJson('messages_' + orderId + '.json', messages); saveAsJson(`messages_${orderId}.json`, messages);
}} }}
> >
<div style={{ width: 28, height: 20 }}> <div style={{ width: 28, height: 20 }}>

View File

@ -0,0 +1,80 @@
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
Divider,
List,
ListItemText,
ListItem,
ListItemIcon,
Typography,
} from '@mui/material';
import BoltIcon from '@mui/icons-material/Bolt';
import PublicIcon from '@mui/icons-material/Public';
import FavoriteIcon from '@mui/icons-material/Favorite';
import { RoboSatsNoTextIcon } from '../Icons';
import { AppContext, type AppContextProps } from '../../contexts/AppContext';
interface Props {
open: boolean;
onClose: () => void;
}
const ClientDialog = ({ open = false, onClose }: Props): JSX.Element => {
const { t } = useTranslation();
const { clientVersion } = useContext<AppContextProps>(AppContext);
return (
<Dialog open={open} onClose={onClose}>
<DialogContent>
<Typography component='h5' variant='h5'>
{t('Client info')}
</Typography>
<List dense>
<Divider />
<ListItem>
<ListItemIcon>
<RoboSatsNoTextIcon
sx={{ width: '1.4em', height: '1.4em', right: '0.2em', position: 'relative' }}
/>
</ListItemIcon>
<ListItemText primary={clientVersion.long} secondary={t('RoboSats client version')} />
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<PublicIcon />
</ListItemIcon>
<ListItemText
primary={
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'left',
flexWrap: 'wrap',
}}
>
<span>{`${t('Made with')} `}</span>
<FavoriteIcon sx={{ color: '#ff0000', height: '22px', width: '22px' }} />
<span>{` ${t('and')} `}</span>
<BoltIcon sx={{ color: '#fcba03', height: '23px', width: '23px' }} />
</div>
}
secondary={t('... somewhere on Earth!')}
/>
</ListItem>
</List>
</DialogContent>
</Dialog>
);
};
export default ClientDialog;

View File

@ -156,7 +156,7 @@ const CommunityDialog = ({ open = false, onClose }: Props): JSX.Element => {
</ListItemIcon> </ListItemIcon>
<ListItemText secondary={t('We are abandoning Telegram! Our old TG groups')}> <ListItemText secondary={t('We are abandoning Telegram! Our old TG groups')}>
<Tooltip title={t('Join RoboSats Spanish speaking community!') || ''}> <Tooltip title={t('Join RoboSats Spanish speaking community!')}>
<IconButton <IconButton
component='a' component='a'
target='_blank' target='_blank'
@ -167,7 +167,7 @@ const CommunityDialog = ({ open = false, onClose }: Props): JSX.Element => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title={t('Join RoboSats English speaking community!') || ''}> <Tooltip title={t('Join RoboSats English speaking community!')}>
<IconButton <IconButton
component='a' component='a'
target='_blank' target='_blank'

View File

@ -0,0 +1,754 @@
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
Divider,
Grid,
List,
ListItemText,
ListItem,
ListItemIcon,
Typography,
IconButton,
Tooltip,
Link,
Box,
CircularProgress,
Accordion,
AccordionDetails,
AccordionSummary,
} from '@mui/material';
import {
Inventory,
Sell,
SmartToy,
Percent,
PriceChange,
Book,
Reddit,
Key,
Bolt,
Description,
Dns,
Email,
Equalizer,
ExpandMore,
GitHub,
Language,
Send,
Tag,
Twitter,
} from '@mui/icons-material';
import LinkIcon from '@mui/icons-material/Link';
import { pn } from '../../utils';
import { type Contact, type Coordinator } from '../../models';
import RobotAvatar from '../RobotAvatar';
import {
AmbossIcon,
BitcoinSignIcon,
RoboSatsNoTextIcon,
BadgeFounder,
BadgeDevFund,
BadgePrivacy,
BadgeLoved,
BadgeLimits,
NostrIcon,
} from '../Icons';
import { AppContext } from '../../contexts/AppContext';
import { systemClient } from '../../services/System';
import { type Badges } from '../../models/Coordinator.model';
interface Props {
open: boolean;
onClose: () => void;
coordinator: Coordinator | null;
network: 'mainnet' | 'testnet' | undefined;
}
const ContactButtons = ({
nostr,
pgp,
email,
telegram,
twitter,
matrix,
website,
reddit,
}: Contact): JSX.Element => {
const { t } = useTranslation();
const [showMatrix, setShowMatrix] = useState<boolean>(false);
const [showNostr, setShowNostr] = useState<boolean>(false);
return (
<Grid container direction='row' alignItems='center' justifyContent='center'>
{nostr !== undefined && (
<Grid item>
<Tooltip
title={
<div>
<Typography variant='body2'>
{t('...Opening on Nostr gateway. Pubkey copied!')}
</Typography>
<Typography variant='body2'>
<i>{nostr}</i>
</Typography>
</div>
}
open={showNostr}
>
<IconButton
onClick={() => {
setShowNostr(true);
setTimeout(() => window.open(`https://snort.social/p/${nostr}`, '_blank'), 1500);
setTimeout(() => {
setShowNostr(false);
}, 10000);
systemClient.copyToClipboard(nostr);
}}
>
<NostrIcon />
</IconButton>
</Tooltip>
</Grid>
)}
{pgp !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('See PGP Key')}>
<IconButton component='a' target='_blank' href={`https://${pgp}`} rel='noreferrer'>
<Key />
</IconButton>
</Tooltip>
</Grid>
)}
{email !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Send Email')}>
<IconButton component='a' href={`mailto: ${email}`}>
<Email />
</IconButton>
</Tooltip>
</Grid>
)}
{telegram !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Telegram')}>
<IconButton
component='a'
target='_blank'
href={`https://t.me/${telegram}`}
rel='noreferrer'
>
<Send />
</IconButton>
</Tooltip>
</Grid>
)}
{twitter !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Twitter')}>
<IconButton
component='a'
target='_blank'
href={`https://twitter.com/${twitter}`}
rel='noreferrer'
>
<Twitter />
</IconButton>
</Tooltip>
</Grid>
)}
{reddit !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Reddit')}>
<IconButton
component='a'
target='_blank'
href={`https://reddit.com/${reddit}`}
rel='noreferrer'
>
<Reddit />
</IconButton>
</Tooltip>
</Grid>
)}
{website !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Website')}>
<IconButton component='a' target='_blank' href={website} rel='noreferrer'>
<Language />
</IconButton>
</Tooltip>
</Grid>
)}
{matrix !== undefined && (
<Grid item>
<Tooltip
title={
<Typography variant='body2'>
{t('Matrix channel copied! {{matrix}}', { matrix })}
</Typography>
}
open={showMatrix}
>
<IconButton
onClick={() => {
setShowMatrix(true);
setTimeout(() => {
setShowMatrix(false);
}, 10000);
systemClient.copyToClipboard(matrix);
}}
>
<Tag />
</IconButton>
</Tooltip>
</Grid>
)}
</Grid>
);
};
interface BadgesProps {
badges: Badges | undefined;
}
const BadgesHall = ({ badges }: BadgesProps): JSX.Element => {
const { t } = useTranslation();
const sxProps = {
width: '3em',
height: '3em',
filter: 'drop-shadow(3px 3px 3px RGB(0,0,0,0.3))',
};
const tooltipProps = { enterTouchDelay: 0, enterNextDelay: 2000 };
return (
<Grid container direction='row' alignItems='center' justifyContent='center' spacing={1}>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.isFounder === true
? t('Founder: coordinating trades since the testnet federation.')
: t('Not a federation founder')}
</Typography>
}
>
<Grid item sx={{ filter: badges?.isFounder !== true ? 'grayscale(100%)' : undefined }}>
<BadgeFounder sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{t('Development fund supporter: donates {{percent}}% to make RoboSats better.', {
percent: badges?.donatesToDevFund,
})}
</Typography>
}
>
<Grid
item
sx={{ filter: Number(badges?.donatesToDevFund) >= 20 ? undefined : 'grayscale(100%)' }}
>
<BadgeDevFund sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.hasGoodOpSec === true
? t(
'Good OpSec: the coordinator follows best practices to protect his and your privacy.',
)
: t('The privacy practices of this coordinator could improve')}
</Typography>
}
>
<Grid item sx={{ filter: badges?.hasGoodOpSec === true ? undefined : 'grayscale(100%)' }}>
<BadgePrivacy sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.robotsLove === true
? t('Loved by robots: receives positive comments by robots over the internet.')
: t(
'The coordinator does not seem to receive exceptional love from robots over the internet',
)}
</Typography>
}
>
<Grid item sx={{ filter: badges?.robotsLove === true ? undefined : 'grayscale(100%)' }}>
<BadgeLoved sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.hasLargeLimits === true
? t('Large limits: the coordinator has large trade limits.')
: t('Does not have large trade limits.')}
</Typography>
}
>
<Grid item sx={{ filter: badges?.hasLargeLimits === true ? undefined : 'grayscale(100%)' }}>
<BadgeLimits sx={sxProps} />
</Grid>
</Tooltip>
</Grid>
);
};
const CoordinatorDialog = ({ open = false, onClose, coordinator, network }: Props): JSX.Element => {
const { t } = useTranslation();
const { clientVersion, page, hostUrl } = useContext(AppContext);
const [expanded, setExpanded] = useState<'summary' | 'stats' | 'policies' | undefined>(undefined);
const listItemProps = { sx: { maxHeight: '3em', width: '100%' } };
const coordinatorVersion = `v${coordinator?.info?.version?.major ?? '?'}.${
coordinator?.info?.version?.minor ?? '?'
}.${coordinator?.info?.version?.patch ?? '?'}`;
return (
<Dialog open={open} onClose={onClose}>
<DialogContent>
<Typography align='center' component='h5' variant='h5'>
{String(coordinator?.longAlias)}
</Typography>
<List dense>
<ListItem sx={{ display: 'flex', justifyContent: 'center' }}>
<Grid container direction='column' alignItems='center' padding={0}>
<Grid item>
<RobotAvatar
nickname={coordinator?.shortAlias}
coordinator={true}
style={{ width: '7.5em', height: '7.5em' }}
smooth={true}
flipHorizontally={false}
baseUrl={hostUrl}
/>
</Grid>
<Grid item>
<Typography align='center' variant='body2'>
<i>{String(coordinator?.motto)}</i>
</Typography>
</Grid>
<Grid item>
<ContactButtons {...coordinator?.contact} />
</Grid>
</Grid>
</ListItem>
{['create'].includes(page) && (
<>
<ListItem {...listItemProps}>
<ListItemIcon>
<Percent />
</ListItemIcon>
<Grid container>
<Grid item xs={6}>
<ListItemText secondary={t('Maker fee')}>
{(coordinator?.info?.maker_fee ?? 0 * 100).toFixed(3)}%
</ListItemText>
</Grid>
<Grid item xs={6}>
<ListItemText secondary={t('Taker fee')}>
{(coordinator?.info?.taker_fee ?? 0 * 100).toFixed(3)}%
</ListItemText>
</Grid>
</Grid>
</ListItem>
<ListItem {...listItemProps}>
<ListItemIcon>
<LinkIcon />
</ListItemIcon>
<ListItemText
primary={`${String(coordinator?.info?.current_swap_fee_rate.toPrecision(3))}%`}
secondary={t('Current onchain payout fee')}
/>
</ListItem>
</>
)}
<ListItem>
<BadgesHall badges={coordinator?.badges} />
</ListItem>
<ListItem>
<ListItemIcon>
<Description />
</ListItemIcon>
<ListItemText
primary={coordinator?.description}
primaryTypographyProps={{ sx: { maxWidth: '20em' } }}
secondary={t('Coordinator description')}
/>
</ListItem>
{coordinator?.mainnetNodesPubkeys?.[0] !== undefined && network === 'mainnet' ? (
<ListItem>
<ListItemIcon>
<AmbossIcon />
</ListItemIcon>
<ListItemText secondary={t('Mainnet LN Node')}>
<Link
target='_blank'
href={`https://amboss.space/node/${coordinator?.mainnetNodesPubkeys[0]}`}
rel='noreferrer'
>
{`${coordinator?.mainnetNodesPubkeys?.[0].slice(0, 12)}... (AMBOSS)`}
</Link>
</ListItemText>
</ListItem>
) : (
<></>
)}
{coordinator?.testnetNodesPubkeys?.[0] !== undefined && network === 'testnet' ? (
<ListItem>
<ListItemIcon>
<Dns />
</ListItemIcon>
<ListItemText secondary={t('Testnet LN Node')}>
<Link
target='_blank'
href={`https://1ml.com/testnet/node/${coordinator?.testnetNodesPubkeys[0]}`}
rel='noreferrer'
>
{`${coordinator?.testnetNodesPubkeys[0].slice(0, 12)}... (1ML)`}
</Link>
</ListItemText>
</ListItem>
) : (
<></>
)}
</List>
{coordinator?.loadingInfo === true ? (
<Box style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
) : coordinator?.info !== undefined ? (
<Box>
{coordinator?.policies !== undefined && (
<Accordion
expanded={expanded === 'policies'}
onChange={() => {
setExpanded(expanded === 'policies' ? undefined : 'policies');
}}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography>{t('Policies')}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ padding: 0 }}>
<List dense>
{Object.keys(coordinator?.policies).map((key, index) => (
<ListItem key={index} sx={{ maxWidth: '24em' }}>
<ListItemIcon>{index + 1}</ListItemIcon>
<ListItemText primary={key} secondary={coordinator?.policies[key]} />
</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
)}
<Accordion
expanded={expanded === 'summary'}
onChange={() => {
setExpanded(expanded === 'summary' ? undefined : 'summary');
}}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography>{t('Summary')}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ padding: 0 }}>
<List dense>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Percent />
</ListItemIcon>
<Grid container>
<Grid item xs={6}>
<ListItemText secondary={t('Maker fee')}>
{(coordinator?.info?.maker_fee * 100).toFixed(3)}%
</ListItemText>
</Grid>
<Grid item xs={6}>
<ListItemText secondary={t('Taker fee')}>
{(coordinator?.info?.taker_fee * 100).toFixed(3)}%
</ListItemText>
</Grid>
</Grid>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<LinkIcon />
</ListItemIcon>
<ListItemText
primary={`${coordinator?.info?.current_swap_fee_rate.toPrecision(3)}%`}
secondary={t('Current onchain payout fee')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Inventory />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.num_public_buy_orders}
secondary={t('Public buy orders')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Sell />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.num_public_sell_orders}
secondary={t('Public sell orders')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Book />
</ListItemIcon>
<ListItemText
primary={`${pn(coordinator?.info?.book_liquidity)} Sats`}
secondary={t('Book liquidity')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<SmartToy />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.active_robots_today}
secondary={t('Today active robots')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<PriceChange />
</ListItemIcon>
<ListItemText
primary={`${coordinator?.info?.last_day_nonkyc_btc_premium}%`}
secondary={t('24h non-KYC bitcoin premium')}
/>
</ListItem>
</List>
</AccordionDetails>
</Accordion>
<Accordion
expanded={expanded === 'stats'}
onChange={() => {
setExpanded(expanded === 'stats' ? undefined : 'stats');
}}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography>{t('Stats for Nerds')}</Typography>
</AccordionSummary>
<AccordionDetails>
<List dense>
<ListItem {...listItemProps}>
<ListItemIcon>
<RoboSatsNoTextIcon
sx={{
width: '1.4em',
height: '1.4em',
right: '0.2em',
position: 'relative',
}}
/>
</ListItemIcon>
<ListItemText
primary={`${t('Coordinator')} ${coordinatorVersion} - ${t('Client')} ${String(
clientVersion.short,
)}`}
secondary={t('RoboSats version')}
/>
</ListItem>
<Divider />
{coordinator?.info?.lnd_version !== undefined && (
<ListItem {...listItemProps}>
<ListItemIcon>
<Bolt />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.lnd_version}
secondary={t('LND version')}
/>
</ListItem>
)}
{Boolean(coordinator?.info?.cln_version) && (
<ListItem {...listItemProps}>
<ListItemIcon>
<Bolt />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.cln_version}
secondary={t('CLN version')}
/>
</ListItem>
)}
<Divider />
{coordinator?.info?.network === 'testnet' ? (
<ListItem {...listItemProps}>
<ListItemIcon>
<Dns />
</ListItemIcon>
<ListItemText secondary={`${t('LN Node')}: ${coordinator?.info?.node_alias}`}>
<Link
target='_blank'
href={`https://1ml.com/testnet/node/${coordinator?.info?.node_id}`}
rel='noreferrer'
>
{`${coordinator?.info?.node_id.slice(0, 12)}... (1ML)`}
</Link>
</ListItemText>
</ListItem>
) : (
<ListItem {...listItemProps}>
<ListItemIcon>
<AmbossIcon />
</ListItemIcon>
<ListItemText secondary={coordinator?.info?.node_alias}>
<Link
target='_blank'
href={`https://amboss.space/node/${coordinator?.info?.node_id}`}
rel='noreferrer'
>
{`${coordinator?.info?.node_id.slice(0, 12)}... (AMBOSS)`}
</Link>
</ListItemText>
</ListItem>
)}
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<GitHub />
</ListItemIcon>
<ListItemText secondary={t('Coordinator commit hash')}>
<Link
target='_blank'
href={`https://github.com/Reckless-Satoshi/robosats/tree/${coordinator?.info?.robosats_running_commit_hash}`}
rel='noreferrer'
>
{`${coordinator?.info?.robosats_running_commit_hash.slice(0, 12)}...`}
</Link>
</ListItemText>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Equalizer />
</ListItemIcon>
<ListItemText secondary={t('24h contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(parseFloat(coordinator?.info?.last_day_volume).toFixed(8))}
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
</div>
</ListItemText>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Equalizer />
</ListItemIcon>
<ListItemText secondary={t('Lifetime contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(parseFloat(coordinator?.info?.lifetime_volume).toFixed(8))}
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
</div>
</ListItemText>
</ListItem>
</List>
</AccordionDetails>
</Accordion>
</Box>
) : (
<Typography align='center' variant='h6' color='error'>
{t('Coordinator offline')}
</Typography>
)}
</DialogContent>
</Dialog>
);
};
export default CoordinatorDialog;

View File

@ -1,174 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
Divider,
Grid,
List,
ListItemText,
ListItem,
ListItemIcon,
Typography,
LinearProgress,
} from '@mui/material';
import InventoryIcon from '@mui/icons-material/Inventory';
import SellIcon from '@mui/icons-material/Sell';
import SmartToyIcon from '@mui/icons-material/SmartToy';
import PercentIcon from '@mui/icons-material/Percent';
import PriceChangeIcon from '@mui/icons-material/PriceChange';
import BookIcon from '@mui/icons-material/Book';
import LinkIcon from '@mui/icons-material/Link';
import { pn } from '../../utils';
import { type Info } from '../../models';
interface Props {
open: boolean;
onClose: () => void;
info: Info;
}
const CoordinatorSummaryDialog = ({ open = false, onClose, info }: Props): JSX.Element => {
const { t } = useTranslation();
if (info.current_swap_fee_rate === null || info.current_swap_fee_rate === undefined) {
info.current_swap_fee_rate = 0;
}
return (
<Dialog open={open} onClose={onClose}>
<div style={info.loading ? {} : { display: 'none' }}>
<LinearProgress />
</div>
<DialogContent>
<Typography component='h5' variant='h5'>
{t('Coordinator Summary')}
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<InventoryIcon />
</ListItemIcon>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
primary={info.num_public_buy_orders}
secondary={t('Public buy orders')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<SellIcon />
</ListItemIcon>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
primary={info.num_public_sell_orders}
secondary={t('Public sell orders')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<BookIcon />
</ListItemIcon>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
primary={`${pn(info.book_liquidity)} Sats`}
secondary={t('Book liquidity')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<SmartToyIcon />
</ListItemIcon>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
primary={info.active_robots_today}
secondary={t('Today active robots')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<PriceChangeIcon />
</ListItemIcon>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
primary={`${info.last_day_nonkyc_btc_premium}%`}
secondary={t('Last 24h mean premium')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<PercentIcon />
</ListItemIcon>
<Grid container>
<Grid item xs={6}>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
secondary={t('Maker fee')}
>
{(info.maker_fee * 100).toFixed(3)}%
</ListItemText>
</Grid>
<Grid item xs={6}>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
secondary={t('Taker fee')}
>
{(info.taker_fee * 100).toFixed(3)}%
</ListItemText>
</Grid>
</Grid>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<LinkIcon />
</ListItemIcon>
<ListItemText
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
primary={`${info.current_swap_fee_rate.toPrecision(3)}%`}
secondary={t('Current onchain payout fee')}
/>
</ListItem>
</List>
</DialogContent>
</Dialog>
);
};
export default CoordinatorSummaryDialog;

View File

@ -27,12 +27,12 @@ const EnableTelegramDialog = ({ open, onClose, tgBotName, tgToken }: Props): JSX
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const handleClickOpenBrowser = () => { const handleClickOpenBrowser = (): void => {
window.open(`https://t.me/${tgBotName}?start=${tgToken}`, '_blank').focus(); window.open(`https://t.me/${tgBotName}?start=${tgToken}`, '_blank').focus();
setOpenEnableTelegram(false); setOpenEnableTelegram(false);
}; };
const handleOpenTG = () => { const handleOpenTG = (): void => {
window.open(`tg://resolve?domain=${tgBotName}&start=${tgToken}`); window.open(`tg://resolve?domain=${tgBotName}&start=${tgToken}`);
}; };

View File

@ -0,0 +1,190 @@
import React, { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
Divider,
List,
ListItemText,
ListItem,
ListItemIcon,
Typography,
LinearProgress,
} from '@mui/material';
import {
Inventory,
Sell,
SmartToy,
PriceChange,
Book,
Groups3,
Equalizer,
} from '@mui/icons-material';
import { pn } from '../../utils';
import { BitcoinSignIcon } from '../Icons';
import { FederationContext } from '../../contexts/FederationContext';
interface Props {
open: boolean;
onClose: () => void;
}
const ExchangeDialog = ({ open = false, onClose }: Props): JSX.Element => {
const { t } = useTranslation();
const { federation } = useContext(FederationContext);
const loadingProgress = useMemo(() => {
return (federation.exchange.onlineCoordinators / federation.exchange.totalCoordinators) * 100;
}, [federation.exchange.onlineCoordinators, federation.exchange.totalCoordinators]);
return (
<Dialog open={open} onClose={onClose}>
<div style={loadingProgress < 100 ? {} : { display: 'none' }}>
<LinearProgress variant='determinate' value={loadingProgress} />
</div>
<DialogContent>
<Typography component='h5' variant='h5'>
{t('Exchange Summary')}
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<Groups3 />
</ListItemIcon>
<ListItemText
primary={federation.exchange.onlineCoordinators}
secondary={t('Online RoboSats coordinators')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<Groups3 />
</ListItemIcon>
<ListItemText
primary={federation.exchange.totalCoordinators}
secondary={t('Enabled RoboSats coordinators')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<Inventory />
</ListItemIcon>
<ListItemText
primary={federation.exchange.info.num_public_buy_orders}
secondary={t('Public buy orders')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<Sell />
</ListItemIcon>
<ListItemText
primary={federation.exchange.info.num_public_sell_orders}
secondary={t('Public sell orders')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<Book />
</ListItemIcon>
<ListItemText
primary={`${pn(federation.exchange.info.book_liquidity)} Sats`}
secondary={t('Book liquidity')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<SmartToy />
</ListItemIcon>
<ListItemText
primary={federation.exchange.info.active_robots_today}
secondary={t('Today active robots')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<PriceChange />
</ListItemIcon>
<ListItemText
primary={`${String(
federation.exchange.info.last_day_nonkyc_btc_premium.toPrecision(3),
)}%`}
secondary={t('24h non-KYC bitcoin premium')}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<Equalizer />
</ListItemIcon>
<ListItemText secondary={t('24h contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(federation.exchange.info.last_day_volume)}
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color='text.secondary' />
</div>
</ListItemText>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<Equalizer />
</ListItemIcon>
<ListItemText secondary={t('Lifetime contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(federation.exchange.info.lifetime_volume)}
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color='text.secondary' />
</div>
</ListItemText>
</ListItem>
</List>
</DialogContent>
</Dialog>
);
};
export default ExchangeDialog;

View File

@ -31,7 +31,7 @@ const F2fMapDialog = ({
onClose = () => {}, onClose = () => {},
latitude, latitude,
longitude, longitude,
interactive, interactive = false,
zoom, zoom,
message = '', message = '',
}: Props): JSX.Element => { }: Props): JSX.Element => {
@ -41,8 +41,8 @@ const F2fMapDialog = ({
const [acceptedTilesWarning, setAcceptedTilesWarning] = useState<boolean>(false); const [acceptedTilesWarning, setAcceptedTilesWarning] = useState<boolean>(false);
const [openWarningDialog, setOpenWarningDialog] = useState<boolean>(false); const [openWarningDialog, setOpenWarningDialog] = useState<boolean>(false);
const onSave = () => { const onSave: () => void = () => {
if (position && position[0] && position[1]) { if (position?.[0] && position?.[1]) {
onClose([position[0] + Math.random() * 0.1 - 0.05, position[1] + Math.random() * 0.1 - 0.05]); onClose([position[0] + Math.random() * 0.1 - 0.05, position[1] + Math.random() * 0.1 - 0.05]);
} }
}; };
@ -59,7 +59,9 @@ const F2fMapDialog = ({
<Dialog <Dialog
open={open} open={open}
fullWidth fullWidth
onClose={() => onClose()} onClose={() => {
onClose();
}}
aria-labelledby='worldmap-dialog-title' aria-labelledby='worldmap-dialog-title'
aria-describedby='worldmap-description' aria-describedby='worldmap-description'
maxWidth={false} maxWidth={false}
@ -154,15 +156,22 @@ const F2fMapDialog = ({
</Grid> </Grid>
<Grid item> <Grid item>
{interactive ? ( {interactive ? (
<Button color='primary' variant='contained' onClick={onSave} disabled={!position}> <Button
color='primary'
variant='contained'
onClick={onSave}
disabled={position == null}
>
{t('Save')} {t('Save')}
</Button> </Button>
) : ( ) : (
<Button <Button
color='primary' color='primary'
variant='contained' variant='contained'
onClick={() => onClose()} onClick={() => {
disabled={!position} onClose();
}}
disabled={position == null}
> >
{t('Close')} {t('Close')}
</Button> </Button>

View File

@ -1,126 +1,40 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useTheme } from '@mui/material/styles';
import { useNavigate } from 'react-router-dom';
import { import {
Badge,
Button,
CircularProgress,
Dialog, Dialog,
DialogContent, DialogContent,
Divider, Divider,
FormControlLabel,
Grid,
List, List,
ListItemAvatar, ListItemAvatar,
ListItemButton,
ListItemText, ListItemText,
ListItem, ListItem,
ListItemIcon,
Switch,
TextField,
Tooltip,
Typography, Typography,
LinearProgress, LinearProgress,
} from '@mui/material'; } from '@mui/material';
import { EnableTelegramDialog } from '.';
import BoltIcon from '@mui/icons-material/Bolt'; import BoltIcon from '@mui/icons-material/Bolt';
import SendIcon from '@mui/icons-material/Send';
import NumbersIcon from '@mui/icons-material/Numbers';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import { UserNinjaIcon } from '../Icons';
import { getWebln } from '../../utils';
import RobotAvatar from '../RobotAvatar'; import RobotAvatar from '../RobotAvatar';
import { apiClient } from '../../services/api'; import RobotInfo from '../RobotInfo';
import { type Robot } from '../../models'; import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { signCleartextMessage } from '../../pgp'; import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { Coordinator } from '../../models';
interface Props { interface Props {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
robot: Robot;
setRobot: (state: Robot) => void;
baseUrl: string; baseUrl: string;
} }
const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Props): JSX.Element => { const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element => {
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const [loading, setLoading] = useState<boolean>(true);
const theme = useTheme();
const [rewardInvoice, setRewardInvoice] = useState<string>('');
const [showRewardsSpinner, setShowRewardsSpinner] = useState<boolean>(false);
const [withdrawn, setWithdrawn] = useState<boolean>(false);
const [badInvoice, setBadInvoice] = useState<string>('');
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
const handleWebln = async () => {
const webln = await getWebln()
.then(() => {
setWeblnEnabled(true);
})
.catch(() => {
setWeblnEnabled(false);
console.log('WebLN not available');
});
return webln;
};
useEffect(() => { useEffect(() => {
handleWebln(); setLoading(garage.getRobot().loading);
}, []); }, [robotUpdatedAt]);
const handleWeblnInvoiceClicked = async (e: any) => {
e.preventDefault();
if (robot.earnedRewards) {
const webln = await getWebln();
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
if (invoice) {
handleSubmitInvoiceClicked(e, invoice.paymentRequest);
}
});
}
};
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string) => {
setBadInvoice('');
setShowRewardsSpinner(true);
signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then((signedInvoice) => {
apiClient
.post(
baseUrl,
'/api/reward/',
{
invoice: signedInvoice,
},
{ tokenSHA256: robot.tokenSHA256 },
)
.then((data: any) => {
setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal);
setOpenClaimRewards(!data.successful_withdrawal);
setRobot({
...robot,
earnedRewards: data.successful_withdrawal ? 0 : robot.earnedRewards,
});
});
});
e.preventDefault();
};
const setStealthInvoice = (wantsStealth: boolean) => {
apiClient
.post(baseUrl, '/api/stealth/', { wantsStealth }, { tokenSHA256: robot.tokenSHA256 })
.then((data) => {
setRobot({ ...robot, stealthInvoices: data?.wantsStealth });
});
};
return ( return (
<Dialog <Dialog
@ -129,7 +43,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
aria-labelledby='profile-title' aria-labelledby='profile-title'
aria-describedby='profile-description' aria-describedby='profile-description'
> >
<div style={robot.loading ? {} : { display: 'none' }}> <div style={loading ? {} : { display: 'none' }}>
<LinearProgress /> <LinearProgress />
</div> </div>
<DialogContent> <DialogContent>
@ -143,7 +57,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
<ListItem className='profileNickname'> <ListItem className='profileNickname'>
<ListItemText secondary={t('Your robot')}> <ListItemText secondary={t('Your robot')}>
<Typography component='h6' variant='h6'> <Typography component='h6' variant='h6'>
{robot.nickname ? ( {garage.getRobot().nickname !== undefined && (
<div style={{ position: 'relative', left: '-7px' }}> <div style={{ position: 'relative', left: '-7px' }}>
<div <div
style={{ style={{
@ -156,12 +70,12 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
> >
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
<a>{robot.nickname}</a> <a>{garage.getRobot().nickname}</a>
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
</div> </div>
</div> </div>
) : null} )}
</Typography> </Typography>
</ListItemText> </ListItemText>
@ -169,219 +83,35 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
<RobotAvatar <RobotAvatar
avatarClass='profileAvatar' avatarClass='profileAvatar'
style={{ width: 65, height: 65 }} style={{ width: 65, height: 65 }}
nickname={robot.nickname} nickname={garage.getRobot().nickname}
baseUrl={baseUrl} baseUrl={baseUrl}
/> />
</ListItemAvatar> </ListItemAvatar>
</ListItem> </ListItem>
<Divider /> <Divider />
{robot.activeOrderId ? (
<ListItemButton
onClick={() => {
navigate(`/order/${robot.activeOrderId}`);
onClose();
}}
>
<ListItemIcon>
<Badge badgeContent='' color='primary'>
<NumbersIcon color='primary' />
</Badge>
</ListItemIcon>
<ListItemText
primary={t('One active order #{{orderID}}', { orderID: robot.activeOrderId })}
secondary={t('Your current order')}
/>
</ListItemButton>
) : robot.lastOrderId ? (
<ListItemButton
onClick={() => {
navigate(`/order/${robot.lastOrderId}`);
onClose();
}}
>
<ListItemIcon>
<NumbersIcon color='primary' />
</ListItemIcon>
<ListItemText
primary={t('Your last order #{{orderID}}', { orderID: robot.lastOrderId })}
secondary={t('Inactive order')}
/>
</ListItemButton>
) : (
<ListItem>
<ListItemIcon>
<NumbersIcon />
</ListItemIcon>
<ListItemText
primary={t('No active orders')}
secondary={t('You do not have previous orders')}
/>
</ListItem>
)}
<Divider />
<EnableTelegramDialog
open={openEnableTelegram}
onClose={() => {
setOpenEnableTelegram(false);
}}
tgBotName={robot.tgBotName}
tgToken={robot.tgToken}
/>
<ListItem>
<ListItemIcon>
<SendIcon />
</ListItemIcon>
<ListItemText>
{robot.tgEnabled ? (
<Typography color={theme.palette.success.main}>
<b>{t('Telegram enabled')}</b>
</Typography>
) : (
<Button
color='primary'
onClick={() => {
setOpenEnableTelegram(true);
}}
>
{t('Enable Telegram Notifications')}
</Button>
)}
</ListItemText>
</ListItem>
<ListItem>
<ListItemIcon>
<UserNinjaIcon />
</ListItemIcon>
<ListItemText>
<Tooltip
placement='bottom'
enterTouchDelay={0}
title={t(
"Stealth lightning invoices do not contain details about the trade except an order reference. Enable this setting if you don't want to disclose details to a custodial lightning wallet.",
)}
>
<Grid item>
<FormControlLabel
labelPlacement='end'
label={t('Use stealth invoices')}
control={
<Switch
checked={robot.stealthInvoices}
onChange={() => {
setStealthInvoice(!robot.stealthInvoices);
}}
/>
}
/>
</Grid>
</Tooltip>
</ListItemText>
</ListItem>
<ListItem>
<ListItemIcon>
<EmojiEventsIcon />
</ListItemIcon>
{!openClaimRewards ? (
<ListItemText secondary={t('Your earned rewards')}>
<Grid container>
<Grid item xs={9}>
<Typography>{`${robot.earnedRewards} Sats`}</Typography>
</Grid>
<Grid item xs={3}>
<Button
disabled={robot.earnedRewards === 0}
onClick={() => {
setOpenClaimRewards(true);
}}
variant='contained'
size='small'
>
{t('Claim')}
</Button>
</Grid>
</Grid>
</ListItemText>
) : (
<form noValidate style={{ maxWidth: 270 }}>
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
<Grid item style={{ display: 'flex', maxWidth: 160 }}>
<TextField
error={!!badInvoice}
helperText={badInvoice || ''}
label={t('Invoice for {{amountSats}} Sats', {
amountSats: robot.earnedRewards,
})}
size='small'
value={rewardInvoice}
onChange={(e) => {
setRewardInvoice(e.target.value);
}}
/>
</Grid>
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 80 }}>
<Button
sx={{ maxHeight: 38 }}
onClick={(e) => {
handleSubmitInvoiceClicked(e, rewardInvoice);
}}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Submit')}
</Button>
</Grid>
</Grid>
{weblnEnabled ? (
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 240 }}>
<Button
sx={{ maxHeight: 38, minWidth: 230 }}
onClick={async (e) => {
await handleWeblnInvoiceClicked(e);
}}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Generate with Webln')}
</Button>
</Grid>
</Grid>
) : (
<></>
)}
</form>
)}
</ListItem>
{showRewardsSpinner && (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</div>
)}
{withdrawn && (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Typography color='primary' variant='body2'>
<b>{t('There it goes, thank you!🥇')}</b>
</Typography>
</div>
)}
</List> </List>
<Typography>
<b>{t('Coordinators that know your robots')}</b>
</Typography>
{Object.values(federation.coordinators).map((coordinator: Coordinator): JSX.Element => {
if (garage.getRobot()?.loading === false) {
return (
<div key={coordinator.shortAlias}>
<RobotInfo
coordinator={coordinator}
robot={garage.getRobot()}
slotIndex={garage.currentSlot}
onClose={onClose}
/>
</div>
);
} else {
return <div key={coordinator.shortAlias} />;
}
})}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -1,225 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
Divider,
Link,
List,
ListItemText,
ListItem,
ListItemIcon,
Typography,
LinearProgress,
} from '@mui/material';
import BoltIcon from '@mui/icons-material/Bolt';
import PublicIcon from '@mui/icons-material/Public';
import DnsIcon from '@mui/icons-material/Dns';
import WebIcon from '@mui/icons-material/Web';
import FavoriteIcon from '@mui/icons-material/Favorite';
import GitHubIcon from '@mui/icons-material/GitHub';
import EqualizerIcon from '@mui/icons-material/Equalizer';
import { AmbossIcon, BitcoinSignIcon, RoboSatsNoTextIcon } from '../Icons';
import { pn } from '../../utils';
import { type Info } from '../../models';
interface Props {
open: boolean;
onClose: () => void;
info: Info;
}
const StatsDialog = ({ open = false, onClose, info }: Props): JSX.Element => {
const { t } = useTranslation();
const isOnionAccess = window.location.hostname.endsWith('.onion');
const ambossURL = isOnionAccess
? `http://amboss5jfdzzblty5dr5zaig5twvkgsla6y5xuy6s5c5ogpjfcqgltid.onion/node/${info.node_id}`
: `https://amboss.space/node/${info.node_id}`;
return (
<Dialog open={open} onClose={onClose}>
<div style={info.loading ? {} : { display: 'none' }}>
<LinearProgress />
</div>
<DialogContent>
<Typography component='h5' variant='h5'>
{t('Stats For Nerds')}
</Typography>
<List dense>
<Divider />
<ListItem>
<ListItemIcon>
<RoboSatsNoTextIcon
sx={{ width: '1.4em', height: '1.4em', right: '0.2em', position: 'relative' }}
/>
</ListItemIcon>
<ListItemText
primary={`${t('Client')} ${info.clientVersion} - ${t('Coordinator')} ${
info.coordinatorVersion
}`}
secondary={t('RoboSats version')}
/>
</ListItem>
<Divider />
{info.lnd_version ? (
<ListItem>
<ListItemIcon>
<BoltIcon />
</ListItemIcon>
<ListItemText primary={info.lnd_version} secondary={t('LND version')} />
</ListItem>
) : null}
{info.cln_version ? (
<ListItem>
<ListItemIcon>
<BoltIcon />
</ListItemIcon>
<ListItemText primary={info.cln_version} secondary={t('CLN version')} />
</ListItem>
) : null}
<Divider />
{info.network === 'testnet' ? (
<ListItem>
<ListItemIcon>
<DnsIcon />
</ListItemIcon>
<ListItemText secondary={`${t('LN Node')}: ${info.node_alias}`}>
<Link
target='_blank'
href={`https://1ml.com/testnet/node/${info.node_id}`}
rel='noreferrer'
>
{`${info.node_id.slice(0, 12)}... (1ML)`}
</Link>
</ListItemText>
</ListItem>
) : (
<ListItem>
<ListItemIcon>
<AmbossIcon />
</ListItemIcon>
<ListItemText secondary={info.node_alias}>
<Link target='_blank' href={ambossURL} rel='noreferrer'>
{`${info.node_id.slice(0, 12)}... (AMBOSS)`}
</Link>
</ListItemText>
</ListItem>
)}
<Divider />
<ListItem>
<ListItemIcon>
<WebIcon />
</ListItemIcon>
<ListItemText secondary={info.alternative_name}>
<Link target='_blank' href={`http://${info.alternative_site}`} rel='noreferrer'>
{`${info.alternative_site.slice(0, 12)}...onion`}
</Link>
</ListItemText>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<GitHubIcon />
</ListItemIcon>
<ListItemText secondary={t('Coordinator commit hash')}>
<Link
target='_blank'
href={`https://github.com/RoboSats/robosats/tree/${info.robosats_running_commit_hash}`}
rel='noreferrer'
>
{`${info.robosats_running_commit_hash.slice(0, 12)}...`}
</Link>
</ListItemText>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<EqualizerIcon />
</ListItemIcon>
<ListItemText secondary={t('24h contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(info.last_day_volume)}
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
</div>
</ListItemText>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<EqualizerIcon />
</ListItemIcon>
<ListItemText secondary={t('Lifetime contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(info.lifetime_volume)}
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
</div>
</ListItemText>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<PublicIcon />
</ListItemIcon>
<ListItemText
primary={
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'left',
flexWrap: 'wrap',
}}
>
<span>{`${t('Made with')} `}</span>
<FavoriteIcon sx={{ color: '#ff0000', height: '22px', width: '22px' }} />
<span>{` ${t('and')} `}</span>
<BoltIcon sx={{ color: '#fcba03', height: '23px', width: '23px' }} />
</div>
}
secondary={t('... somewhere on Earth!')}
/>
</ListItem>
</List>
</DialogContent>
</Dialog>
);
};
export default StatsDialog;

View File

@ -14,7 +14,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { systemClient } from '../../services/System'; import { systemClient } from '../../services/System';
import ContentCopy from '@mui/icons-material/ContentCopy'; import ContentCopy from '@mui/icons-material/ContentCopy';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
interface Props { interface Props {
open: boolean; open: boolean;
@ -24,7 +24,7 @@ interface Props {
} }
const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): JSX.Element => { const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): JSX.Element => {
const { robot } = useContext<UseAppStoreType>(AppContext); const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -43,7 +43,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
sx={{ width: '100%', maxWidth: '550px' }} sx={{ width: '100%', maxWidth: '550px' }}
disabled disabled
label={t('Back it up!')} label={t('Back it up!')}
value={robot.token} value={garage.getRobot().token}
variant='filled' variant='filled'
size='small' size='small'
InputProps={{ InputProps={{
@ -51,7 +51,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}> <Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton <IconButton
onClick={() => { onClick={() => {
systemClient.copyToClipboard(robot.token); systemClient.copyToClipboard(garage.getRobot().token);
}} }}
> >
<ContentCopy color='primary' /> <ContentCopy color='primary' />

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@ -17,24 +17,32 @@ import {
import WebIcon from '@mui/icons-material/Web'; import WebIcon from '@mui/icons-material/Web';
import AndroidIcon from '@mui/icons-material/Android'; import AndroidIcon from '@mui/icons-material/Android';
import UpcomingIcon from '@mui/icons-material/Upcoming'; import UpcomingIcon from '@mui/icons-material/Upcoming';
import { checkVer } from '../../utils';
import { type Version } from '../../models';
interface Props { interface Props {
open: boolean; coordinatorVersion: Version;
clientVersion: string; clientVersion: Version;
coordinatorVersion: string;
onClose: () => void; onClose: () => void;
} }
const UpdateClientDialog = ({ const UpdateDialog = ({ coordinatorVersion, clientVersion }: Props): JSX.Element => {
open = false,
clientVersion,
coordinatorVersion,
onClose,
}: Props): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState<boolean>(() => checkVer(coordinatorVersion));
const coordinatorString = `v${coordinatorVersion.major}-${coordinatorVersion.minor}-${coordinatorVersion.patch}`;
const clientString = `v${clientVersion.major}-${clientVersion.minor}-${clientVersion.patch}`;
useEffect(() => {
setOpen(checkVer(coordinatorVersion));
}, [coordinatorVersion]);
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog
open={open}
onClose={() => {
setOpen(false);
}}
>
<DialogContent> <DialogContent>
<Typography component='h5' variant='h5'> <Typography component='h5' variant='h5'>
{t('Update your RoboSats client')} {t('Update your RoboSats client')}
@ -45,7 +53,7 @@ const UpdateClientDialog = ({
<Typography> <Typography>
{t( {t(
'The RoboSats coordinator is on version {{coordinatorVersion}}, but your client app is {{clientVersion}}. This version mismatch might lead to a bad user experience.', 'The RoboSats coordinator is on version {{coordinatorVersion}}, but your client app is {{clientVersion}}. This version mismatch might lead to a bad user experience.',
{ coordinatorVersion, clientVersion }, { coordinatorString, clientString },
)} )}
</Typography> </Typography>
@ -53,7 +61,7 @@ const UpdateClientDialog = ({
<ListItemButton <ListItemButton
component='a' component='a'
target='_blank' target='_blank'
href={`https://github.com/RoboSats/robosats/releases/tag/${coordinatorVersion}-alpha`} href={`https://github.com/RoboSats/robosats/releases/tag/${coordinatorString}-alpha`}
rel='noreferrer' rel='noreferrer'
> >
<ListItemIcon> <ListItemIcon>
@ -107,7 +115,13 @@ const UpdateClientDialog = ({
</ListItemButton> </ListItemButton>
<DialogActions> <DialogActions>
<Button onClick={onClose}>{t('Go away!')}</Button> <Button
onClick={() => {
setOpen(false);
}}
>
{t('Go away!')}
</Button>
</DialogActions> </DialogActions>
</List> </List>
</DialogContent> </DialogContent>
@ -115,4 +129,4 @@ const UpdateClientDialog = ({
); );
}; };
export default UpdateClientDialog; export default UpdateDialog;

View File

@ -1,14 +1,15 @@
export { default as AuditPGPDialog } from './AuditPGP'; export { default as AuditPGPDialog } from './AuditPGP';
export { default as CommunityDialog } from './Community'; export { default as CommunityDialog } from './Community';
export { default as InfoDialog } from './Info'; export { default as AboutDialog } from './About';
export { default as LearnDialog } from './Learn'; export { default as LearnDialog } from './Learn';
export { default as NoRobotDialog } from './NoRobot'; export { default as NoRobotDialog } from './NoRobot';
export { default as StoreTokenDialog } from './StoreToken'; export { default as StoreTokenDialog } from './StoreToken';
export { default as ConfirmationDialog } from './Confirmation'; export { default as ConfirmationDialog } from './Confirmation';
export { default as CoordinatorSummaryDialog } from './CoordinatorSummary'; export { default as ExchangeDialog } from './Exchange';
export { default as CoordinatorDialog } from './Coordinator';
export { default as ProfileDialog } from './Profile'; export { default as ProfileDialog } from './Profile';
export { default as StatsDialog } from './Stats'; export { default as ClientDialog } from './Client';
export { default as EnableTelegramDialog } from './EnableTelegram'; export { default as EnableTelegramDialog } from './EnableTelegram';
export { default as UpdateClientDialog } from './UpdateClient';
export { default as NoticeDialog } from './Notice'; export { default as NoticeDialog } from './Notice';
export { default as F2fMapDialog } from './F2fMap'; export { default as F2fMapDialog } from './F2fMap';
export { default as UpdateDialog } from './Update';

View File

@ -30,7 +30,7 @@ export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
}, 30000); }, 30000);
} }
render() { render(): React.ReactNode {
if (this.state.hasError) { if (this.state.hasError) {
return ( return (
<div style={{ overflow: 'auto', height: '100%', width: '100%', background: 'white' }}> <div style={{ overflow: 'auto', height: '100%', width: '100%', background: 'white' }}>

View File

@ -0,0 +1,250 @@
import React, { useCallback, useEffect, useState, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, useTheme, Checkbox, CircularProgress, Typography, Grid } from '@mui/material';
import { DataGrid, type GridColDef, type GridValidRowModel } from '@mui/x-data-grid';
import { type Coordinator } from '../../models';
import RobotAvatar from '../RobotAvatar';
import { Link, LinkOff } from '@mui/icons-material';
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
import {
ActionFederation,
UseFederationStoreType,
FederationContext,
} from '../../contexts/FederationContext';
interface FederationTableProps {
federation: Record<string, Coordinator>;
dispatchFederation: (action: ActionFederation) => void;
fetchCoordinatorInfo: (coordinator: Coordinator) => Promise<void>;
setFocusedCoordinator: (state: number) => void;
openCoordinator: () => void;
maxWidth?: number;
maxHeight?: number;
fillContainer?: boolean;
}
const FederationTable = ({
openCoordinator,
maxWidth = 90,
maxHeight = 50,
fillContainer = false,
}: FederationTableProps): JSX.Element => {
const { t } = useTranslation();
const { federation, setFocusedCoordinator } =
useContext<UseFederationStoreType>(FederationContext);
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
const [pageSize, setPageSize] = useState<number>(0);
// all sizes in 'em'
const fontSize = theme.typography.fontSize;
const verticalHeightFrame = 3.25;
const verticalHeightRow = 3.25;
const defaultPageSize = Math.max(
Math.floor((maxHeight - verticalHeightFrame) / verticalHeightRow),
1,
);
const height = defaultPageSize * verticalHeightRow + verticalHeightFrame;
const [useDefaultPageSize, setUseDefaultPageSize] = useState(true);
useEffect(() => {
if (useDefaultPageSize) {
setPageSize(defaultPageSize);
}
});
const localeText = {
MuiTablePagination: { labelRowsPerPage: t('Coordinators per page:') },
noResultsOverlayLabel: t('No coordinators found.'),
};
const onClickCoordinator = function (shortAlias: string): void {
setFocusedCoordinator(shortAlias);
openCoordinator();
};
const aliasObj = useCallback((width: number) => {
return {
field: 'longAlias',
headerName: t('Coordinator'),
width: width * fontSize,
renderCell: (params: any) => {
return (
<Grid
container
direction='row'
sx={{ cursor: 'pointer', position: 'relative', left: '-0.3em', width: '50em' }}
wrap='nowrap'
onClick={() => {
onClickCoordinator(params.row.shortAlias);
}}
alignItems='center'
spacing={1}
>
<Grid item>
<RobotAvatar
nickname={params.row.shortAlias}
coordinator={true}
style={{ width: '3.215em', height: '3.215em' }}
smooth={true}
flipHorizontally={true}
baseUrl={hostUrl}
small={true}
/>
</Grid>
<Grid item>
<Typography>{params.row.longAlias}</Typography>
</Grid>
</Grid>
);
},
};
}, []);
const enabledObj = useCallback(
(width: number) => {
return {
field: 'enabled',
headerName: t('Enabled'),
width: width * fontSize,
renderCell: (params: any) => {
return (
<Checkbox
checked={params.row.enabled}
onClick={() => {
onEnableChange(params.row.shortAlias);
}}
/>
);
},
};
},
[federation],
);
const upObj = useCallback((width: number) => {
return {
field: 'up',
headerName: t('Up'),
width: width * fontSize,
renderCell: (params: any) => {
return (
<div
style={{ cursor: 'pointer' }}
onClick={() => {
onClickCoordinator(params.row.shortAlias);
}}
>
{Boolean(params.row.loadingInfo) && Boolean(params.row.enabled) ? (
<CircularProgress thickness={0.35 * fontSize} size={1.5 * fontSize} />
) : params.row.info !== undefined ? (
<Link color='success' />
) : (
<LinkOff color='error' />
)}
</div>
);
},
};
}, []);
const columnSpecs = {
alias: {
priority: 2,
order: 1,
normal: {
width: 12.1,
object: aliasObj,
},
},
up: {
priority: 3,
order: 2,
normal: {
width: 3.5,
object: upObj,
},
},
enabled: {
priority: 1,
order: 3,
normal: {
width: 5,
object: enabledObj,
},
},
};
const filteredColumns = function (): {
columns: Array<GridColDef<GridValidRowModel>>;
width: number;
} {
const useSmall = maxWidth < 30;
const selectedColumns: object[] = [];
let width: number = 0;
for (const value of Object.values(columnSpecs)) {
const colWidth = Number(
useSmall && Boolean(value.small) ? value.small.width : value.normal.width,
);
const colObject = useSmall && Boolean(value.small) ? value.small.object : value.normal.object;
if (width + colWidth < maxWidth || selectedColumns.length < 2) {
width = width + colWidth;
selectedColumns.push([colObject(colWidth, false), value.order]);
} else {
selectedColumns.push([colObject(colWidth, true), value.order]);
}
}
// sort columns by column.order value
selectedColumns.sort(function (first, second) {
return first[1] - second[1];
});
const columns: Array<GridColDef<GridValidRowModel>> = selectedColumns.map(function (item) {
return item[0];
});
return { columns, width: width * 0.9 };
};
const { columns, width } = filteredColumns();
const onEnableChange = function (shortAlias: string): void {
if (federation.getCoordinator(shortAlias).enabled) {
federation.disbaleCoordinator(shortAlias);
} else {
federation.enaleCoordinator(shortAlias);
}
};
return (
<Box
sx={
fillContainer
? { width: '100%', height: '100%' }
: { width: `${width}em`, height: `${height}em`, overflow: 'auto' }
}
>
<DataGrid
localeText={localeText}
rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize}
rows={Object.values(federation.coordinators)}
getRowId={(params: Coordinator) => params.shortAlias}
columns={columns}
checkboxSelection={false}
pageSize={pageSize}
rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);
setUseDefaultPageSize(false);
}}
hideFooter={true}
/>
</Box>
);
};
export default FederationTable;

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Paper, Alert, AlertTitle, Button, Link } from '@mui/material'; import { Paper, Alert, AlertTitle, Button } from '@mui/material';
const SelfhostedAlert = (): JSX.Element => { const SelfhostedAlert = (): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -12,7 +12,7 @@ const UnsafeAlert = (): JSX.Element => {
const [unsafeClient, setUnsafeClient] = useState<boolean>(false); const [unsafeClient, setUnsafeClient] = useState<boolean>(false);
// To do. Read from Coordinators Obj. // To do. Read from Coordinators Obj.
const safe_urls = [ const safeUrls = [
'robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion', 'robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion',
'robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion', 'robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion',
'robodevs7ixniseezbv7uryxhamtz3hvcelzfwpx3rvoipttjomrmpqd.onion', 'robodevs7ixniseezbv7uryxhamtz3hvcelzfwpx3rvoipttjomrmpqd.onion',
@ -20,11 +20,8 @@ const UnsafeAlert = (): JSX.Element => {
'r7r4sckft6ptmk4r2jajiuqbowqyxiwsle4iyg4fijtoordc6z7a.b32.i2p', 'r7r4sckft6ptmk4r2jajiuqbowqyxiwsle4iyg4fijtoordc6z7a.b32.i2p',
]; ];
const checkClient = () => { const checkClient = (): void => {
const http = new XMLHttpRequest(); const unsafe = !safeUrls.includes(getHost());
const h = getHost();
const unsafe = !safe_urls.includes(h);
setUnsafeClient(unsafe); setUnsafeClient(unsafe);
}; };
@ -106,6 +103,8 @@ const UnsafeAlert = (): JSX.Element => {
)} )}
</Paper> </Paper>
); );
} else {
return <></>;
} }
}; };

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react'; import { useContext } from 'react';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import SelfhostedAlert from './SelfhostedAlert'; import SelfhostedAlert from './SelfhostedAlert';
import UnsafeAlert from './UnsafeAlert'; import UnsafeAlert from './UnsafeAlert';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function AmbossIcon(props) { const AmbossIcon: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 95.7 84.9'> <SvgIcon {...props} x='0px' y='0px' viewBox='0 0 95.7 84.9'>
<g id='Layer_2_00000052094167160547307180000012226084410257483709_'> <g id='Layer_2_00000052094167160547307180000012226084410257483709_'>
@ -25,4 +25,6 @@ export default function AmbossIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default AmbossIcon;

View File

@ -0,0 +1,152 @@
import React from 'react';
import { SvgIcon, type SvgIconProps } from '@mui/material';
const BadgeDevFund: React.FC<SvgIconProps> = (props) => {
return (
<SvgIcon {...props} viewBox='0 0 100 100'>
<g>
<path
fill='#7BD3FF'
d='M100,12.893C100,5.535,93.686,0,86.33,0H13.44C6.085,0,0,5.535,0,12.893v72.879
C0,93.129,6.085,100,13.44,100h72.89c7.355,0,13.67-6.871,13.67-14.229V12.893z'
/>
<g>
<path
fill='none'
d='M60.172,65H38.148c-0.824,0-1.2,0.006-1.2,0.5c0,0.555,0.433,0.5,0.944,0.5h22.024
c0.514,0,0.946,0.055,0.946-0.5C60.863,65.053,60.528,65,60.172,65z'
/>
<g>
<path
fill='#0275BD'
d='M57.228,27.309l-5.355-0.375l-4.449-0.312l-1.681-0.117L41.5,27.358l-9.062,7.438
c-0.028,0.057-0.068,0.109-0.12,0.152c-0.022,0.018-0.046,0.033-0.068,0.047l-2.752,11.648c0.317,0.322,1.107,1.011,1.999,0.887
c0.912-0.126,1.784-1.099,2.522-2.813c0.486-1.128,0.922-2.109,1.309-2.961c0.037,0.284,0.082,0.565,0.136,0.845
c0.051,0.26,0.106,0.518,0.171,0.772c1.481,5.843,6.773,10.167,13.078,10.167c7.449,0,13.49-6.041,13.49-13.49
c0-0.873-0.084-1.725-0.243-2.55l-0.689,0.233l-0.692,0.232c0.12,0.678,0.182,1.373,0.182,2.084
c0,6.643-5.404,12.047-12.047,12.047c-6.315,0-11.342-4.883-11.842-11.07C36.846,40.705,37,40.379,37,40.051
c0-0.013,0-0.025,0-0.039c0-0.404-0.145-0.805-0.105-1.199c0.621-6.062,5.677-10.81,11.902-10.81
c2.838,0,5.406,0.987,7.469,2.636l0.202-0.74l0.213-0.74L57.228,27.309z'
/>
<rect
x='41.968'
y='43.084'
transform='matrix(0.9718 0.2357 -0.2357 0.9718 11.7218 -9.985)'
fill='#0275BD'
width='11.31'
height='1.9'
/>
<path
fill='#0275BD'
d='M42.923,39.859l5.267,1.277c-0.005-0.029-0.01-0.059-0.015-0.088c-0.139-1.016,0.423-1.721,0.445-1.749
l0.01-0.012l-5.259-1.276L42.923,39.859z'
/>
<path
fill='#0275BD'
d='M53.914,42.526l0.065-0.272c-0.133,0.068-0.263,0.133-0.39,0.195L53.914,42.526z'
/>
<path
fill='#0275BD'
d='M43.863,36.33l6.238,1.514c0.055-0.053,0.11-0.106,0.168-0.163c0.441-0.427,0.937-0.906,1.445-1.401
l-7.402-1.795L43.863,36.33z'
/>
<rect
x='45.277'
y='46.947'
transform='matrix(-0.2448 0.9696 -0.9696 -0.2448 104.3776 14.2907)'
fill='#0275BD'
width='2.692'
height='1.692'
/>
<polygon
fill='#0275BD'
points='50.902,34.197 51.019,33.735 51.136,33.274 51.532,31.698 49.893,31.285 49.435,33.104
49.317,33.565 49.235,33.895 50.134,34.121 50.874,34.308 '
/>
<path
fill='#0275BD'
d='M71.156,32.678l4.156-8.516c-0.048-0.015-0.096-0.029-0.142-0.049l-0.012,0.039l-0.789-0.244l-8.853-2.74
c-1.147,0.99-2.557,2.221-3.837,3.354c-0.119,0.107-0.239,0.215-0.357,0.32c-0.123,0.109-0.244,0.217-0.362,0.322
c-1.068,0.954-1.973,1.781-2.448,2.256c-0.175,0.175-0.294,0.01-0.338,0.075c0.001-0.002-0.056,0.001-0.099,0.019l-0.611,1.979
l-0.23,0.91l-0.231,0.842l-0.402,1.37l0.148-0.073l0.777,1.258l0.301,0.487l0.303,0.487l1.556,2.5l0.693-0.232l0.685-0.232
l0.687-0.23l5.118-1.729C69.259,34.021,70.797,32.943,71.156,32.678z M66.1,26.309c-0.34,0-0.646-0.146-0.858-0.379
c-0.193-0.209-0.312-0.486-0.312-0.791c0-0.646,0.524-1.17,1.17-1.17s1.169,0.523,1.169,1.17c0,0.123-0.019,0.242-0.055,0.354
C67.063,25.965,66.622,26.309,66.1,26.309z'
/>
<path
fill='#0275BD'
d='M26.862,36.773c0.965,0,2.087-0.457,3.338-1.357l1.13-0.927l-2.778-3.38
c-0.091-0.109-0.121-0.247-0.1-0.376l-4.283,4.135C24.44,35.45,25.235,36.773,26.862,36.773z'
/>
<path
fill='#0275BD'
d='M31.657,33.404l0.376,0.457l0.024,0.03l0.421-0.345l3.94-3.234l-1.29-1.811l-1.778-2.498l-4.496,4.341
c0.156-0.021,0.317,0.036,0.424,0.166L31.657,33.404z'
/>
<polygon
fill='#0275BD'
points='37.148,29.713 40.781,26.729 40.08,22.621 39.996,22.131 39.912,21.641 39.623,19.949
34.036,25.342 35.66,27.624 '
/>
<path
fill='#0275BD'
d='M46.396,19.289l0.491,5.988L48,25.604c0,0.013,0,0.024,0,0.036l10.742,0.791
c0.473-0.459,0.915-1.121,2.006-2.096c0.113-0.101,0.094-0.205,0.215-0.312c0.113-0.102,0.161-0.204,0.281-0.311
c0.032-0.029,0.028-0.055,0.061-0.084c1.894-1.676,3.794-3.328,3.814-3.345l0.188-0.171l9.377,2.905l-8.331-9.662l-0.448,0.257
l-0.077,0.016c-2.756,0.532-12.898,2.773-19.544,4.252l0.073,0.924L46.396,19.289z M64.687,15.846c0.646,0,1.17,0.523,1.17,1.17
c0,0.646-0.524,1.168-1.17,1.168s-1.169-0.522-1.169-1.168C63.518,16.369,64.041,15.846,64.687,15.846z M48.854,18.955
c0.215-0.245,0.529-0.401,0.881-0.401c0.338,0,0.641,0.144,0.854,0.372c0.194,0.209,0.315,0.488,0.315,0.797
c0,0.646-0.524,1.17-1.17,1.17s-1.169-0.523-1.169-1.17C48.565,19.43,48.675,19.16,48.854,18.955z'
/>
<path
fill='#0275BD'
d='M40.871,21.658l0.084,0.49l0.719,4.215l4.008-0.809l0.272,0.02l-0.439-5.49L45.476,19.6l-0.039-0.484
l-0.082-1.029c-2.497,0.557-4.357,0.973-4.905,1.096l0.339,1.986L40.871,21.658z'
/>
<path
fill='#0275BD'
d='M49.348,39.898c-0.037,0.051-0.323,0.459-0.237,1.039c0.022,0.153,0.068,0.306,0.139,0.457
c0.186,0.405,0.536,0.805,1.051,1.193c0.268-0.041,0.873-0.166,1.764-0.51c0.35-0.135,0.742-0.304,1.176-0.515
c-0.396-0.544-0.732-1.007-1.019-1.403c-0.659-0.915-1.05-1.473-1.283-1.816c-0.004,0.006-0.008,0.01-0.013,0.014
c-0.47,0.455-0.924,0.895-1.225,1.189C49.518,39.727,49.39,39.854,49.348,39.898z'
/>
<path
fill='#0275BD'
d='M54.281,41.015c1.28-0.724,2.82-1.774,4.57-3.298l-1.87-3.002l-0.306-0.491l-0.307-0.491l-0.291-0.467
c-0.121,0.127-0.254,0.266-0.399,0.414c-0.134,0.139-0.278,0.285-0.43,0.438c-0.14,0.141-0.284,0.287-0.436,0.438
c-0.618,0.617-1.322,1.308-2.018,1.986c-0.399,0.39-0.797,0.774-1.172,1.14c0.105,0.157,0.258,0.377,0.442,0.639
c0.41,0.582,0.979,1.372,1.567,2.182c0.149,0.208,0.301,0.416,0.452,0.623C54.15,41.089,54.216,41.051,54.281,41.015z'
/>
<polygon
fill='#0275BD'
points='79.825,16.213 69.762,10.616 67.608,12.816 75.157,21.717 '
/>
<polygon
fill='#0275BD'
points='70.018,9.68 79.255,14.817 80.446,15.48 80.866,15.714 81.285,15.947 81.969,16.328
83.609,12.775 70.943,5.73 69.304,9.283 69.591,9.443 '
/>
<path
fill='#0275BD'
d='M81.919,91.473c0.007-0.013,0.014-0.025,0.021-0.038c-0.006-0.017-0.01-0.036-0.015-0.055
C81.923,91.41,81.921,91.44,81.919,91.473z'
/>
<path
fill='#0275BD'
d='M77.68,59.103c-0.064-0.093-54.519-0.051-54.519-0.051L14,73.543V91.86v0.251v0.061
c0,0.013,0.137,0.025,0.138,0.038c0.002,0.004,0.069,0.041,0.073,0.044c0.051,0.4,0.448,0.746,0.851,0.746h0.016
C15.514,93,17,92.563,17,92.111V91.86c0-0.015-0.001-0.029-0.002-0.044C16.999,91.815,17,91.813,17,91.812V75h65v16.86v0.229
v0.021C82,92.563,82.178,93,82.614,93h0.016c0.437,0,1.37-0.437,1.37-0.889v-0.002V91.86V73.235
C84,72.922,77.68,59.103,77.68,59.103z M59.917,66H37.893c-0.512,0-0.944,0.055-0.944-0.5c0-0.494,0.376-0.5,1.2-0.5h22.023
c0.356,0,0.691,0.053,0.691,0.5C60.863,66.055,60.431,66,59.917,66z'
/>
</g>
</g>
</g>
</SvgIcon>
);
};
export default BadgeDevFund;

View File

@ -0,0 +1,79 @@
import React from 'react';
import { SvgIcon, type SvgIconProps } from '@mui/material';
const BadgeFounder: React.FC<SvgIconProps> = (props) => {
return (
<SvgIcon {...props} viewBox='0 0 100 100'>
<g>
<path
fill='#DF87FF'
d='M100,12.543C100,5.194,93.416,0,86.036,0H13.239C5.858,0,0,5.194,0,12.543v72.912
C0,92.804,5.858,100,13.239,100h72.797C93.416,100,100,92.804,100,85.455V12.543z'
/>
<g>
<path
fill='#9517CE'
d='M47.421,31.641l0.003-0.967c0.009-1.817-1.459-3.296-3.275-3.303l-11.994-0.05
c-1.816-0.007-3.296,1.46-3.304,3.276l-0.004,0.965c-0.008,1.818,1.459,3.297,3.277,3.305l11.991,0.049
C45.934,34.924,47.413,33.456,47.421,31.641z'
/>
<path
fill='#9517CE'
d='M70,45.531v5.008l2.57-4.386c0.43,0.041,0.66,0.063,1.104,0.063c7.59,0,13.642-6.154,13.642-13.746
c0-7.59-6.206-13.743-13.797-13.743s-13.357,6.153-13.357,13.743C60.161,38.563,65,43.729,70,45.531z M71.773,41.121l-1.619-0.407
l0.649-2.58l1.62,0.409L71.773,41.121z M74.52,23.316l1.619,0.41l-0.65,2.577l-1.62-0.407L74.52,23.316z M69.005,26.477
l10.859,2.637l-0.443,1.824l-10.858-2.635L69.005,26.477z M68.075,29.965L78.935,32.6l-0.443,1.824l-10.858-2.635L68.075,29.965z
M67.067,33.683l10.86,2.636l-0.444,1.825l-10.858-2.636L67.067,33.683z'
/>
</g>
<path
fill='#9517CE'
d='M90.127,83.767c-0.516-0.809-0.598-1.344-1.545-1.613L89,83.285V83.18L87.997,82c0.001,0-0.034,0-0.034,0
l2.545-11.693c0-4.914-4.002-8.307-8.917-8.307H58.618c-3.943-2-7.946-4.107-11.954-4.908c-0.894-0.18-1.787-0.582-2.682-0.693
l-0.006,1.05l-0.004,1.604C46.879,59.443,49.97,60,53.21,62h-1.782c-4.914,0-8.292,2.769-8.897,8.406L40.29,82H22.418h-0.454
l-0.011-5.766c3.257-1.551,5.418-4.831,5.434-8.609c0.01-2.396-0.855-4.6-2.303-6.289c2.21-1.017,4.907-1.982,8.001-2.573
l-0.01,2.682c-0.01,2.455,1.979,4.505,4.435,4.516L39.49,66h0.019c2.444,0,4.441-2.021,4.451-4.467l0.013-3.036l0.004-1.173
l0.006-1.177l0.01-2.543c1.155-0.467,2.245-1.091,3.229-1.862c2.458-1.925,4.144-4.577,4.873-7.701
c2.039-8.738,2.42-27.79-9.423-30.905c-0.535-0.141-1.123-0.241-1.757-0.298l0.017-4.204c0.001-0.052-0.001-0.104-0.002-0.154
c0.62-0.569,1.009-1.387,1.009-2.295c0-1.719-1.394-3.112-3.112-3.112s-3.112,1.394-3.112,3.112c0,0.808,0.308,1.543,0.812,2.096
c-0.009,0.11-0.016,0.222-0.016,0.336l-0.017,4.195c-0.671,0.049-1.292,0.145-1.853,0.285c-11.868,3.02-11.645,22.07-9.677,30.826
c1.017,4.521,4.113,8.062,8.146,9.702l-0.011,2.784c-0.16,0.029-0.319,0.056-0.479,0.086c-3.233,0.619-6.439,2.215-9.396,3.664
C21.686,59.105,19.827,59,17.825,59h-0.04c-5.282,0-9.598,3.76-9.619,9.043c-0.013,3.072,1.427,5.592,3.668,7.36L11.806,82h-1.646
c-1.72,0-2.982,0.588-3.752,1.797c-0.572,0.896-0.839,2.066-0.839,3.706c0,1.639,0.267,2.799,0.839,3.696
C7.177,92.407,8.439,93,10.159,93h76.216c1.721,0,2.983-0.598,3.752-1.805c0.573-0.898,0.839-2.074,0.839-3.713
S90.7,84.664,90.127,83.767z M42,61.489c0,1.17-1.14,2.122-2.31,2.122l-1.779-0.008C36.737,63.598,36,62.639,36,61.464v-0.172
c1,0.363,1.648,0.784,3.059,0.784c0.898,0,1.941-0.171,2.941-0.637V61.489z M42,58.233l-0.182,1.609
c-0.329,0.164-0.74,0.299-1.048,0.405c-1.828,0.633-3.298,0.343-4.178-0.017c-0.609-0.248-0.979-0.528-1.019-0.561
c-0.045-0.038,0.025-0.724-0.026-0.749l0.031-1.297L36,57.664c0,0.206,0.605,0.43,1.234,0.585c0.486,0.121,0.891,0.2,1.504,0.2
c0.604,0,1.367-0.077,2.082-0.27c0.349-0.095,1.18-0.215,1.18-0.367V58.233z M41.661,55.924l-0.002,0.291
c-0.331,0.164-0.652,0.3-0.961,0.405c-1.828,0.633-3.255,0.343-4.137-0.017c-0.553-0.225-0.892-0.477-0.978-0.546
c-0.009-0.007-0.015-0.012-0.019-0.015c-0.045-0.038-0.093-0.068-0.144-0.094l0.007-1.616c0.91,0.19,1.852,0.292,2.815,0.292
c0.081,0,0.161-0.03,0.242-0.031c0.117,0.003,0.236-0.034,0.354-0.034l0,0c0.955,0,1.901-0.079,2.827-0.272L41.661,55.924z
M38.368,9.265c0.149,0.022,0.303,0.033,0.458,0.033c0.082,0,0.162-0.004,0.241-0.011l-0.014,3.504
c-0.131,0.004-0.263,0.009-0.396,0.016c-0.102-0.006-0.202-0.01-0.305-0.014L38.368,9.265z M31.256,49.9
c-2.044-1.615-3.437-3.856-4.026-6.481c-1.735-7.716-2.132-25.495,7.983-28.068c0.375-0.095,0.805-0.163,1.27-0.206
c0.297-0.025,0.609-0.043,0.932-0.047c0.303-0.004,0.613,0.002,0.931,0.018c0.077,0.004,0.155,0.009,0.232,0.014l0.071,0.004
l0.073-0.004c0.107-0.005,0.216-0.01,0.322-0.015c0.317-0.011,0.629-0.013,0.931-0.004c0.323,0.009,0.635,0.03,0.931,0.062
c0.429,0.047,0.825,0.115,1.176,0.208c10.094,2.655,9.55,20.431,7.752,28.132c-0.606,2.605-2.006,4.824-4.044,6.421
c-0.561,0.439-1.165,0.821-1.799,1.145c-0.374,0.19-0.758,0.36-1.15,0.508c-0.997,0.374-2.004,0.799-3.064,0.939
c-0.76,0.101-3.221-0.096-3.462-0.141c-0.71-0.131-1.361-0.363-2.035-0.614c-0.393-0.146-0.777-0.401-1.15-0.591
C32.467,50.844,31.839,50.361,31.256,49.9z M87.689,83.287L87.689,83.287l-0.012,0.055L87.689,83.287z M61.314,68h0.438h1.163
h1.161h7.311c1.37,0,1.305,1.201,1.305,2c0,0.801,0.065,2-1.305,2h-6.569h-0.863h-0.298h-0.863h-1.162h-0.316
c-0.278,0-0.497,0-0.669,0h-0.047C60.046,72,60,70.252,60,69.725c0-0.066,0-0.259,0-0.325C60,68.6,59.944,68,61.314,68z M17.475,82
h-3.346l0.007-1.979c0.674,0.299,1.604,0.56,2.743,0.56c0.222,0,0.449-0.02,0.686-0.043c0.642-0.061,1.34-0.183,2.086-0.462
L19.642,82H17.475z M14.143,78.358l-0.117-1.896C15.136,76.916,16.447,77,17.719,77h0.023h0.046c0.642,0,1.311-0.254,1.917-0.373
l-0.049,2.059c-0.229,0.104-0.452,0.082-0.67,0.158c-0.471,0.162-0.914,0.206-1.327,0.262c-1.2,0.16-2.157-0.097-2.812-0.363
C14.547,78.62,14.311,78.464,14.143,78.358z M10.488,67.889C10.504,63.881,13.778,61,17.785,61h0.029
c1.068,0,2.08-0.117,2.994,0.297c0.41,0.186,0.8,0.232,1.165,0.489c0.343,0.239,0.664,0.419,0.96,0.713
c1.333,1.324,2.157,3.116,2.149,5.144c-0.011,2.439-1.229,4.57-3.084,5.881c-1.422,1.005-3.479,1.598-5.262,1.443
c-0.509-0.044-1.47-0.17-1.94-0.374c-0.873-0.38-1.078-0.617-1.793-1.237c-0.433-0.375-0.821-0.771-1.155-1.236
C10.987,70.919,10.48,69.479,10.488,67.889z M86.375,91H10.159c-1.471,0-2.268,0.203-2.268-2.5c0-2.086,0.361-2.5,2.268-2.5h1.638
h0.733h0.428h1.16h1.839h1.369h0.464h1.843h1.161h1.161h0.463h17.441h13.731h1.014h1.161h1.161h0.521h3.624h0.008h1.361h0.953
h0.207h1.161h10.929h10.684C86.789,86,88,86,88,86v-1.584c1,0.37,0.918,1.279,0.918,3.223C88.918,89.723,88.283,91,86.375,91z'
/>
</g>
</SvgIcon>
);
};
export default BadgeFounder;

View File

@ -0,0 +1,186 @@
import React from 'react';
import { SvgIcon, type SvgIconProps } from '@mui/material';
const BadgeLimits: React.FC<SvgIconProps> = (props) => {
return (
<SvgIcon {...props} viewBox='0 0 100 100'>
<g>
<path
fill='#FFE47B'
d='M100,12.146C100,4.874,94.133,0,86.904,0H13.079C5.852,0,0,4.874,0,12.146v73.666
C0,93.084,5.852,100,13.079,100h73.825C94.133,100,100,93.084,100,85.812V12.146z'
/>
<g>
<path
fill='#DD6C01'
d='M42.982,39.041c0.646,0.696,1.7,0.835,2.675,0.447c0.205-0.066,0.396-0.164,0.572-0.293
c0.166-0.107,0.327-0.23,0.479-0.371l0.062-0.056c0.089-0.083,0.17-0.169,0.247-0.257l22.319-20.703
c0.979-0.871,6.873-6.916,5.815-8.105l-0.055-0.061c-1.057-1.189-8.329,4.508-9.309,5.377L44.932,32.549l-8.202-7.813
c-0.891-0.962-4.807-4.321-5.973-3.239l-0.062,0.057c-1.166,1.081,2.524,5.685,3.415,6.646L42.982,39.041z'
/>
<path
fill='#DD6C01'
d='M28.018,55l0.004-0.051l0.3-1.146c-1.722-0.509-3.59-1.063-5.303-1.544l-4.585,15.679l5.682,1.619
L28.021,55H28.018z'
/>
<path
fill='#DD6C01'
d='M5,47.607v16.729l2.036,0.652l4.629-15.5C9.237,48.781,7,48.07,5,47.607z'
/>
<path
fill='#DD6C01'
d='M88.421,54.312l-0.216-0.305c-0.309-1.103-1.399-1.873-2.561-1.873c-0.242,0-0.517,0.033-0.751,0.099
l-5.704,1.59c-1.391,0.389-2.217,1.816-1.836,3.185l0.262,0.947c-0.757,0.206-1.498,0.431-2.188,0.666l-0.327,0.111l-0.08,0.335
c-0.438,1.807-1.39,3.402-2.684,4.49c-0.636,0.536-1.41,0.915-2.3,1.126l-0.054,0.013l-0.051,0.022
c-1.358,0.591-3.126,0.56-5.059-0.076c-0.404-0.133-0.792-0.292-1.164-0.469l-0.38,0.514l-0.378,0.511
c0.485,0.246,0.998,0.459,1.53,0.635c2.209,0.726,4.263,0.749,5.898,0.057c1.059-0.259,1.989-0.721,2.764-1.374
c1.414-1.189,2.472-2.89,3.006-4.816c0.574-0.19,1.181-0.371,1.796-0.539l3.626,12.974c-1.7,0.656-3.707,1.229-5.853,1.725
l0.438,0.291c0.087,0.057,0.167,0.12,0.244,0.187c0.181,0.156,0.333,0.336,0.456,0.532c1.826-0.451,3.544-0.964,5.057-1.541
c0.357,1.017,1.34,1.715,2.445,1.715c0.241,0,0.483-0.034,0.717-0.1l5.688-1.589c1.391-0.389,2.212-1.818,1.829-3.184l0.102-0.558
C93.041,69.504,94,69.395,94,69.285V68.63v-0.657c0,0.147-1.193,0.292-1.643,0.431l-3.471-12.885L94,53.958v-0.657v-0.656
L88.421,54.312z M90.994,68.746l0.169,0.604l0.169,0.604l0.146,0.526c0.21,0.753-0.25,1.541-1.025,1.758l-5.688,1.59
c-0.133,0.037-0.27,0.057-0.406,0.057c-0.633,0-1.191-0.407-1.372-0.994c-0.004-0.012-0.008-0.022-0.011-0.034l-0.16-0.568
l-0.166-0.6L79.473,60.32l-0.404-1.449l-0.169-0.602l-0.168-0.604l-0.104-0.368l-0.168-0.603c-0.211-0.753,0.25-1.542,1.026-1.759
l5.687-1.589c0.134-0.038,0.271-0.057,0.405-0.057c0.646,0,1.214,0.423,1.385,1.027l0.094,0.34l0.17,0.604l0.168,0.603
L90.994,68.746z'
/>
<path
fill='#DD6C01'
d='M21.812,51.913l0.004-0.014l0.107-0.299c0.029-0.1,0.077-0.198,0.077-0.298c0-0.004,0-0.008,0-0.013
c0-0.224-0.033-0.448-0.111-0.66c-0.024-0.074-0.066-0.146-0.104-0.217c-0.201-0.364-0.537-0.63-0.936-0.746l-6.075-1.776
c-0.143-0.041-0.292-0.062-0.438-0.062c-0.576,0-1.101,0.324-1.368,0.819c-0.053,0.095-0.096,0.196-0.127,0.302l-0.086,0.288
l-0.176,0.603L8.047,65.342l-0.176,0.601l-0.176,0.602l-0.012,0.038c-0.116,0.4-0.07,0.821,0.129,1.186
c0.201,0.367,0.53,0.632,0.931,0.748l6.071,1.775c0.144,0.042,0.291,0.063,0.439,0.063c0.687,0,1.302-0.461,1.495-1.122
l0.009-0.034l0.117-0.399l0.06-0.201l0.116-0.4l0.06-0.201l0.116-0.399l4.413-15.096L21.812,51.913z'
/>
<path
fill='#DD6C01'
d='M61.145,67.641l-1.209,1.634l1.351,0.822c0.037-0.069,0.078-0.139,0.122-0.204l0.986-1.489L61.145,67.641z
'
/>
<path
fill='#DD6C01'
d='M69.353,79.089l-2.999-1.985c-0.006,0.453-0.14,0.896-0.396,1.281l-1.141,1.722
c-0.274,0.416-0.667,0.728-1.116,0.904l3.043,2.015c0.22,0.145,0.473,0.222,0.733,0.222c0.447,0,0.862-0.223,1.108-0.596
l1.141-1.721C70.13,80.319,69.963,79.493,69.353,79.089z'
/>
<path
fill='#DD6C01'
d='M55.339,75.491l-0.376,0.51c0.032,0.031,0.067,0.062,0.103,0.091c0.036-0.002,0.072-0.003,0.108-0.003
c0.092,0,0.183,0.006,0.271,0.017c-0.059-0.196-0.091-0.401-0.099-0.606c0-0.006-0.001-0.012-0.001-0.018L55.339,75.491z'
/>
<path
fill='#DD6C01'
d='M63.935,79.521l1.14-1.723c0.17-0.256,0.244-0.556,0.214-0.858c-0.004-0.046-0.011-0.093-0.02-0.139
c-0.035-0.172-0.103-0.332-0.196-0.474c-0.037-0.055-0.079-0.106-0.124-0.155c-0.073-0.08-0.154-0.152-0.248-0.214l-0.277-0.184
l-1.188-0.787L62.647,74.6l-3.043-2.016c-0.219-0.145-0.472-0.221-0.732-0.221c-0.302,0-0.586,0.103-0.815,0.282
c-0.112,0.087-0.213,0.191-0.293,0.313l-0.115,0.172l-0.346,0.522l-0.68,1.026c-0.162,0.244-0.23,0.523-0.218,0.797
c0.021,0.408,0.226,0.803,0.593,1.045l0.03,0.021l0.846,0.559l0.061,0.041l0.782,0.52l0.975,0.645l0.304,0.201l0.562,0.372
l0.845,0.56l0.691,0.458c0.219,0.144,0.472,0.221,0.731,0.221C63.273,80.117,63.688,79.895,63.935,79.521z'
/>
<path
fill='#DD6C01'
d='M75.713,75.141c-0.045-0.039-0.09-0.076-0.141-0.109l-0.566-0.376l-0.726-0.479l-2.267-1.501
c0.043,0.53-0.089,1.057-0.388,1.507l-0.383,0.58l-0.476,0.718l-0.28,0.424c-0.073,0.109-0.154,0.212-0.244,0.308
c-0.195,0.21-0.424,0.381-0.676,0.508l3.397,2.249c0.219,0.146,0.473,0.222,0.733,0.222c0.447,0,0.862-0.223,1.108-0.596
l1.14-1.721C76.317,76.312,76.206,75.571,75.713,75.141z'
/>
<path
fill='#DD6C01'
d='M62.098,71.475c0.062,0.301,0.22,0.564,0.452,0.756c0.036,0.031,0.076,0.061,0.116,0.088l3.559,2.355
l0.621,0.411l0.459,0.305l0.457,0.302c0.146,0.097,0.307,0.162,0.475,0.195c0.033,0.007,0.065,0.012,0.099,0.017
c0.053,0.006,0.105,0.01,0.159,0.01c0.228,0,0.447-0.059,0.641-0.165c0.186-0.102,0.348-0.247,0.468-0.43l0.208-0.313l0.933-1.408
c0.195-0.296,0.265-0.65,0.194-0.998c-0.071-0.348-0.273-0.648-0.568-0.844l-0.467-0.309l-0.695-0.461l-0.537-0.354l-3.396-2.25
c-0.219-0.145-0.472-0.221-0.732-0.221c-0.046,0-0.09,0.004-0.134,0.008c-0.264,0.027-0.51,0.13-0.71,0.296
c-0.101,0.083-0.191,0.179-0.266,0.291l-0.133,0.2l-1.007,1.522c-0.037,0.056-0.068,0.114-0.097,0.174
c-0.097,0.207-0.137,0.434-0.119,0.662C62.08,71.366,62.086,71.421,62.098,71.475z'
/>
</g>
<path
fill='#DD6C01'
d='M73.355,57.131c-0.943-4.371-5.124-7.201-8.465-8.806l-0.038-0.019l-12.591-4.036
c-0.149-0.071-0.484-0.198-0.97-0.198c-0.864,0-1.964,0.376-3.217,1.648c-0.143,0.145-0.286,0.301-0.433,0.47
c-0.128,0.147-0.257,0.304-0.388,0.47c-0.006-0.003-0.01-0.006-0.017-0.009c-0.005,0.006-0.011,0.014-0.016,0.021
c-0.298-0.15-0.952-0.402-2.021-0.402c-2.321,0-6.247,1.131-12.321,6.044c3.521-0.469,7.893-0.591,11.463-0.593
c-0.141,0.346-0.33,0.811-0.476,1.173l-0.044,0.082c-3.187,0.013-7.911,0.137-11.5,0.684c-0.46,0.069-0.899,0.146-1.315,0.231
c-0.021,0.004-0.041,0.008-0.062,0.013c-0.434,0.089-0.839,0.187-1.213,0.293c-0.006,0.002-0.013,0.004-0.021,0.006
c-0.075,0.022-0.152,0.043-0.226,0.066l-0.05,0.185l-0.137,0.508l-0.166,0.626l-3.813,14.255l-0.104,0.393
c0.054,0.077,0.121,0.173,0.203,0.285c0.155,0.214,0.36,0.491,0.612,0.819c0.401,0.524,0.923,1.181,1.558,1.931l-0.398,0.51
l-0.394,0.504l-1.553,1.993c-0.809,1.039-0.623,2.543,0.415,3.354l1.629,1.269c0.424,0.33,0.931,0.505,1.466,0.505
c0.401,0,0.786-0.099,1.126-0.28c0.145,0.424,0.409,0.812,0.788,1.106l1.628,1.27c0.425,0.33,0.932,0.504,1.469,0.504
c0.74,0,1.428-0.335,1.884-0.919l0.218-0.28l0.321-0.412c0.112,0.428,0.337,0.814,0.658,1.12c0.059,0.055,0.119,0.107,0.182,0.156
l1.628,1.27c0.337,0.263,0.727,0.426,1.143,0.481c0.107,0.016,0.215,0.023,0.325,0.023c0.262,0,0.517-0.043,0.759-0.122
c0.238-0.079,0.463-0.195,0.665-0.347c0.194,0.105,0.389,0.212,0.586,0.314l1.602-0.501c0.12,0.157,0.259,0.302,0.421,0.429
l0.089,0.069l0.602,0.468l0.938,0.731c0.423,0.331,0.931,0.505,1.467,0.505c0.609,0,1.181-0.226,1.618-0.63
c0.096-0.089,0.186-0.186,0.267-0.29l0.088-0.112l0.326-0.419l3.291,2.179c0.219,0.146,0.472,0.222,0.731,0.222
c0.447,0,0.863-0.223,1.109-0.596l1.141-1.721c0.269-0.406,0.283-0.908,0.084-1.317c-0.102-0.206-0.255-0.389-0.458-0.524
l-1.304-0.863l-1.209-0.8l-0.474-0.314l0.239-0.306l0.087-0.113l0.052-0.065l0.054-0.068c0.047-0.062,0.091-0.123,0.131-0.186
c0.024-0.038,0.044-0.077,0.066-0.115l2.748,1.819l0.193,0.128c0.271,0.18,0.494,0.406,0.664,0.661l1.696,1.123
c0.22,0.145,0.473,0.222,0.732,0.222c0.447,0,0.862-0.223,1.109-0.596l1.14-1.723c0.404-0.61,0.235-1.436-0.374-1.84l-4.387-2.905
l-0.709-0.47c-0.054-0.035-0.109-0.066-0.168-0.094c-0.176-0.083-0.368-0.127-0.564-0.127c-0.13,0-0.257,0.021-0.378,0.057
c-0.124,0.037-0.24,0.091-0.35,0.161c-0.09,0.06-0.173,0.129-0.247,0.209c-0.048,0.053-0.095,0.107-0.136,0.169l-0.188,0.285
l-0.039,0.059l-0.581,0.878c-0.051-0.141-0.113-0.274-0.188-0.402c-0.028-0.047-0.058-0.093-0.089-0.14
c-0.092-0.132-0.196-0.256-0.314-0.368c-0.016-0.015-0.032-0.028-0.049-0.043c-0.027-0.025-0.059-0.051-0.088-0.075
c-0.016-0.014-0.03-0.028-0.047-0.041l-0.304-0.235l0.287-0.387l0.546-0.739l0.678-0.918l0.617-0.834l0.328-0.444l0.327-0.44
l0.619-0.838l0.042-0.057l0.656-0.888l2.046-2.767l0.44-0.597l1.516-2.049l0.439-0.597l1.146-1.547l0.23-0.312
c-0.119-0.083-0.236-0.166-0.351-0.249c-0.321-0.229-0.624-0.448-0.973-0.614c-0.515-0.242-1.034-0.52-1.566-0.831
c-0.328-0.193-0.661-0.4-1-0.621c-0.184-0.119-0.369-0.243-0.557-0.371c-0.182-0.126-0.365-0.256-0.552-0.389
c-0.094-0.067-0.188-0.134-0.281-0.203c-1.568-1.15-3.563-2.688-5.396-4.318c-0.036-0.031-0.071-0.062-0.106-0.094l0.37-0.511
l0.117-0.161l0.252-0.349l0.09-0.124l0.333,0.268l0.588,0.473l2.303,1.852c0.186,0.187,1.124,1.108,2.521,2.193
c0.089,0.068,0.178,0.138,0.271,0.207c0.181,0.138,0.367,0.276,0.562,0.416c0.304,0.219,0.623,0.441,0.956,0.661
c0.205,0.137,0.417,0.271,0.633,0.404c0.785,0.487,1.632,0.813,2.513,1.203c1.501,0.664,3.104,0.981,4.685,0.981c0,0,0,0,0.001,0
c2.106,0,3.911-0.646,5.367-2.231l0.062,0.021l0.041-0.049C73.016,61.731,73.934,59.805,73.355,57.131z M41.97,59.117l0.357-0.673
l0.388-0.725l1.294-2.43c0.06,0.009,0.12,0.009,0.183-0.002c0.012-0.002,0.467-0.069,1.121,0.024
c0.694,0.099,1.614,0.382,2.469,1.122c0.135,0.115,0.267,0.244,0.397,0.384c0.038,0.043,0.077,0.088,0.117,0.133
c0.139,0.16,0.274,0.335,0.404,0.526c0.065,0.096,0.128,0.194,0.19,0.297l-0.803,1.105l-0.026,0.041
c-1.482,2.415-3.013,3.64-4.549,3.64c-1.254,0-2.243-0.839-2.688-1.297L41.97,59.117z M30.647,78.373l-0.393,0.504
c-0.226,0.288-0.371,0.614-0.445,0.949l-0.004,0.005c-0.254,0.325-0.636,0.513-1.05,0.513c-0.297,0-0.578-0.098-0.814-0.281
l-1.629-1.269c-0.577-0.451-0.681-1.288-0.23-1.866l1.426-1.829l0.392-0.502l0.398-0.51l1.358-1.744l0.184-0.234
c0.086-0.11,0.188-0.204,0.301-0.281c0.217-0.147,0.476-0.23,0.748-0.23c0.299,0,0.581,0.179,0.816,0.362l0.732,0.654h0.001
l0.894,0.616c0.281,0.218,0.459,0.49,0.502,0.843c0.011,0.081,0.012,0.144,0.008,0.224l-2.415,3.088l-0.393,0.498L30.647,78.373z
M35.794,81.175l-0.389,0.498l-0.382,0.491l-0.209,0.269c-0.255,0.324-0.637,0.511-1.048,0.511c-0.299,0-0.581-0.097-0.817-0.28
l-1.628-1.27c-0.357-0.279-0.534-0.708-0.508-1.13c0.016-0.259,0.105-0.516,0.276-0.735l0.331-0.425l0.386-0.493l0.468-0.502
L35,74.711v-0.002c1-0.325,0.561-0.513,0.974-0.513c0.298,0,0.542,0.098,0.778,0.281l1.608,1.27
c0.279,0.219,0.448,0.533,0.491,0.885c0.044,0.352-0.057,0.7-0.275,0.98L35.794,81.175z M41.175,83.871
c-0.188,0.24-0.444,0.403-0.732,0.474c-0.103,0.024-0.209,0.038-0.317,0.038c-0.298,0-0.579-0.098-0.815-0.281l-1.628-1.27
c-0.191-0.148-0.334-0.342-0.42-0.562c-0.007-0.005-0.015-0.012-0.023-0.017c-0.035-0.098-0.054-0.2-0.067-0.305
c-0.044-0.352,0.058-0.7,0.274-0.98l3.76-4.82c0.254-0.324,0.637-0.512,1.051-0.512c0.298,0,0.581,0.098,0.816,0.281l1.628,1.27
c0.578,0.45,0.682,1.287,0.231,1.864L41.175,83.871z M51.46,78.445l0.087,0.068l0.282,0.219c0.07,0.056,0.135,0.117,0.191,0.184
c0.002,0.001,0.002,0.003,0.004,0.005c0.014,0.015,0.025,0.032,0.038,0.048c0.114,0.148,0.197,0.318,0.241,0.503
c0.002,0.01,0.005,0.02,0.007,0.027c0.005,0.024,0.008,0.048,0.013,0.071c0.002,0.017,0.006,0.031,0.008,0.048
c0.025,0.21,0,0.419-0.07,0.613c-0.016,0.044-0.033,0.088-0.055,0.131c-0.006,0.013-0.014,0.026-0.02,0.039
c-0.037,0.068-0.079,0.135-0.127,0.196l-0.13,0.166l-0.025,0.033l-0.131,0.167l-0.196,0.252l-0.038,0.049l-0.387,0.498
l-0.388,0.497l-1.253,1.605l-0.388,0.498l-0.388,0.497l-0.07,0.091l-0.327,0.419l-0.037,0.048
c-0.103,0.131-0.227,0.239-0.365,0.322c-0.203,0.121-0.438,0.189-0.684,0.189c-0.298,0-0.579-0.098-0.814-0.281l-0.361-0.28
l-0.601-0.469l-0.601-0.468L44.81,84.38c-0.578-0.45-0.682-1.287-0.23-1.866l3.756-4.818c0.255-0.325,0.638-0.513,1.05-0.513
c0.298,0,0.581,0.098,0.816,0.281l0.728,0.566L51.46,78.445z M52.808,50.553c-0.091,0.888-0.312,1.85-0.77,2.768
c-0.182,0.248-0.29,0.495-0.439,0.744l-0.08,0.106c0,0.002,0.006,0.003,0.011,0.005c-0.003,0.006-0.007,0.01-0.01,0.015
l-0.018-0.012c0.001-0.003,0.002-0.005,0.004-0.008l-0.374,0.516l-0.371,0.512l-0.312,0.428l-0.06,0.083l-0.563,0.777l-0.148,0.203
c-0.03-0.043-0.06-0.088-0.091-0.13c-0.143-0.194-0.288-0.374-0.438-0.54c-1.55-1.727-3.449-2.003-4.456-2.011l0.371-0.698
l0.026-0.058c0.039-0.093,0.077-0.185,0.114-0.275c0.091-0.214,0.179-0.423,0.268-0.627c0.094-0.213,0.187-0.422,0.279-0.623
c0.916-2.004,1.768-3.388,2.523-4.343c0.136-0.173,0.271-0.332,0.4-0.478c0.152-0.169,0.3-0.321,0.44-0.456
c0.995-0.951,1.755-1.127,2.176-1.127c0.132,0,0.235,0.019,0.307,0.037c-0.06-0.104-0.095-0.16-0.099-0.166l0.046-0.029
c0.003,0.004,0.007,0.007,0.012,0.011l-0.031,0.019c0.003,0.006,0.013,0.062,0.072,0.166c0.015,0.004,0.027,0.007,0.038,0.01
c0.096,0.172,0.209,0.46,0.438,0.846c0.232,0.678,0.671,1.648,0.79,2.784L53,49.045c0,0.004,0,0.011,0,0.018l-0.162-0.062
c0.027,0.259,0.006,0.524,0.011,0.798c0.002,0.241-0.026,0.486-0.047,0.737L52.808,50.553z M61.043,60.258
c-0.156,0.124-0.317,0.246-0.482,0.366c-0.191-0.125-0.378-0.251-0.561-0.378c-0.213-0.147-0.421-0.295-0.622-0.441
c-1.861-1.366-3.118-2.632-3.146-2.659l-1.748-1.423l-0.583-0.475l-0.598-0.486l-0.461-0.375c0.557-0.956,0.924-1.977,1.101-3.04
c0.039-0.229,0.068-0.461,0.088-0.693c0.022-0.246,0.031-0.493,0.032-0.741c0.005-0.711-0.07-1.434-0.23-2.164
c-0.156-0.711-0.371-1.336-0.579-1.84l11.133,3.57c0.061,0.029,0.119,0.059,0.179,0.089c0.04,0.102,0.117,0.312,0.2,0.611
c0.184,0.666,0.396,1.773,0.295,3.104c-0.154,2.033-0.965,3.868-2.419,5.485c-0.309,0.344-0.646,0.677-1.012,1
C61.441,59.934,61.245,60.098,61.043,60.258z M71.878,61.25c-0.609,0.648-1.276,1.133-2.01,1.455
c-0.437,0.191-0.896,0.399-1.382,0.476c-0.315,0.049-0.643,0.148-0.98,0.148h-0.001c-1.953,0-3.997-0.893-5.798-1.927
c0.19-0.147,0.372-0.335,0.549-0.485c0.161-0.139,0.318-0.297,0.469-0.437c0.158-0.146,0.307-0.305,0.453-0.453
c0.023-0.024,0.049-0.054,0.073-0.08c0.233-0.243,0.448-0.488,0.649-0.732c0.193-0.237,0.375-0.477,0.542-0.714
c1.306-1.862,1.759-3.693,1.867-5.123c0.091-1.188-0.039-2.222-0.203-2.986c3.353,1.955,5.418,4.335,6.008,6.943
C72.567,59.333,72.021,60.896,71.878,61.25z'
/>
</g>
<circle fill='#DD6C01' cx='81.483' cy='57.609' r='1.819' />
<circle fill='#DD6C01' cx='85.483' cy='70.609' r='1.819' />
<circle fill='#FFE47B' cx='23.483' cy='60.609' r='1.819' />
</SvgIcon>
);
};
export default BadgeLimits;

View File

@ -0,0 +1,98 @@
import React from 'react';
import { SvgIcon, type SvgIconProps } from '@mui/material';
const BadgeLoved: React.FC<SvgIconProps> = (props) => {
return (
<SvgIcon {...props} viewBox='0 0 100 100'>
<path
fill='#FF9FB3'
d='M100,12.785C100,5.444,93.838,0,86.466,0H13.645C6.271,0,0,5.444,0,12.785v72.938
C0,93.064,6.271,100,13.645,100h72.821C93.838,100,100,93.064,100,85.723V12.785z'
/>
<g>
<path fill='#CF2B00' d='M17,30.843v-0.246C17,30.643,16.997,30.737,17,30.843z' />
<path
fill='#CF2B00'
d='M83.416,16H59.137H52.83h-1.967C50.387,16,50,16.387,50,16.863v0.273C50,17.613,50.387,18,50.863,18H54
h5.137h24.279C85.821,18,89,18.849,89,21.314v33.497C89,57.294,85.782,60,83.416,60h-7.754l-5.83,4.802L71.482,60H22.285
C19.841,60,19,57.172,19,54.812V33v-1v-1c0-0.553-0.447-1-1-1c-0.538,0-0.968,0.426-0.991,0.958
c-0.005-0.04-0.008-0.077-0.009-0.115V31v2v21.812C17,58.559,18.452,62,22.285,62H68.06l-3.349,9.739L76.538,62h6.878
C87.113,62,91,58.623,91,54.812V21.314C91,17.496,87.175,16,83.416,16z'
/>
<path
fill='#CF2B00'
d='M80.554,33H26.932c-0.451,0-0.814,0.551-0.814,1s0.363,1,0.814,1h53.622c0.449,0,0.814-0.551,0.814-1
S81.003,33,80.554,33z'
/>
<path
fill='#CF2B00'
d='M81.368,42c0-0.449-0.365-1-0.814-1H26.932c-0.451,0-0.814,0.551-0.814,1s0.363,1,0.814,1h53.622
C81.003,43,81.368,42.449,81.368,42z'
/>
<path
fill='#CF2B00'
d='M26.683,49c-0.284,0-0.512,0.545-0.512,1s0.228,1,0.512,1h33.216c0.282,0,0.511-0.545,0.511-1
s-0.229-1-0.511-1H26.683z'
/>
<path
fill='#CF2B00'
d='M31.007,16.333C31.022,16.35,31.015,16.314,31.007,16.333L31.007,16.333z'
/>
<path
fill='#CF2B00'
d='M18,24.707c0,0.415,0,0.83,0,1.245c0,0.38,0.078,0.595,0.234,0.767c0.219,0.24,0.234,0.188,0.426,0.281
h10.241c0.709,0,2.11-0.004,2.099-1.048v-8.894v-0.649c0-0.046,0.003-0.067,0.007-0.076c-0.006-0.006-0.015-0.017-0.03-0.049
C30.764,15.858,29.422,16,28.901,16h-8.143c0.01,0,0.021-0.687,0.03-0.824c0.115-1.422,0.311-2.85,0.8-4.197
c0.591-1.627,2.094-2.839,3.578-3.627c0.769-0.408,1.863-0.609,2.161-1.551c0.438-1.384-1.191-1.764-2.201-1.461
c-1.516,0.453-3.095,1.1-4.206,2.262c-1.041,1.09-1.793,2.518-2.247,3.946c-0.28,0.881-0.453,1.798-0.551,2.717
c-0.119,1.119-0.149,2.247-0.15,3.376c-0.002,1.063,0.023,2.127,0.024,3.186C17.999,21.453,18.003,23.08,18,24.707z'
/>
<path
fill='#CF2B00'
d='M36.129,76.546c-0.83-0.203-3.49-0.496-5.189-0.659c-0.685-1.565-1.717-3.837-2.148-4.533
c-0.274-0.44-0.654-0.98-1.23-0.965c-0.576-0.016-0.958,0.524-1.231,0.965c-0.43,0.696-1.465,2.968-2.148,4.533
c-1.698,0.163-4.359,0.456-5.188,0.659c-0.506,0.122-1.137,0.319-1.301,0.869c-0.191,0.542,0.203,1.073,0.538,1.47
c0.551,0.651,2.526,2.457,3.804,3.591c-0.465,1.645-1.145,4.195-1.256,5.035c-0.067,0.516-0.115,1.174,0.341,1.526
c0.434,0.375,1.071,0.199,1.563,0.031c0.835-0.286,3.373-1.547,4.879-2.323c1.504,0.776,4.045,2.037,4.879,2.323
c0.492,0.168,1.129,0.344,1.564-0.031c0.454-0.353,0.409-1.011,0.341-1.526c-0.112-0.84-0.793-3.391-1.258-5.035
c1.277-1.134,3.253-2.939,3.806-3.591c0.335-0.396,0.728-0.928,0.535-1.47C37.264,76.865,36.635,76.668,36.129,76.546z'
/>
<path
fill='#CF2B00'
d='M61.442,76.546c-0.828-0.203-3.49-0.496-5.188-0.659c-0.685-1.565-1.718-3.837-2.149-4.533
c-0.272-0.44-0.653-0.98-1.229-0.965c-0.575-0.016-0.957,0.524-1.23,0.965c-0.432,0.696-1.465,2.968-2.149,4.533
c-1.699,0.163-4.358,0.456-5.188,0.659c-0.505,0.122-1.137,0.319-1.3,0.869c-0.192,0.542,0.201,1.073,0.537,1.47
c0.551,0.651,2.525,2.457,3.805,3.591c-0.466,1.645-1.146,4.195-1.256,5.035c-0.068,0.516-0.115,1.174,0.34,1.526
c0.435,0.375,1.07,0.199,1.564,0.031c0.834-0.286,3.372-1.547,4.878-2.323c1.505,0.776,4.045,2.037,4.879,2.323
c0.491,0.168,1.129,0.344,1.564-0.031c0.454-0.353,0.409-1.011,0.34-1.526c-0.111-0.84-0.791-3.391-1.258-5.035
c1.278-1.134,3.254-2.939,3.806-3.591c0.336-0.396,0.729-0.928,0.537-1.47C62.579,76.865,61.949,76.668,61.442,76.546z'
/>
<path
fill='#CF2B00'
d='M86.758,76.546c-0.83-0.203-3.49-0.496-5.189-0.659c-0.685-1.565-1.718-3.837-2.148-4.533
c-0.273-0.44-0.654-0.98-1.23-0.965c-0.574-0.016-0.958,0.524-1.229,0.965c-0.433,0.696-1.465,2.968-2.149,4.533
c-1.699,0.163-4.359,0.456-5.188,0.659c-0.507,0.122-1.139,0.319-1.301,0.869c-0.191,0.542,0.202,1.073,0.538,1.47
c0.55,0.651,2.525,2.457,3.804,3.591c-0.466,1.645-1.145,4.195-1.255,5.035c-0.069,0.516-0.116,1.174,0.339,1.526
c0.435,0.375,1.071,0.199,1.563,0.031c0.835-0.286,3.374-1.547,4.879-2.323c1.505,0.776,4.045,2.037,4.879,2.323
c0.492,0.168,1.129,0.345,1.565-0.031c0.453-0.353,0.409-1.011,0.34-1.526c-0.111-0.84-0.792-3.391-1.258-5.035
c1.278-1.134,3.253-2.939,3.806-3.591c0.335-0.396,0.729-0.928,0.535-1.469C87.894,76.865,87.263,76.668,86.758,76.546z'
/>
<path
fill='#CF2B00'
d='M34,24.707c0,0.415,0,0.83,0,1.245c0,0.38,0.078,0.595,0.234,0.767c0.219,0.24,0.234,0.188,0.426,0.281
h10.241c0.709,0,2.11-0.004,2.099-1.048v-8.894v-0.649c0-0.046,0.003-0.067,0.007-0.076c-0.006-0.006-0.015-0.017-0.03-0.049
C46.764,15.858,45.422,16,44.901,16h-8.143c0.01,0,0.021-0.687,0.03-0.824c0.115-1.422,0.311-2.85,0.8-4.197
c0.591-1.627,2.094-2.839,3.578-3.627c0.769-0.408,1.863-0.609,2.161-1.551c0.438-1.384-1.191-1.764-2.201-1.461
c-1.516,0.453-3.095,1.1-4.206,2.262c-1.041,1.09-1.793,2.518-2.247,3.946c-0.28,0.881-0.453,1.798-0.551,2.717
c-0.119,1.119-0.149,2.247-0.15,3.376c-0.002,1.063,0.023,2.127,0.024,3.186C33.999,21.453,34.003,23.08,34,24.707z'
/>
<path
fill='#CF2B00'
d='M47.007,16.333C47.022,16.35,47.015,16.314,47.007,16.333L47.007,16.333z'
/>
</g>
</SvgIcon>
);
};
export default BadgeLoved;

View File

@ -0,0 +1,278 @@
import React from 'react';
import { SvgIcon, type SvgIconProps } from '@mui/material';
const BadgePrivacy: React.FC<SvgIconProps> = (props) => {
return (
<SvgIcon {...props} viewBox='0 0 100 100'>
<g>
<path
fill='#91EA9A'
d='M100,13.478C100,6.049,93.421,0,85.938,0H12.428C4.946,0,0,6.049,0,13.478v73.705
C0,94.611,4.946,101,12.428,101h73.511C93.421,101,100,94.611,100,87.183V13.478z'
/>
<g>
<path
fill='none'
d='M50.406,53.726C47.937,53.604,45.774,55,45.007,58h0.562h9.139h0.49C54.551,55,52.688,53.848,50.406,53.726z'
/>
<path
fill='none'
d='M64.986,21.303c-1.371-1.979-2.342-3.919-3.026-5.637l-15.166,0.258l-8.718-0.207
c-0.685,1.707-1.652,3.635-3.013,5.597c-3.098,4.467-8.825,9.942-19.044,12.47c-0.262,4.29-0.197,11.291,1.855,19.046
c1.092,4.13,2.607,7.956,4.539,11.473c0.13-0.057,0.28-0.07,0.425-0.021c0.367,0.122,2.048,0.173,2.776,0.106
c0.045-0.954,0.102-3.551,0.141-5.713c-0.653-0.275-1.112-0.923-1.112-1.676c0-1.001,0.813-1.815,1.816-1.815
c1.002,0,1.816,0.814,1.816,1.815c0,0.836-0.567,1.539-1.336,1.752c-0.104,5.926-0.172,6.169-0.195,6.252
c-0.048,0.171-0.167,0.314-0.334,0.404c-0.262,0.138-0.942,0.192-1.672,0.192c-0.574,0-0.769-0.033-1.226-0.083
C25.943,69.641,29,73.322,33,76.551v-4.978l-2.953-0.19l-0.141-4.706c-0.695-0.256-1.295-0.922-1.295-1.706
c0-1.001,0.763-1.815,1.765-1.815s1.792,0.814,1.792,1.815c0,0.811-0.545,1.498-1.277,1.732l0.266,3.595L34,70.483v7.083
c1,0.481,1,0.95,2,1.41V62.271c-1-0.288-1.494-0.918-1.494-1.648c0-1.001,0.596-1.814,1.598-1.814c1,0,2.057,0.813,2.057,1.814
c0,0.86-0.16,1.581-1.16,1.769v17.456c4,2.877,8.383,5.308,13.529,7.283C55.652,85.165,61,82.748,64,79.89V62.32
c0-0.26-1.328-0.922-1.328-1.698c0-1.001,0.738-1.814,1.74-1.814s1.844,0.813,1.844,1.814c0,0.817-0.256,1.51-1.256,1.738v16.66
c1-0.425,1-0.861,2-1.307v-7.231l2.383-0.191l-0.113-3.664c-0.631-0.285-1.133-0.919-1.133-1.656c0-1.001,0.785-1.815,1.786-1.815
c1.003,0,1.802,0.814,1.802,1.815c0,0.854-0.599,1.571-1.394,1.766l0.142,4.652L68,71.576v5.132c4-3.262,6.622-6.99,9.09-11.176
c-0.429,0.04-1.029,0.066-1.533,0.066c-0.73,0-1.448-0.055-1.71-0.192c-0.167-0.09-0.307-0.233-0.355-0.405
c-0.022-0.082-0.098-0.329-0.203-6.251c-0.77-0.213-1.341-0.916-1.341-1.752c0-1.001,0.813-1.815,1.814-1.815
c1.003,0,1.815,0.814,1.815,1.815c0,0.753-0.46,1.4-1.114,1.676c0.039,2.162,0.096,4.758,0.14,5.709
c0.729,0.067,2.41,0.019,2.776-0.104c0.095-0.032,0.189-0.035,0.28-0.021c1.85-3.374,3.317-7.032,4.395-10.971
c2.152-7.86,2.224-15.104,1.964-19.505C73.806,31.252,68.081,25.771,64.986,21.303z M41.152,26.227
c0.148-0.506,0.729-0.686,1.191-0.799c0.76-0.185,3.198-0.454,4.754-0.602c0.628-1.437,1.573-3.518,1.97-4.154
c0.25-0.404,0.601-0.899,1.127-0.885c0.527-0.015,0.877,0.48,1.127,0.885c0.396,0.637,1.342,2.718,1.97,4.154
c1.556,0.147,3.995,0.417,4.754,0.602c0.464,0.113,1.041,0.293,1.191,0.799c0.132,0.372-0.039,0.738-0.262,1.053
c-0.074,0.104-0.153,0.203-0.23,0.293c-0.506,0.596-2.314,2.251-3.486,3.289c0.1,0.353,0.21,0.75,0.322,1.162
c0.368,1.353,0.753,2.862,0.831,3.452c0.045,0.342,0.079,0.754-0.064,1.077c-0.056,0.123-0.134,0.232-0.248,0.32
c-0.399,0.346-0.983,0.184-1.434,0.029c-0.764-0.262-3.092-1.416-4.471-2.129c-1.379,0.713-3.705,1.867-4.47,2.129
c-0.451,0.154-1.035,0.316-1.434-0.029c-0.114-0.088-0.193-0.197-0.247-0.32c-0.145-0.323-0.109-0.735-0.063-1.077
c0.077-0.59,0.461-2.1,0.828-3.452c0.112-0.412,0.223-0.81,0.322-1.162c-1.172-1.038-2.98-2.693-3.485-3.289
c-0.077-0.09-0.156-0.189-0.23-0.293C41.19,26.965,41.02,26.598,41.152,26.227z M38.479,48.123
c-0.054,0.123-0.133,0.232-0.247,0.322c-0.399,0.345-0.983,0.184-1.433,0.028c-0.766-0.261-3.093-1.417-4.473-2.128
c-1.379,0.711-3.704,1.867-4.47,2.128c-0.451,0.155-1.033,0.316-1.433-0.028c-0.114-0.09-0.193-0.199-0.248-0.322
c-0.143-0.323-0.109-0.734-0.064-1.077c0.078-0.591,0.463-2.099,0.829-3.45c0.112-0.412,0.223-0.811,0.322-1.163
c-1.171-1.037-2.98-2.692-3.484-3.29c-0.077-0.091-0.157-0.189-0.231-0.293c-0.223-0.313-0.394-0.68-0.263-1.053
c0.15-0.505,0.729-0.686,1.192-0.798c0.759-0.185,3.196-0.454,4.755-0.604c0.627-1.434,1.574-3.514,1.968-4.15
c0.252-0.406,0.601-0.9,1.127-0.887c0.528-0.014,0.877,0.48,1.127,0.887c0.396,0.637,1.342,2.717,1.97,4.15
c1.556,0.15,3.996,0.42,4.754,0.604c0.464,0.112,1.042,0.293,1.192,0.798c0.133,0.373-0.038,0.739-0.263,1.053
c-0.073,0.104-0.153,0.202-0.229,0.293c-0.505,0.598-2.316,2.253-3.487,3.29c0.1,0.353,0.21,0.751,0.322,1.163
c0.368,1.352,0.752,2.859,0.83,3.45C38.589,47.389,38.623,47.8,38.479,48.123z M61,59.447v14.64C61,74.849,59.623,76,58.862,76
H41.19C40.429,76,40,74.849,40,74.087v-14.64v-1.208c0-0.639,0.344-1.192,0.934-1.348c-0.002,0.003-0.155,0.01-0.155,0.015
c0.072-0.042,0.055,0.586,0.142,0.553C40.925,57.416,40.834,58,40.84,58c0.003,0,0.004,0,0.007,0c0.903-5,4.992-9.174,9.772-8.915
c4.448,0.235,7.996,3.735,8.729,8.325c0.006,0.035,0.007-0.094,0.013-0.06c0.07,0.042,0.128,0.004,0.189,0.054
c-0.013-0.123,0.202-0.286,0.181-0.409C60.251,57.192,61,57.651,61,58.239V59.447z M76.504,38.85
c-0.073,0.104-0.153,0.202-0.23,0.293c-0.504,0.598-2.314,2.252-3.485,3.29c0.1,0.353,0.21,0.751,0.322,1.163
c0.367,1.352,0.752,2.859,0.83,3.45c0.045,0.343,0.079,0.754-0.064,1.076c-0.056,0.124-0.134,0.233-0.248,0.323
c-0.397,0.345-0.982,0.184-1.433,0.028c-0.765-0.263-3.092-1.417-4.471-2.128c-1.378,0.711-3.705,1.865-4.47,2.128
c-0.451,0.155-1.035,0.316-1.434-0.028c-0.114-0.09-0.192-0.199-0.247-0.32c-0.145-0.324-0.11-0.736-0.063-1.079
c0.077-0.591,0.461-2.099,0.828-3.45c0.111-0.412,0.222-0.811,0.321-1.163c-1.171-1.038-2.979-2.692-3.485-3.29
c-0.077-0.091-0.156-0.188-0.23-0.293c-0.224-0.313-0.394-0.68-0.262-1.053c0.15-0.506,0.728-0.686,1.191-0.798
c0.76-0.185,3.197-0.456,4.754-0.603c0.628-1.436,1.574-3.516,1.97-4.152c0.251-0.406,0.601-0.9,1.127-0.887
c0.526-0.014,0.877,0.48,1.127,0.887c0.396,0.637,1.342,2.717,1.97,4.152c1.556,0.146,3.994,0.418,4.754,0.603
c0.462,0.112,1.041,0.292,1.191,0.798C76.899,38.17,76.729,38.536,76.504,38.85z'
/>
<path
fill='none'
d='M50.579,50.516c-0.063-0.002-0.137-0.349-0.21-0.364c-0.072-0.017-0.146,0.461-0.21,0.461
c-3.841,0-7.092,2.388-7.936,7.388h0.36h1.018c0.825-4,3.618-6.283,6.876-6.121c2.933,0.156,6.007,1.992,6.761,5.258
C57.262,57.135,58,56.795,58,56.792c0-0.001,0-0.018,0-0.018l-0.689,0.673c-0.01,0-0.709,0.553-0.717,0.553h1.048h0.229
C57,53,54.144,50.705,50.579,50.516z'
/>
<path
fill='none'
d='M65.187,60.622c0-0.468-0.381-0.849-0.851-0.849c-0.469,0-0.851,0.381-0.851,0.849
c0,0.246,0.105,0.467,0.272,0.622c0.151,0.143,0.354,0.229,0.578,0.229c0.186,0,0.357-0.061,0.497-0.161
C65.047,61.157,65.187,60.905,65.187,60.622z'
/>
<path
fill='none'
d='M36.733,60.622c0-0.468-0.382-0.849-0.852-0.849c-0.469,0-0.85,0.381-0.85,0.849
c0,0.208,0.075,0.397,0.198,0.546c0.156,0.187,0.391,0.306,0.651,0.306c0.145,0,0.279-0.036,0.398-0.099
C36.549,61.231,36.733,60.948,36.733,60.622z'
/>
<path
fill='none'
d='M52.479,61.55c-0.219-0.181-0.625-0.333-0.886-0.453c-0.419-0.189-0.346-0.297-0.834-0.297
c-1.814,0-2.76,1.479-2.76,3.291c0,0.001,0,0.001,0,0.003c0,0.202-0.585,0.398-0.55,0.592c-0.035,0.192-0.351,0.392-0.351,0.595
c0-0.203-0.135-0.517-0.1-0.709C47.215,65.76,47.933,67,49.052,67c0.001,0-0.009,0-0.008,0c-0.08,1-0.171,0.815-0.262,1.183
c-0.2,0.823-0.419,1.73-0.601,2.411c-0.237,0.896-0.254,1.277-0.254,1.277c0.023,0.117,0.079,0.501,0.152,0.573
C48.307,72.67,48.69,73,48.69,73h0.761h0.617h0.612h0.751c0,0,0.435-0.418,0.596-0.594l0,0c0.007-0.01,0.015-0.156,0.021-0.164
c0.012-0.017,0.429-0.101,0.438-0.118C52.555,72.022,53,71.841,53,71.688c0-0.01,0-0.02,0-0.03c0-0.159-0.447-0.528-0.525-0.825
C52.281,70.111,51.711,70,51.47,68c0.001,0-0.046,0-0.046,0c-0.096-1-0.213-0.604-0.298-0.965c0-0.004,0.339,0.077,0.338,0.072
C52.584,66.74,53,65.86,54,64.687C54,64.683,54,65,54,65h-0.638C53.398,65,54,64.286,54,64.079c0-0.001,0-0.001,0-0.002
C54,63.06,53.208,62.15,52.479,61.55z M50.058,60.726c0.001,0,0.001,0,0.002,0c0.351,0,0.687,0.058,1.004,0.16
C50.745,60.783,50.406,60.726,50.058,60.726z'
/>
<path
fill='none'
d='M73.759,56.149c-0.469,0-0.85,0.381-0.85,0.849c0,0.282,0.139,0.533,0.351,0.688
c0.142,0.102,0.312,0.163,0.499,0.163c0.28,0,0.529-0.137,0.684-0.347c0.104-0.142,0.167-0.315,0.167-0.505
C74.609,56.53,74.229,56.149,73.759,56.149z'
/>
<path
fill='none'
d='M31.176,64.971c0-0.468-0.381-0.849-0.851-0.849c-0.469,0-0.85,0.381-0.85,0.849
c0,0.231,0.092,0.442,0.241,0.595c0.155,0.158,0.37,0.258,0.608,0.258c0.218,0,0.416-0.084,0.567-0.219
C31.066,65.448,31.176,65.223,31.176,64.971z'
/>
<path
fill='none'
d='M26.958,57.687c0.213-0.155,0.352-0.406,0.352-0.688c0-0.468-0.381-0.849-0.85-0.849
c-0.47,0-0.851,0.381-0.851,0.849c0,0.189,0.062,0.363,0.167,0.505c0.154,0.21,0.403,0.347,0.684,0.347
C26.646,57.85,26.817,57.788,26.958,57.687z'
/>
<path
fill='none'
d='M69.205,18.381c-2.209-3.189-3.136-6.219-3.511-7.91L46.843,10.79l-12.504-0.294
c-0.964,4.14-5.284,16.263-23.051,19.004c-0.433,3.211-1.389,13.333,1.653,24.751c4.889,18.347,17.261,31.211,36.775,38.238
l0.287,0.072l0.348-0.078c19.505-7.027,31.871-19.891,36.759-38.23c3.041-11.418,2.085-21.54,1.654-24.75
C77.828,27.814,72.103,22.562,69.205,18.381z M83.506,53.687c-4.507,16.467-15.59,28.19-32.942,34.848l-0.538,0.207l-0.54-0.207
c-17.522-6.723-28.647-18.605-33.066-35.32c-2.077-7.854-2.182-14.95-1.902-19.521l0.067-1.104l1.074-0.265
c9.915-2.453,15.352-7.808,18.169-11.867c1.144-1.651,2.105-3.435,2.853-5.3l0.389-0.969l9.761,0.23l16.144-0.275l0.385,0.965
c0.749,1.877,1.713,3.674,2.866,5.339c2.812,4.062,8.247,9.419,18.156,11.877l1.075,0.265l0.065,1.105
C85.801,38.434,85.68,45.75,83.506,53.687z'
/>
<path
fill='none'
d='M70.324,65.703c0.252-0.147,0.419-0.42,0.419-0.732c0-0.468-0.381-0.849-0.85-0.849
c-0.47,0-0.851,0.381-0.851,0.849c0,0.15,0.039,0.291,0.106,0.413c0.146,0.262,0.424,0.439,0.744,0.439
C70.052,65.823,70.198,65.779,70.324,65.703z'
/>
</g>
</g>
<g>
<path
fill='none'
d='M50.406,53.726C47.937,53.604,45.774,55,45.007,58h0.562h9.139h0.49C54.551,55,52.688,53.848,50.406,53.726z'
/>
<path
fill='none'
d='M47.445,66.015c0.214,0.31,0.471,0.573,0.767,0.748C47.917,66.556,47.663,66.3,47.445,66.015z'
/>
<path
fill='none'
d='M49.017,67.176c-0.075,0.794-0.155,0.699-0.239,1.042c-0.201,0.823-0.417,1.821-0.598,2.503
c-0.239,0.894-0.254,1.32-0.254,1.32c0.023,0.116,0.08,0.346,0.153,0.418C48.306,72.684,48.69,73,48.69,73h0.761h0.617h0.612h0.751
c0,0,0.536-0.418,0.697-0.594c0.007-0.01,0.115-0.297,0.121-0.304c0.012-0.017,0.227-0.032,0.236-0.048
c0.068-0.104-1.303,0.041-1.303-0.112c0-0.011,0.463-0.101,0.452-0.101c-1.239,0.009-0.07-0.629-0.15-0.928
C51.292,70.195,51.616,70,51.376,68c0.001,0,0.003,0,0.003,0c-0.079,0-0.145-0.401-0.212-0.746
c-0.304,0.086-0.618,0.146-0.949,0.146C49.794,67.4,49.393,67.313,49.017,67.176z'
/>
<path
fill='none'
d='M50.579,50.023c-0.063-0.003-0.347,0.027-0.412,0.027c-3.841,0-7.1,2.949-7.944,7.949h0.36h1.018
c0.825-4,3.618-6.264,6.876-6.101c2.884,0.153,5.29,1.955,6.084,5.131c0.427-0.009,0.989-0.097,1.454-0.062
C57.144,53.209,54.143,50.213,50.579,50.023z'
/>
<g>
<path
fill='#018B21'
d='M91.562,27.889l-0.202-1.074l-1.086-0.139C70.261,24.119,68.516,9.418,68.452,8.799l-0.134-1.385
L46.854,7.779L31.735,7.423l-0.135,1.393c-0.004,0.038-0.395,3.771-3.229,7.858c-3.848,5.551-10.104,8.916-18.595,9.998
l-1.085,0.139l-0.204,1.076c-0.095,0.507-2.302,12.593,1.514,27.023C13.536,68.275,23.13,86.142,48.77,95.348l1.197,0.309
l1.139-0.258l0.176-0.051c25.64-9.206,35.233-27.071,38.77-40.437C93.864,40.482,91.659,28.396,91.562,27.889z M87.11,54.253
c-4.888,18.34-17.254,31.203-36.759,38.23l-0.348,0.078l-0.287-0.072C30.202,85.462,17.83,72.598,12.941,54.251
c-3.042-11.418-2.086-21.54-1.653-24.751c17.767-2.741,22.087-14.864,23.051-19.004l12.504,0.294l18.852-0.319
c0.375,1.691,1.302,4.721,3.511,7.91c2.897,4.182,8.623,9.434,19.56,11.122C89.195,32.713,90.151,42.835,87.11,54.253z'
/>
<path
fill='#018B21'
d='M85.455,32.588l-1.075-0.265c-9.909-2.458-15.344-7.814-18.156-11.877
c-1.153-1.665-2.117-3.462-2.866-5.339l-0.385-0.965l-16.144,0.275l-9.761-0.23l-0.389,0.969c-0.747,1.865-1.709,3.648-2.853,5.3
c-2.817,4.06-8.254,9.414-18.169,11.867l-1.074,0.265l-0.067,1.104c-0.279,4.571-0.175,11.668,1.902,19.521
c4.419,16.715,15.544,28.598,33.066,35.32l0.54,0.207l0.538-0.207c17.353-6.657,28.436-18.381,32.942-34.848
c2.174-7.937,2.295-15.253,2.015-19.993L85.455,32.588z M36.28,61.375c-0.119,0.062-0.254,0.099-0.398,0.099
c-0.261,0-0.495-0.119-0.651-0.306c-0.123-0.148-0.198-0.338-0.198-0.546c0-0.468,0.381-0.849,0.85-0.849
c0.47,0,0.852,0.381,0.852,0.849C36.733,60.948,36.549,61.231,36.28,61.375z M64.336,61.474c-0.224,0-0.427-0.087-0.578-0.229
c-0.167-0.155-0.272-0.376-0.272-0.622c0-0.468,0.382-0.849,0.851-0.849c0.47,0,0.851,0.381,0.851,0.849
c0,0.283-0.14,0.535-0.354,0.69C64.693,61.413,64.521,61.474,64.336,61.474z M82.054,53.288
c-1.077,3.938-2.545,7.597-4.395,10.971c-0.091-0.015-0.186-0.012-0.28,0.021c-0.366,0.122-2.047,0.171-2.776,0.104
c-0.044-0.951-0.101-3.547-0.14-5.709c0.654-0.275,1.113-0.923,1.113-1.676c0-1.001-0.815-1.815-1.817-1.815
s-1.817,0.814-1.817,1.815c0,0.836,0.568,1.539,1.337,1.752c0.105,5.922,0.173,6.169,0.195,6.251
c0.048,0.172,0.167,0.315,0.334,0.405c0.262,0.138,0.941,0.192,1.672,0.192c0.502,0,1.182-0.026,1.61-0.066
C74.622,69.718,72,73.446,68,76.708v-5.132l2.468-0.188L70.4,66.736c0.795-0.194,1.35-0.911,1.35-1.766
c0-1.001-0.836-1.815-1.838-1.815s-1.826,0.814-1.826,1.815c0,0.737,0.436,1.371,1.066,1.656l0.232,3.664L67,70.482v7.231
c-1,0.445-1,0.882-2,1.307V62.36c1-0.229,1.219-0.921,1.219-1.738c0-1.001-0.849-1.814-1.852-1.814
c-1.002,0-1.68,0.813-1.68,1.814c0,0.776,1.312,1.438,1.312,1.698V79.89c-3,2.858-8.348,5.275-13.471,7.24
C45.383,85.154,41,82.724,37,79.847V62.391c1-0.188,1.051-0.908,1.051-1.769c0-1.001-0.991-1.814-1.993-1.814
c-1,0-1.465,0.813-1.465,1.814c0,0.73,0.407,1.36,1.407,1.648v16.706c-1-0.46-1-0.929-2-1.41v-7.083l-2.851-0.186l-0.111-3.595
c0.732-0.234,1.184-0.922,1.184-1.732c0-1.001-0.854-1.815-1.856-1.815s-1.837,0.814-1.837,1.815c0,0.784,0.489,1.45,1.185,1.706
l0.339,4.706L33,71.573v4.978c-4-3.229-7.057-6.91-9.486-11.035c0.457,0.05,0.855,0.083,1.43,0.083c0.73,0,1.31-0.055,1.57-0.192
c0.168-0.09,0.234-0.233,0.281-0.404c0.024-0.083,0.066-0.326,0.171-6.252c0.769-0.213,1.324-0.916,1.324-1.752
c0-1.001-0.822-1.815-1.824-1.815s-1.819,0.814-1.819,1.815c0,0.753,0.457,1.4,1.11,1.676c-0.038,2.162-0.097,4.759-0.142,5.713
c-0.729,0.066-2.409,0.016-2.776-0.106c-0.145-0.049-0.295-0.035-0.425,0.021c-1.932-3.517-3.447-7.343-4.539-11.473
c-2.053-7.755-2.117-14.756-1.855-19.046c10.219-2.527,15.946-8.003,19.044-12.47c1.36-1.962,2.328-3.89,3.013-5.597l8.718,0.207
l15.166-0.258c0.685,1.718,1.655,3.658,3.026,5.637c3.095,4.469,8.819,9.949,19.031,12.48
C84.277,38.185,84.206,45.428,82.054,53.288z M74.442,57.503c-0.154,0.21-0.403,0.347-0.684,0.347
c-0.187,0-0.357-0.062-0.499-0.163c-0.212-0.155-0.351-0.406-0.351-0.688c0-0.468,0.381-0.849,0.85-0.849
c0.47,0,0.851,0.381,0.851,0.849C74.609,57.188,74.547,57.361,74.442,57.503z M69.894,65.823c-0.32,0-0.598-0.178-0.744-0.439
c-0.067-0.122-0.106-0.263-0.106-0.413c0-0.468,0.381-0.849,0.851-0.849c0.469,0,0.85,0.381,0.85,0.849
c0,0.312-0.167,0.585-0.419,0.732C70.198,65.779,70.052,65.823,69.894,65.823z M30.893,65.604
c-0.151,0.135-0.35,0.219-0.567,0.219c-0.238,0-0.453-0.1-0.608-0.258c-0.149-0.152-0.241-0.363-0.241-0.595
c0-0.468,0.381-0.849,0.85-0.849c0.47,0,0.851,0.381,0.851,0.849C31.176,65.223,31.066,65.448,30.893,65.604z M25.776,57.503
c-0.104-0.142-0.167-0.315-0.167-0.505c0-0.468,0.381-0.849,0.851-0.849c0.469,0,0.85,0.381,0.85,0.849
c0,0.282-0.139,0.533-0.352,0.688c-0.141,0.102-0.312,0.163-0.498,0.163C26.18,57.85,25.931,57.713,25.776,57.503z'
/>
<path
fill='#018B21'
d='M37.713,43.596c-0.112-0.412-0.223-0.811-0.322-1.163c1.171-1.037,2.982-2.692,3.487-3.29
c0.075-0.091,0.155-0.188,0.229-0.293c0.225-0.313,0.396-0.68,0.263-1.053c-0.15-0.505-0.729-0.686-1.192-0.798
c-0.758-0.185-3.198-0.454-4.754-0.604c-0.628-1.434-1.573-3.514-1.97-4.15c-0.25-0.406-0.599-0.9-1.127-0.887
c-0.526-0.014-0.875,0.48-1.127,0.887c-0.394,0.637-1.341,2.717-1.968,4.15c-1.559,0.15-3.996,0.42-4.755,0.604
c-0.464,0.112-1.042,0.293-1.192,0.798c-0.131,0.373,0.04,0.739,0.263,1.053c0.074,0.104,0.154,0.202,0.231,0.293
c0.504,0.598,2.313,2.253,3.484,3.29c-0.1,0.353-0.21,0.751-0.322,1.163c-0.366,1.352-0.751,2.859-0.829,3.45
c-0.045,0.343-0.078,0.754,0.064,1.077c0.055,0.123,0.134,0.232,0.248,0.322c0.399,0.345,0.981,0.184,1.433,0.028
c0.766-0.261,3.091-1.417,4.47-2.128c1.38,0.711,3.707,1.867,4.473,2.128c0.449,0.155,1.033,0.316,1.433-0.028
c0.114-0.09,0.193-0.199,0.247-0.322c0.145-0.323,0.11-0.734,0.064-1.077C38.465,46.455,38.081,44.947,37.713,43.596z'
/>
<path
fill='#018B21'
d='M75.575,36.999c-0.76-0.185-3.198-0.456-4.754-0.603c-0.628-1.436-1.574-3.516-1.97-4.152
c-0.25-0.406-0.601-0.9-1.127-0.887c-0.526-0.014-0.876,0.48-1.127,0.887c-0.396,0.637-1.342,2.717-1.97,4.152
c-1.557,0.146-3.994,0.418-4.754,0.603c-0.464,0.112-1.041,0.292-1.191,0.798c-0.132,0.373,0.038,0.739,0.262,1.053
c0.074,0.104,0.153,0.202,0.23,0.293c0.506,0.598,2.314,2.252,3.485,3.29c-0.1,0.353-0.21,0.751-0.321,1.163
c-0.367,1.352-0.751,2.859-0.828,3.45c-0.047,0.343-0.081,0.755,0.063,1.079c0.055,0.121,0.133,0.23,0.247,0.32
c0.398,0.345,0.982,0.184,1.434,0.028c0.765-0.263,3.092-1.417,4.47-2.128c1.379,0.711,3.706,1.865,4.471,2.128
c0.45,0.155,1.035,0.316,1.433-0.028c0.114-0.09,0.192-0.199,0.248-0.323c0.144-0.322,0.109-0.733,0.064-1.076
c-0.078-0.591-0.463-2.099-0.83-3.45c-0.112-0.412-0.223-0.811-0.322-1.163c1.171-1.038,2.981-2.692,3.485-3.29
c0.077-0.091,0.157-0.188,0.23-0.293c0.225-0.313,0.396-0.68,0.263-1.053C76.616,37.291,76.037,37.111,75.575,36.999z'
/>
<path
fill='#018B21'
d='M41.646,27.572c0.505,0.596,2.313,2.251,3.485,3.289c-0.1,0.353-0.21,0.75-0.322,1.162
c-0.367,1.353-0.751,2.862-0.828,3.452c-0.046,0.342-0.081,0.754,0.063,1.077c0.054,0.123,0.133,0.232,0.247,0.32
c0.398,0.346,0.982,0.184,1.434,0.029c0.765-0.262,3.091-1.416,4.47-2.129c1.379,0.713,3.707,1.867,4.471,2.129
c0.45,0.154,1.034,0.316,1.434-0.029c0.114-0.088,0.192-0.197,0.248-0.32c0.144-0.323,0.109-0.735,0.064-1.077
c-0.078-0.59-0.463-2.1-0.831-3.452c-0.112-0.412-0.223-0.81-0.322-1.162c1.172-1.038,2.98-2.693,3.486-3.289
c0.077-0.09,0.156-0.189,0.23-0.293c0.223-0.314,0.394-0.681,0.262-1.053c-0.15-0.506-0.728-0.686-1.191-0.799
c-0.759-0.185-3.198-0.454-4.754-0.602c-0.628-1.437-1.574-3.518-1.97-4.154c-0.25-0.404-0.6-0.899-1.127-0.885
c-0.526-0.015-0.877,0.48-1.127,0.885c-0.396,0.637-1.342,2.718-1.97,4.154c-1.556,0.147-3.994,0.417-4.754,0.602
c-0.463,0.113-1.043,0.293-1.191,0.799c-0.133,0.371,0.038,0.738,0.263,1.053C41.489,27.383,41.568,27.482,41.646,27.572z'
/>
<path
fill='#018B21'
d='M50.058,60.726c0.349,0,0.688,0.058,1.006,0.16c-0.317-0.103-0.653-0.16-1.004-0.16
C50.059,60.726,50.059,60.726,50.058,60.726z'
/>
<path
fill='#018B21'
d='M60,57h-0.164c-0.035-0.015-0.071-0.032-0.104-0.045c0.002,0.011-0.116,0.021-0.114,0.031
c-0.057-0.021-0.175-0.041-0.234-0.061c-0.789-4.52-4.337-7.609-8.736-7.841c-4.457-0.241-8.334,3.372-9.571,7.915H41
c-0.552,0-1,0.447-1,1c0,0.035,0.017,0.065,0.02,0.101C40.016,58.148,40,58.188,40,58.239v1.208v14.64
C40,74.849,40.429,76,41.19,76h17.672C59.623,76,61,74.849,61,74.087v-14.64v-1.208c0-0.046-0.012-0.091-0.021-0.136
C60.983,58.068,61,58.037,61,58C61,57.447,60.553,57,60,57z M50.167,50.051c0.064,0,0.348-0.03,0.412-0.027
c3.563,0.189,6.564,3.186,7.437,6.945c-0.316-0.023-0.664,0.005-0.997,0.031h-0.467c-0.802-3.156-3.2-4.947-6.074-5.101
c-2.974-0.148-5.556,1.729-6.611,5.101h-1.433C43.521,52.61,46.591,50.051,50.167,50.051z M45.341,57
c0.955-2.308,2.889-3.381,5.065-3.274c2.008,0.107,3.688,1.016,4.505,3.274H45.341z'
/>
</g>
</g>
<path
fill='#91EA9A'
d='M53.719,63.898c0-1.934-1.567-3.501-3.501-3.501c-1.934,0-3.501,1.567-3.501,3.501
c0,0.287,0.044,0.562,0.109,0.829c0.007-0.053,0.011-0.109,0.02-0.156c0.027,0.152,0.066,0.305,0.113,0.455
c0.111,0.354,0.279,0.69,0.486,0.988c0.218,0.285,0.473,0.541,0.767,0.748c0.222,0.131,0.466,0.205,0.724,0.223
c-0.125,0.683-1.04,5.233-1.04,5.233s-0.301,1.079,2.058,0.929l2.61-0.126c0,0,0.683,0.309-1.294-5.806
C52.685,66.767,53.719,65.462,53.719,63.898z'
/>
</SvgIcon>
);
};
export default BadgePrivacy;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function BasqueCountryFlag(props) { const BasqueCountryFlag: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 50 28'> <SvgIcon {...props} x='0px' y='0px' viewBox='0 0 50 28'>
<path d='M0,0 v28 h50 v-28 z' fill='#D52B1E' /> <path d='M0,0 v28 h50 v-28 z' fill='#D52B1E' />
@ -9,4 +9,6 @@ export default function BasqueCountryFlag(props) {
<path d='M25,0 v28 M0,14 h50' stroke='#fff' strokeWidth='4.3' /> <path d='M25,0 v28 M0,14 h50' stroke='#fff' strokeWidth='4.3' />
</SvgIcon> </SvgIcon>
); );
} };
export default BasqueCountryFlag;

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function BitcoinIcon(props) { const Bitcoin: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 512 512'> <SvgIcon sx={props.sx} color={props.color} viewBox='0 0 512 512'>
<path d='M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zm-141.651-35.33c4.937-32.999-20.191-50.739-54.55-62.573l11.146-44.702-27.213-6.781-10.851 43.524c-7.154-1.783-14.502-3.464-21.803-5.13l10.929-43.81-27.198-6.781-11.153 44.686c-5.922-1.349-11.735-2.682-17.377-4.084l.031-.14-37.53-9.37-7.239 29.062s20.191 4.627 19.765 4.913c11.022 2.751 13.014 10.044 12.68 15.825l-12.696 50.925c.76.194 1.744.473 2.829.907-.907-.225-1.876-.473-2.876-.713l-17.796 71.338c-1.349 3.348-4.767 8.37-12.471 6.464.271.395-19.78-4.937-19.78-4.937l-13.51 31.147 35.414 8.827c6.588 1.651 13.045 3.379 19.4 5.006l-11.262 45.213 27.182 6.781 11.153-44.733a1038.209 1038.209 0 0 0 21.687 5.627l-11.115 44.523 27.213 6.781 11.262-45.128c46.404 8.781 81.299 5.239 95.986-36.727 11.836-33.79-.589-53.281-25.004-65.991 17.78-4.098 31.174-15.792 34.747-39.949zm-62.177 87.179c-8.41 33.79-65.308 15.523-83.755 10.943l14.944-59.899c18.446 4.603 77.6 13.717 68.811 48.956zm8.417-87.667c-7.673 30.736-55.031 15.12-70.393 11.292l13.548-54.327c15.363 3.828 64.836 10.973 56.845 43.035z' /> <path d='M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zm-141.651-35.33c4.937-32.999-20.191-50.739-54.55-62.573l11.146-44.702-27.213-6.781-10.851 43.524c-7.154-1.783-14.502-3.464-21.803-5.13l10.929-43.81-27.198-6.781-11.153 44.686c-5.922-1.349-11.735-2.682-17.377-4.084l.031-.14-37.53-9.37-7.239 29.062s20.191 4.627 19.765 4.913c11.022 2.751 13.014 10.044 12.68 15.825l-12.696 50.925c.76.194 1.744.473 2.829.907-.907-.225-1.876-.473-2.876-.713l-17.796 71.338c-1.349 3.348-4.767 8.37-12.471 6.464.271.395-19.78-4.937-19.78-4.937l-13.51 31.147 35.414 8.827c6.588 1.651 13.045 3.379 19.4 5.006l-11.262 45.213 27.182 6.781 11.153-44.733a1038.209 1038.209 0 0 0 21.687 5.627l-11.115 44.523 27.213 6.781 11.262-45.128c46.404 8.781 81.299 5.239 95.986-36.727 11.836-33.79-.589-53.281-25.004-65.991 17.78-4.098 31.174-15.792 34.747-39.949zm-62.177 87.179c-8.41 33.79-65.308 15.523-83.755 10.943l14.944-59.899c18.446 4.603 77.6 13.717 68.811 48.956zm8.417-87.667c-7.673 30.736-55.031 15.12-70.393 11.292l13.548-54.327c15.363 3.828 64.836 10.973 56.845 43.035z' />
</SvgIcon> </SvgIcon>
); );
} };
export default Bitcoin;

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function BitcoinSignIcon(props) { const BitcoinSign: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 320 512'> <SvgIcon sx={props.sx} color={props.color} viewBox='0 0 320 512'>
<path d='M48 32C48 14.33 62.33 0 80 0C97.67 0 112 14.33 112 32V64H144V32C144 14.33 158.3 0 176 0C193.7 0 208 14.33 208 32V64C208 65.54 207.9 67.06 207.7 68.54C254.1 82.21 288 125.1 288 176C288 200.2 280.3 222.6 267.3 240.9C298.9 260.7 320 295.9 320 336C320 397.9 269.9 448 208 448V480C208 497.7 193.7 512 176 512C158.3 512 144 497.7 144 480V448H112V480C112 497.7 97.67 512 80 512C62.33 512 48 497.7 48 480V448H41.74C18.69 448 0 429.3 0 406.3V101.6C0 80.82 16.82 64 37.57 64H48V32zM176 224C202.5 224 224 202.5 224 176C224 149.5 202.5 128 176 128H64V224H176zM64 288V384H208C234.5 384 256 362.5 256 336C256 309.5 234.5 288 208 288H64z' /> <path d='M48 32C48 14.33 62.33 0 80 0C97.67 0 112 14.33 112 32V64H144V32C144 14.33 158.3 0 176 0C193.7 0 208 14.33 208 32V64C208 65.54 207.9 67.06 207.7 68.54C254.1 82.21 288 125.1 288 176C288 200.2 280.3 222.6 267.3 240.9C298.9 260.7 320 295.9 320 336C320 397.9 269.9 448 208 448V480C208 497.7 193.7 512 176 512C158.3 512 144 497.7 144 480V448H112V480C112 497.7 97.67 512 80 512C62.33 512 48 497.7 48 480V448H41.74C18.69 448 0 429.3 0 406.3V101.6C0 80.82 16.82 64 37.57 64H48V32zM176 224C202.5 224 224 202.5 224 176C224 149.5 202.5 128 176 128H64V224H176zM64 288V384H208C234.5 384 256 362.5 256 336C256 309.5 234.5 288 208 288H64z' />
</SvgIcon> </SvgIcon>
); );
} };
export default BitcoinSign;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function BuySatsIcon(props) { const BuySats: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'> <SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g> <g>
@ -134,4 +134,6 @@ export default function BuySatsIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default BuySats;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function BuySatsCheckedIcon(props) { const BuySatsChecked: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'> <SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g> <g>
@ -76,4 +76,6 @@ export default function BuySatsCheckedIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default BuySatsChecked;

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function CataloniaFlag(props) { const CataloniaFlag: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 810 540'> <SvgIcon {...props} x='0px' y='0px' viewBox='0 0 810 540'>
<rect width='810' height='540' fill='#FCDD09' /> <rect width='810' height='540' fill='#FCDD09' />
<path stroke='#DA121A' strokeWidth='60' d='M0,90H810m0,120H0m0,120H810m0,120H0' /> <path stroke='#DA121A' strokeWidth='60' d='M0,90H810m0,120H0m0,120H810m0,120H0' />
</SvgIcon> </SvgIcon>
); );
} };
export default CataloniaFlag;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function EarthIcon(props) { const Earth: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 440.45 440.45'> <SvgIcon {...props} x='0px' y='0px' viewBox='0 0 440.45 440.45'>
<g id='XMLID_34_'> <g id='XMLID_34_'>
@ -61,11 +61,13 @@ export default function EarthIcon(props) {
</g> </g>
</g> </g>
<path <path
style={{ opacity: '0.3', fill: '#808080', enableBackground: 'new' }} style={{ opacity: '0.3', fill: '#808080' }}
d='M190.23,0.005c5.8,0,11.54,0.26,17.2,0.77 d='M190.23,0.005c5.8,0,11.54,0.26,17.2,0.77
C110.48,9.515,34.5,90.995,34.5,190.225c0,99.25,76.01,180.74,172.99,189.45c-5.68,0.51-11.44,0.77-17.26,0.77 C110.48,9.515,34.5,90.995,34.5,190.225c0,99.25,76.01,180.74,172.99,189.45c-5.68,0.51-11.44,0.77-17.26,0.77
C85.17,380.445,0,295.285,0,190.225S85.17,0.005,190.23,0.005z' C85.17,380.445,0,295.285,0,190.225S85.17,0.005,190.23,0.005z'
/> />
</SvgIcon> </SvgIcon>
); );
} };
export default Earth;

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function ExportIcon(props) { const Export: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 576 512'> <SvgIcon sx={props.sx} color={props.color} viewBox='0 0 576 512'>
<path d='M192 312C192 298.8 202.8 288 216 288H384V160H256c-17.67 0-32-14.33-32-32L224 0H48C21.49 0 0 21.49 0 48v416C0 490.5 21.49 512 48 512h288c26.51 0 48-21.49 48-48v-128H216C202.8 336 192 325.3 192 312zM256 0v128h128L256 0zM568.1 295l-80-80c-9.375-9.375-24.56-9.375-33.94 0s-9.375 24.56 0 33.94L494.1 288H384v48h110.1l-39.03 39.03C450.3 379.7 448 385.8 448 392s2.344 12.28 7.031 16.97c9.375 9.375 24.56 9.375 33.94 0l80-80C578.3 319.6 578.3 304.4 568.1 295z' /> <path d='M192 312C192 298.8 202.8 288 216 288H384V160H256c-17.67 0-32-14.33-32-32L224 0H48C21.49 0 0 21.49 0 48v416C0 490.5 21.49 512 48 512h288c26.51 0 48-21.49 48-48v-128H216C202.8 336 192 325.3 192 312zM256 0v128h128L256 0zM568.1 295l-80-80c-9.375-9.375-24.56-9.375-33.94 0s-9.375 24.56 0 33.94L494.1 288H384v48h110.1l-39.03 39.03C450.3 379.7 448 385.8 448 392s2.344 12.28 7.031 16.97c9.375 9.375 24.56 9.375 33.94 0l80-80C578.3 319.6 578.3 304.4 568.1 295z' />
</SvgIcon> </SvgIcon>
); );
} };
export default Export;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function GoldIcon(props) { const Gold: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 511.882 511.882'> <SvgIcon {...props} x='0px' y='0px' viewBox='0 0 511.882 511.882'>
<polygon <polygon
@ -45,4 +45,6 @@ export default function GoldIcon(props) {
/> />
</SvgIcon> </SvgIcon>
); );
} };
export default Gold;

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function NewTabIcon(props) { const NewTab: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 448 512'> <SvgIcon sx={props.sx} color={props.color} viewBox='0 0 448 512'>
<path d='M256 64C256 46.33 270.3 32 288 32H415.1C415.1 32 415.1 32 415.1 32C420.3 32 424.5 32.86 428.2 34.43C431.1 35.98 435.5 38.27 438.6 41.3C438.6 41.35 438.6 41.4 438.7 41.44C444.9 47.66 447.1 55.78 448 63.9C448 63.94 448 63.97 448 64V192C448 209.7 433.7 224 416 224C398.3 224 384 209.7 384 192V141.3L214.6 310.6C202.1 323.1 181.9 323.1 169.4 310.6C156.9 298.1 156.9 277.9 169.4 265.4L338.7 96H288C270.3 96 256 81.67 256 64V64zM0 128C0 92.65 28.65 64 64 64H160C177.7 64 192 78.33 192 96C192 113.7 177.7 128 160 128H64V416H352V320C352 302.3 366.3 288 384 288C401.7 288 416 302.3 416 320V416C416 451.3 387.3 480 352 480H64C28.65 480 0 451.3 0 416V128z' /> <path d='M256 64C256 46.33 270.3 32 288 32H415.1C415.1 32 415.1 32 415.1 32C420.3 32 424.5 32.86 428.2 34.43C431.1 35.98 435.5 38.27 438.6 41.3C438.6 41.35 438.6 41.4 438.7 41.44C444.9 47.66 447.1 55.78 448 63.9C448 63.94 448 63.97 448 64V192C448 209.7 433.7 224 416 224C398.3 224 384 209.7 384 192V141.3L214.6 310.6C202.1 323.1 181.9 323.1 169.4 310.6C156.9 298.1 156.9 277.9 169.4 265.4L338.7 96H288C270.3 96 256 81.67 256 64V64zM0 128C0 92.65 28.65 64 64 64H160C177.7 64 192 78.33 192 96C192 113.7 177.7 128 160 128H64V416H352V320C352 302.3 366.3 288 384 288C401.7 288 416 302.3 416 320V416C416 451.3 387.3 480 352 480H64C28.65 480 0 451.3 0 416V128z' />
</SvgIcon> </SvgIcon>
); );
} };
export default NewTab;

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
// By SatsCoffee https://github.com/satscoffee/nostr_icons/blob/main/nostr_logo_blk.svg // By SatsCoffee https://github.com/satscoffee/nostr_icons/blob/main/nostr_logo_blk.svg
export default function NostrIcon(props) { const Nostr: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 875 875'> <SvgIcon sx={props.sx} color={props.color} viewBox='0 0 875 875'>
<path d='m684.72,485.57c.22,12.59-11.93,51.47-38.67,81.3-26.74,29.83-56.02,20.85-58.42,20.16s-3.09-4.46-7.89-3.77-9.6,6.17-18.86,7.2-17.49,1.71-26.06-1.37c-4.46.69-5.14.71-7.2,2.24s-17.83,10.79-21.6,11.47c0,7.2-1.37,44.57,0,55.89s3.77,25.71,7.54,36c3.77,10.29,2.74,10.63,7.54,9.94s13.37.34,15.77,4.11c2.4,3.77,1.37,6.51,5.49,8.23s60.69,17.14,99.43,19.2c26.74.69,42.86,2.74,52.12,19.54,1.37,7.89,7.54,13.03,11.31,14.06s8.23,2.06,12,5.83,1.03,8.23,5.49,11.66c4.46,3.43,14.74,8.57,25.37,13.71,10.63,5.14,15.09,13.37,15.77,16.11s1.71,10.97,1.71,10.97c0,0-8.91,0-10.97-2.06s-2.74-5.83-2.74-5.83c0,0-6.17,1.03-7.54,3.43s.69,2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8,4.11,8.23,5.83,8.23,10.63,10.97s8.23,5.83,8.23,5.83l-7.2,4.46s-4.46,2.06-14.74-.69-11.66-4.46-12.69-10.63,0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77,4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97,0-19.2,3.43-19.89,1.71-26.74-14.06-55.89-19.89-64.12c-13.03,1.03-50.74-.69-50.74-.69,0,0-2.4-.69-17.49,5.83s-36.48,13.76-46.77,19.93-14.4,9.7-16.12,13.13c.12,3-1.23,7.72-2.79,9.06s-12.48,2.42-12.48,2.42c0,0-5.85,5.86-8.25,9.97-6.86,9.6-55.2,125.14-66.52,149.83-13.54,32.57-9.77,27.43-37.71,27.43s-8.06.3-8.06.3c0,0-12.34,5.88-16.8,5.88s-18.86-2.4-26.4,0-16.46,9.26-23.31,10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12,0,0,1.74-6.51,7.91-10.88,8.23-5.83,25.37-16.11,34.63-21.26s17.49-7.89,23.31-9.26,18.51-6.17,30.51-9.94,19.54-8.23,29.83-31.54c10.29-23.31,50.4-111.43,51.43-116.23.63-2.96,3.73-6.48,4.8-15.09.66-5.35-2.49-13.04,1.71-22.63,10.97-25.03,21.6-20.23,26.4-20.23s17.14.34,26.4-1.37,15.43-2.74,24.69-7.89,11.31-8.91,11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2,1.03,8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17,13.03-1.71-18.86-5.14,7.2-2.74-16.11-4.8,8.23-3.43-14.4-5.83,4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46,3.43-4.46,3.43l1.37,12-12.2-6.27-7-11.9s2.36,4.01-9.62,7.53c-20.55,0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89,3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26,25.71-22.97,25.03-16.46,46.29-17.14c21.26-.69,32.91,2.74,46.29,8.23s38.74,13.71,43.89,17.49c11.31-9.94,28.46-19.89,34.29-19.89,1.03-2.4,6.19-12.33,17.96-17.6,35.31-15.81,108.13-34,131.53-35.54,31.2-2.06,7.89-1.37,39.09,2.06,31.2,3.43,54.17,7.54,69.6,12.69,12.58,4.19,25.03,9.6,34.29,2.06,4.33-1.81,11.81-1.34,17.83-5.14,30.69-25.09,34.72-32.35,43.63-41.95s20.14-24.91,22.54-45.14,4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03,2.06-12,5.14-22.29,6.86-30.51s9.94-14.74,19.89-16.46c9.94-1.71,17.83,1.37,22.29,4.8,4.46,3.43,11.65,6.28,13.37,10.29.34,1.71-1.37,6.51,8.23,8.23,9.6,1.71,16.05,4.16,16.05,4.16,0,0,15.64,4.29,3.11,7.73-12.69,2.06-20.52-.71-24.29,1.69s-7.21,10.08-9.61,11.1-7.2.34-12,4.11-9.6,6.86-12.69,14.4-5.49,15.77-3.43,26.74,8.57,31.54,14.4,43.2c5.83,11.66,20.23,40.8,24.34,47.66s15.77,29.49,16.8,53.83,1.03,44.23,0,54.86-10.84,51.65-35.53,85.94c-8.16,14.14-23.21,31.9-24.67,35.03-1.45,3.13-3.02,4.88-1.61,7.65,4.62,9.05,12.87,22.13,14.71,29.22,2.29,6.64,6.99,16.13,7.22,28.72Z' /> <path d='m684.72,485.57c.22,12.59-11.93,51.47-38.67,81.3-26.74,29.83-56.02,20.85-58.42,20.16s-3.09-4.46-7.89-3.77-9.6,6.17-18.86,7.2-17.49,1.71-26.06-1.37c-4.46.69-5.14.71-7.2,2.24s-17.83,10.79-21.6,11.47c0,7.2-1.37,44.57,0,55.89s3.77,25.71,7.54,36c3.77,10.29,2.74,10.63,7.54,9.94s13.37.34,15.77,4.11c2.4,3.77,1.37,6.51,5.49,8.23s60.69,17.14,99.43,19.2c26.74.69,42.86,2.74,52.12,19.54,1.37,7.89,7.54,13.03,11.31,14.06s8.23,2.06,12,5.83,1.03,8.23,5.49,11.66c4.46,3.43,14.74,8.57,25.37,13.71,10.63,5.14,15.09,13.37,15.77,16.11s1.71,10.97,1.71,10.97c0,0-8.91,0-10.97-2.06s-2.74-5.83-2.74-5.83c0,0-6.17,1.03-7.54,3.43s.69,2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8,4.11,8.23,5.83,8.23,10.63,10.97s8.23,5.83,8.23,5.83l-7.2,4.46s-4.46,2.06-14.74-.69-11.66-4.46-12.69-10.63,0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77,4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97,0-19.2,3.43-19.89,1.71-26.74-14.06-55.89-19.89-64.12c-13.03,1.03-50.74-.69-50.74-.69,0,0-2.4-.69-17.49,5.83s-36.48,13.76-46.77,19.93-14.4,9.7-16.12,13.13c.12,3-1.23,7.72-2.79,9.06s-12.48,2.42-12.48,2.42c0,0-5.85,5.86-8.25,9.97-6.86,9.6-55.2,125.14-66.52,149.83-13.54,32.57-9.77,27.43-37.71,27.43s-8.06.3-8.06.3c0,0-12.34,5.88-16.8,5.88s-18.86-2.4-26.4,0-16.46,9.26-23.31,10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12,0,0,1.74-6.51,7.91-10.88,8.23-5.83,25.37-16.11,34.63-21.26s17.49-7.89,23.31-9.26,18.51-6.17,30.51-9.94,19.54-8.23,29.83-31.54c10.29-23.31,50.4-111.43,51.43-116.23.63-2.96,3.73-6.48,4.8-15.09.66-5.35-2.49-13.04,1.71-22.63,10.97-25.03,21.6-20.23,26.4-20.23s17.14.34,26.4-1.37,15.43-2.74,24.69-7.89,11.31-8.91,11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2,1.03,8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17,13.03-1.71-18.86-5.14,7.2-2.74-16.11-4.8,8.23-3.43-14.4-5.83,4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46,3.43-4.46,3.43l1.37,12-12.2-6.27-7-11.9s2.36,4.01-9.62,7.53c-20.55,0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89,3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26,25.71-22.97,25.03-16.46,46.29-17.14c21.26-.69,32.91,2.74,46.29,8.23s38.74,13.71,43.89,17.49c11.31-9.94,28.46-19.89,34.29-19.89,1.03-2.4,6.19-12.33,17.96-17.6,35.31-15.81,108.13-34,131.53-35.54,31.2-2.06,7.89-1.37,39.09,2.06,31.2,3.43,54.17,7.54,69.6,12.69,12.58,4.19,25.03,9.6,34.29,2.06,4.33-1.81,11.81-1.34,17.83-5.14,30.69-25.09,34.72-32.35,43.63-41.95s20.14-24.91,22.54-45.14,4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03,2.06-12,5.14-22.29,6.86-30.51s9.94-14.74,19.89-16.46c9.94-1.71,17.83,1.37,22.29,4.8,4.46,3.43,11.65,6.28,13.37,10.29.34,1.71-1.37,6.51,8.23,8.23,9.6,1.71,16.05,4.16,16.05,4.16,0,0,15.64,4.29,3.11,7.73-12.69,2.06-20.52-.71-24.29,1.69s-7.21,10.08-9.61,11.1-7.2.34-12,4.11-9.6,6.86-12.69,14.4-5.49,15.77-3.43,26.74,8.57,31.54,14.4,43.2c5.83,11.66,20.23,40.8,24.34,47.66s15.77,29.49,16.8,53.83,1.03,44.23,0,54.86-10.84,51.65-35.53,85.94c-8.16,14.14-23.21,31.9-24.67,35.03-1.45,3.13-3.02,4.88-1.61,7.65,4.62,9.05,12.87,22.13,14.71,29.22,2.29,6.64,6.99,16.13,7.22,28.72Z' />
</SvgIcon> </SvgIcon>
); );
} };
export default Nostr;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function RoboSatsIcon(props) { const RoboSats: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' width='1000px' height='1000px' viewBox='0 0 1000 900'> <SvgIcon {...props} x='0px' y='0px' width='1000px' height='1000px' viewBox='0 0 1000 900'>
<g> <g>
@ -102,4 +102,6 @@ export default function RoboSatsIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default RoboSats;

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function RoboSatsNoTextIcon(props) { const RoboSatsNoText: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' width='1000px' height='1000px' viewBox='0 0 1000 800'> <SvgIcon {...props} x='0px' y='0px' width='1000px' height='1000px' viewBox='0 0 1000 800'>
<g> <g>
@ -39,4 +39,6 @@ export default function RoboSatsNoTextIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default RoboSatsNoText;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function RoboSatsTextIcon(props) { const RoboSatsText: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' width='2000px' height='500px' viewBox='0 620 2000 1'> <SvgIcon {...props} x='0px' y='0px' width='2000px' height='500px' viewBox='0 620 2000 1'>
<g> <g>
@ -95,4 +95,6 @@ export default function RoboSatsTextIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default RoboSatsText;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function SellSatsIcon(props) { const SellSats: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'> <SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g> <g>
@ -135,4 +135,6 @@ export default function SellSatsIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default SellSats;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function SellSatsCheckedIcon(props) { const SellSatsChecked: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'> <SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g> <g>
@ -79,4 +79,6 @@ export default function SellSatsCheckedIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default SellSatsChecked;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function SendReceiveIcon(props) { const SendReceive: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'> <SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g> <g>
@ -20,4 +20,6 @@ export default function SendReceiveIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default SendReceive;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function SimplexIcon(props) { const Simplex: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 1080 1080'> <SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 1080 1080'>
<g transform='matrix(4.68 0 0 4.68 668.81 540.67)'> <g transform='matrix(4.68 0 0 4.68 668.81 540.67)'>
@ -18,4 +18,6 @@ export default function SimplexIcon(props) {
</g> </g>
</SvgIcon> </SvgIcon>
); );
} };
export default Simplex;

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function TorIcon(props) { const Tor: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 180 180'> <SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 180 180'>
<path d='M90.1846205,163.631147 L90.1846205,152.721073 C124.743583,152.621278 152.726063,124.581416 152.726063,89.9975051 C152.726063,55.4160892 124.743583,27.3762266 90.1846205,27.2764318 L90.1846205,16.366358 C130.768698,16.4686478 163.633642,49.3909741 163.633642,89.9975051 C163.633642,130.606531 130.768698,163.531352 90.1846205,163.631147 Z M90.1846205,125.444642 C109.677053,125.342352 125.454621,109.517381 125.454621,89.9975051 C125.454621,70.4801242 109.677053,54.6551533 90.1846205,54.5528636 L90.1846205,43.6452847 C115.704663,43.7450796 136.364695,64.4550091 136.364695,89.9975051 C136.364695,115.542496 115.704663,136.252426 90.1846205,136.35222 L90.1846205,125.444642 Z M90.1846205,70.9167267 C100.640628,71.0165216 109.090758,79.5165493 109.090758,89.9975051 C109.090758,100.480956 100.640628,108.980984 90.1846205,109.080778 L90.1846205,70.9167267 Z M0,89.9975051 C0,139.705328 40.2921772,180 90,180 C139.705328,180 180,139.705328 180,89.9975051 C180,40.2921772 139.705328,0 90,0 C40.2921772,0 0,40.2921772 0,89.9975051 Z'></path> <path d='M90.1846205,163.631147 L90.1846205,152.721073 C124.743583,152.621278 152.726063,124.581416 152.726063,89.9975051 C152.726063,55.4160892 124.743583,27.3762266 90.1846205,27.2764318 L90.1846205,16.366358 C130.768698,16.4686478 163.633642,49.3909741 163.633642,89.9975051 C163.633642,130.606531 130.768698,163.531352 90.1846205,163.631147 Z M90.1846205,125.444642 C109.677053,125.342352 125.454621,109.517381 125.454621,89.9975051 C125.454621,70.4801242 109.677053,54.6551533 90.1846205,54.5528636 L90.1846205,43.6452847 C115.704663,43.7450796 136.364695,64.4550091 136.364695,89.9975051 C136.364695,115.542496 115.704663,136.252426 90.1846205,136.35222 L90.1846205,125.444642 Z M90.1846205,70.9167267 C100.640628,71.0165216 109.090758,79.5165493 109.090758,89.9975051 C109.090758,100.480956 100.640628,108.980984 90.1846205,109.080778 L90.1846205,70.9167267 Z M0,89.9975051 C0,139.705328 40.2921772,180 90,180 C139.705328,180 180,139.705328 180,89.9975051 C180,40.2921772 139.705328,0 90,0 C40.2921772,0 0,40.2921772 0,89.9975051 Z'></path>
</SvgIcon> </SvgIcon>
); );
} };
export default Tor;

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { SvgIcon } from '@mui/material'; import { SvgIcon, type SvgIconProps } from '@mui/material';
export default function UserNinjaIcon(props) { const UserNinja: React.FC<SvgIconProps> = (props) => {
return ( return (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 512 512'> <SvgIcon {...props} x='0px' y='0px' viewBox='0 0 512 512'>
<path d='M64 192c27.25 0 51.75-11.5 69.25-29.75c15 54 64 93.75 122.8 93.75c70.75 0 127.1-57.25 127.1-128s-57.25-128-127.1-128c-50.38 0-93.63 29.38-114.5 71.75C124.1 47.75 96 32 64 32c0 33.37 17.12 62.75 43.13 80C81.13 129.3 64 158.6 64 192zM208 96h95.1C321.7 96 336 110.3 336 128h-160C176 110.3 190.3 96 208 96zM337.8 306.9L256 416L174.2 306.9C93.36 321.6 32 392.2 32 477.3c0 19.14 15.52 34.67 34.66 34.67H445.3c19.14 0 34.66-15.52 34.66-34.67C480 392.2 418.6 321.6 337.8 306.9z' /> <path d='M64 192c27.25 0 51.75-11.5 69.25-29.75c15 54 64 93.75 122.8 93.75c70.75 0 127.1-57.25 127.1-128s-57.25-128-127.1-128c-50.38 0-93.63 29.38-114.5 71.75C124.1 47.75 96 32 64 32c0 33.37 17.12 62.75 43.13 80C81.13 129.3 64 158.6 64 192zM208 96h95.1C321.7 96 336 110.3 336 128h-160C176 110.3 190.3 96 208 96zM337.8 306.9L256 416L174.2 306.9C93.36 321.6 32 392.2 32 477.3c0 19.14 15.52 34.67 34.66 34.67H445.3c19.14 0 34.66-15.52 34.66-34.67C480 392.2 418.6 321.6 337.8 306.9z' />
</SvgIcon> </SvgIcon>
); );
} };
export default UserNinja;

View File

@ -1,6 +1,7 @@
export { default as AmbossIcon } from './Amboss'; export { default as AmbossIcon } from './Amboss';
export { default as BitcoinIcon } from './Bitcoin'; export { default as BitcoinIcon } from './Bitcoin';
export { default as BitcoinSignIcon } from './BitcoinSign'; export { default as BitcoinSignIcon } from './BitcoinSign';
export { default as NostrIcon } from './Nostr';
export { default as BuySatsIcon } from './BuySats'; export { default as BuySatsIcon } from './BuySats';
export { default as BuySatsCheckedIcon } from './BuySatsChecked'; export { default as BuySatsCheckedIcon } from './BuySatsChecked';
export { default as EarthIcon } from './Earth'; export { default as EarthIcon } from './Earth';
@ -16,7 +17,13 @@ export { default as ExportIcon } from './Export';
export { default as UserNinjaIcon } from './UserNinja'; export { default as UserNinjaIcon } from './UserNinja';
export { default as TorIcon } from './Tor'; export { default as TorIcon } from './Tor';
export { default as SimplexIcon } from './Simplex'; export { default as SimplexIcon } from './Simplex';
export { default as NostrIcon } from './Nostr';
// Badges
export { default as BadgeFounder } from './BadgeFounder';
export { default as BadgeDevFund } from './BadgeDevFund';
export { default as BadgePrivacy } from './BadgePrivacy';
export { default as BadgeLimits } from './BadgeLimits';
export { default as BadgeLoved } from './BadgeLoved';
// Flags with props // Flags with props
export { default as FlagWithProps } from './WorldFlags'; export { default as FlagWithProps } from './WorldFlags';

View File

@ -16,7 +16,7 @@ import RangeSlider from './RangeSlider';
import currencyDict from '../../../static/assets/currencies.json'; import currencyDict from '../../../static/assets/currencies.json';
import { pn } from '../../utils'; import { pn } from '../../utils';
const RangeThumbComponent = function (props: object) { const RangeThumbComponent: React.FC<React.PropsWithChildren> = (props) => {
const { children, ...other } = props; const { children, ...other } = props;
return ( return (
<SliderThumb {...other}> <SliderThumb {...other}>
@ -34,16 +34,20 @@ interface AmountRangeProps {
type: number; type: number;
currency: number; currency: number;
handleRangeAmountChange: (e: any, activeThumb: any) => void; handleRangeAmountChange: (e: any, activeThumb: any) => void;
handleMaxAmountChange: () => void; handleMaxAmountChange: (
handleMinAmountChange: () => void; e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
handleCurrencyChange: () => void; ) => void;
handleMinAmountChange: (
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
) => void;
handleCurrencyChange: (newCurrency: number) => void;
maxAmountError: boolean; maxAmountError: boolean;
minAmountError: boolean; minAmountError: boolean;
currencyCode: string; currencyCode: string;
amountLimits: number[]; amountLimits: number[];
} }
function AmountRange({ const AmountRange: React.FC<AmountRangeProps> = ({
minAmount, minAmount,
handleRangeAmountChange, handleRangeAmountChange,
currency, currency,
@ -55,7 +59,7 @@ function AmountRange({
maxAmountError, maxAmountError,
handleMinAmountChange, handleMinAmountChange,
handleMaxAmountChange, handleMaxAmountChange,
}: AmountRangeProps) { }) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@ -130,10 +134,10 @@ function AmountRange({
inputProps={{ inputProps={{
style: { textAlign: 'center' }, style: { textAlign: 'center' },
}} }}
value={currency == 0 ? 1 : currency} value={currency === 0 ? 1 : currency}
renderValue={() => currencyCode} renderValue={() => currencyCode}
onChange={(e) => { onChange={(e) => {
handleCurrencyChange(e.target.value); handleCurrencyChange(Number(e.target.value));
}} }}
> >
{Object.entries(currencyDict).map(([key, value]) => ( {Object.entries(currencyDict).map(([key, value]) => (
@ -188,6 +192,6 @@ function AmountRange({
</Box> </Box>
</Grid> </Grid>
); );
} };
export default AmountRange; export default AmountRange;

View File

@ -2,7 +2,16 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useAutocomplete from '@mui/base/useAutocomplete'; import useAutocomplete from '@mui/base/useAutocomplete';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { Button, Fade, Tooltip, Typography, Grow, useTheme } from '@mui/material'; import {
Button,
Fade,
Tooltip,
Typography,
Grow,
useTheme,
type SxProps,
type Theme,
} from '@mui/material';
import { fiatMethods, swapMethods, PaymentIcon } from '../PaymentMethods'; import { fiatMethods, swapMethods, PaymentIcon } from '../PaymentMethods';
// Icons // Icons
@ -20,12 +29,18 @@ const Root = styled('div')(
const Label = styled('label')( const Label = styled('label')(
({ theme, error, sx }) => ` ({ theme, error, sx }) => `
color: ${ color: ${
theme.palette.mode === 'dark' ? (error ? '#f44336' : '#cfcfcf') : error ? '#dd0000' : '#717171' theme.palette.mode === 'dark'
? error === true
? '#f44336'
: '#cfcfcf'
: error === true
? '#dd0000'
: '#717171'
}; };
pointer-events: none; pointer-events: none;
position: relative; position: relative;
left: 1em; left: 1em;
top: ${sx.top}; top: ${String(sx.top) ?? '0.72em'};
maxHeight: 0em; maxHeight: 0em;
height: 0em; height: 0em;
white-space: no-wrap; white-space: no-wrap;
@ -35,14 +50,20 @@ const Label = styled('label')(
const InputWrapper = styled('div')( const InputWrapper = styled('div')(
({ theme, error, sx }) => ` ({ theme, error, sx }) => `
min-height: ${sx.minHeight}; min-height: ${String(sx.minHeight)};
max-height: ${sx.maxHeight}; max-height: ${String(sx.maxHeight)};
border: 1px solid ${ border: 1px solid ${
theme.palette.mode === 'dark' ? (error ? '#f44336' : '#434343') : error ? '#dd0000' : '#c4c4c4' theme.palette.mode === 'dark'
? error === ''
? '#f44336'
: '#434343'
: error === ''
? '#dd0000'
: '#c4c4c4'
}; };
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'}; background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
border-radius: 4px; border-radius: 4px;
border-color: ${sx.borderColor ? `border-color ${sx.borderColor}` : ''} border-color: ${sx.borderColor !== undefined ? `border-color ${String(sx.borderColor)}` : ''}
padding: 1px; padding: 1px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -52,10 +73,10 @@ const InputWrapper = styled('div')(
&:hover { &:hover {
border-color: ${ border-color: ${
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
? error ? error === true
? '#f44336' ? '#f44336'
: sx.hoverBorderColor : String(sx.hoverBorderColor)
: error : error === true
? '#dd0000' ? '#dd0000'
: '#2f2f2f' : '#2f2f2f'
}; };
@ -64,10 +85,10 @@ const InputWrapper = styled('div')(
&.focused { &.focused {
border: 2px solid ${ border: 2px solid ${
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
? error ? error === true
? '#f44336' ? '#f44336'
: '#90caf9' : '#90caf9'
: error : error === true
? '#dd0000' ? '#dd0000'
: '#1976d2' : '#1976d2'
}; };
@ -97,7 +118,8 @@ interface TagProps {
onDelete: () => void; onDelete: () => void;
onClick: () => void; onClick: () => void;
} }
const Tag = ({ label, icon, onDelete, onClick, ...other }: TagProps) => {
const Tag: React.FC<TagProps> = ({ label, icon, onDelete, onClick, ...other }) => {
const theme = useTheme(); const theme = useTheme();
const iconSize = 1.5 * theme.typography.fontSize; const iconSize = 1.5 * theme.typography.fontSize;
return ( return (
@ -117,7 +139,7 @@ const StyledTag = styled(Tag)(
({ theme, sx }) => ` ({ theme, sx }) => `
display: flex; display: flex;
align-items: center; align-items: center;
height: ${sx.height}; height: ${String(sx?.height ?? '2.1em')};
margin: 2px; margin: 2px;
line-height: 1.5em; line-height: 1.5em;
background-color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : '#fafafa'}; background-color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : '#fafafa'};
@ -163,7 +185,7 @@ const ListHeader = styled('span')(
const Listbox = styled('ul')( const Listbox = styled('ul')(
({ theme, sx }) => ` ({ theme, sx }) => `
width: ${sx != null ? sx.width : '15.6em'}; width: ${String(sx?.width ?? '15.6em')};
margin: 2px 0 0; margin: 2px 0 0;
padding: 0; padding: 0;
position: absolute; position: absolute;
@ -209,7 +231,23 @@ const Listbox = styled('ul')(
`, `,
); );
export default function AutocompletePayments(props) { interface AutocompletePaymentsProps {
value: string;
optionsType: 'fiat' | 'swap';
onAutocompleteChange: (value: string) => void;
tooltipTitle: string;
labelProps: any;
tagProps: any;
listBoxProps: any;
error: string;
label: string;
sx: SxProps<Theme>;
addNewButtonText: string;
isFilter: boolean;
listHeaderText: string;
}
const AutocompletePayments: React.FC<AutocompletePaymentsProps> = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
getRootProps, getRootProps,
@ -220,19 +258,21 @@ export default function AutocompletePayments(props) {
getOptionProps, getOptionProps,
groupedOptions, groupedOptions,
value, value,
focused = 'true', focused = true,
setAnchorEl, setAnchorEl,
} = useAutocomplete({ } = useAutocomplete({
fullWidth: true, fullWidth: true,
id: 'payment-methods', id: 'payment-methods',
multiple: true, multiple: true,
value: props.value, value: props.value,
options: props.optionsType == 'fiat' ? fiatMethods : swapMethods, options: props.optionsType === 'fiat' ? fiatMethods : swapMethods,
getOptionLabel: (option) => option.name, getOptionLabel: (option) => option.name,
onInputChange: (e) => { onInputChange: (e) => {
setVal(e ? (e.target.value ? e.target.value : '') : ''); setVal(e.target.value ?? '');
},
onChange: (event, value) => {
props.onAutocompleteChange(value);
}, },
onChange: (event, value) => props.onAutocompleteChange(value),
onClose: () => { onClose: () => {
setVal(() => ''); setVal(() => '');
}, },
@ -243,36 +283,35 @@ export default function AutocompletePayments(props) {
const theme = useTheme(); const theme = useTheme();
const iconSize = 1.5 * theme.typography.fontSize; const iconSize = 1.5 * theme.typography.fontSize;
function handleAddNew(inputProps) { function handleAddNew(inputProps: any): void {
fiatMethods.push({ name: inputProps.value, icon: 'custom' }); fiatMethods.push({ name: inputProps.value, icon: 'custom' });
const a = value.push({ name: inputProps.value, icon: 'custom' }); const a = value.push({ name: inputProps.value, icon: 'custom' });
setVal(() => ''); setVal(() => '');
if (a || a == null) { if (a !== undefined) {
props.onAutocompleteChange(value); props.onAutocompleteChange(value);
} }
return false;
} }
return ( return (
<Root> <Root>
<Tooltip <Tooltip
placement='top' placement='top'
enterTouchDelay={props.tooltipTitle == '' ? 99999 : 300} enterTouchDelay={props.tooltipTitle === '' ? 99999 : 300}
enterDelay={props.tooltipTitle == '' ? 99999 : 700} enterDelay={props.tooltipTitle === '' ? 99999 : 700}
enterNextDelay={2000} enterNextDelay={2000}
title={props.tooltipTitle} title={props.tooltipTitle}
> >
<div {...getRootProps()}> <div {...getRootProps()}>
<Fade <Fade
appear={false} appear={false}
in={fewerOptions.length == 0 && value.length == 0 && val.length == 0} in={fewerOptions.length === 0 && value.length === 0 && val.length === 0}
> >
<div style={{ height: 0, display: 'flex', alignItems: 'flex-start' }}> <div style={{ height: 0, display: 'flex', alignItems: 'flex-start' }}>
<Label <Label
{...getInputLabelProps()} {...getInputLabelProps()}
sx={{ top: '0.72em', ...(props.labelProps ? props.labelProps.sx : {}) }} sx={{ top: '0.72em', ...(props.labelProps?.sx ?? {}) }}
error={props.error ? 'error' : null} error={Boolean(props.error)}
> >
{props.label} {props.label}
</Label> </Label>
@ -280,7 +319,7 @@ export default function AutocompletePayments(props) {
</Fade> </Fade>
<InputWrapper <InputWrapper
ref={setAnchorEl} ref={setAnchorEl}
error={props.error ? 'error' : null} error={Boolean(props.error)}
className={focused ? 'focused' : ''} className={focused ? 'focused' : ''}
sx={{ sx={{
minHeight: '2.9em', minHeight: '2.9em',
@ -291,10 +330,11 @@ export default function AutocompletePayments(props) {
> >
{value.map((option, index) => ( {value.map((option, index) => (
<StyledTag <StyledTag
key={index}
label={t(option.name)} label={t(option.name)}
icon={option.icon} icon={option.icon}
sx={{ height: '2.1em', ...(props.tagProps ? props.tagProps.sx : {}) }}
onClick={props.onClick} onClick={props.onClick}
sx={{ height: '2.1em', ...(props.tagProps ?? {}) }}
{...getTagProps({ index })} {...getTagProps({ index })}
/> />
))} ))}
@ -303,7 +343,7 @@ export default function AutocompletePayments(props) {
</div> </div>
</Tooltip> </Tooltip>
<Grow in={fewerOptions.length > 0}> <Grow in={fewerOptions.length > 0}>
<Listbox sx={props.listBoxProps ? props.listBoxProps.sx : undefined} {...getListboxProps()}> <Listbox sx={props.listBoxProps?.sx ?? undefined} {...getListboxProps()}>
{!props.isFilter ? ( {!props.isFilter ? (
<div <div
style={{ style={{
@ -341,7 +381,13 @@ export default function AutocompletePayments(props) {
))} ))}
{val != null || !props.isFilter ? ( {val != null || !props.isFilter ? (
val.length > 2 ? ( val.length > 2 ? (
<Button size='small' fullWidth={true} onClick={() => handleAddNew(getInputProps())}> <Button
size='small'
fullWidth={true}
onClick={() => {
handleAddNew(getInputProps());
}}
>
<DashboardCustomizeIcon sx={{ width: '1em', height: '1em' }} /> <DashboardCustomizeIcon sx={{ width: '1em', height: '1em' }} />
{props.addNewButtonText} {props.addNewButtonText}
</Button> </Button>
@ -353,7 +399,12 @@ export default function AutocompletePayments(props) {
{/* Here goes what happens if there is no fewerOptions */} {/* Here goes what happens if there is no fewerOptions */}
<Grow in={getInputProps().value.length > 0 && !props.isFilter && fewerOptions.length === 0}> <Grow in={getInputProps().value.length > 0 && !props.isFilter && fewerOptions.length === 0}>
<Listbox {...getListboxProps()}> <Listbox {...getListboxProps()}>
<Button fullWidth={true} onClick={() => handleAddNew(getInputProps())}> <Button
fullWidth={true}
onClick={() => {
handleAddNew(getInputProps());
}}
>
<DashboardCustomizeIcon sx={{ width: '1.28em', height: '1.28em' }} /> <DashboardCustomizeIcon sx={{ width: '1.28em', height: '1.28em' }} />
{props.addNewButtonText} {props.addNewButtonText}
</Button> </Button>
@ -361,4 +412,6 @@ export default function AutocompletePayments(props) {
</Grow> </Grow>
</Root> </Root>
); );
} };
export default AutocompletePayments;

View File

@ -40,8 +40,11 @@ import { amountToString, computeSats, pn } from '../../utils';
import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit, Map } from '@mui/icons-material'; import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit, Map } from '@mui/icons-material';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { fiatMethods } from '../PaymentMethods'; import { fiatMethods } from '../PaymentMethods';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import SelectCoordinator from './SelectCoordinator';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
interface MakerFormProps { interface MakerFormProps {
disableRequest?: boolean; disableRequest?: boolean;
@ -50,7 +53,7 @@ interface MakerFormProps {
onSubmit?: () => void; onSubmit?: () => void;
onReset?: () => void; onReset?: () => void;
submitButtonLabel?: string; submitButtonLabel?: string;
onOrderCreated?: (id: number) => void; onOrderCreated?: (shortAlias: string, id: number) => void;
onClickGenerateRobot?: () => void; onClickGenerateRobot?: () => void;
} }
@ -64,8 +67,10 @@ const MakerForm = ({
onOrderCreated = () => null, onOrderCreated = () => null,
onClickGenerateRobot = () => null, onClickGenerateRobot = () => null,
}: MakerFormProps): JSX.Element => { }: MakerFormProps): JSX.Element => {
const { fav, setFav, limits, fetchLimits, info, maker, setMaker, baseUrl, robot } = const { fav, setFav, settings, hostUrl, origin } = useContext<UseAppStoreType>(AppContext);
useContext<UseAppStoreType>(AppContext); const { federation, focusedCoordinator, coordinatorUpdatedAt, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { maker, setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
@ -80,30 +85,31 @@ const MakerForm = ({
const [openWorldmap, setOpenWorldmap] = useState<boolean>(false); const [openWorldmap, setOpenWorldmap] = useState<boolean>(false);
const [submittingRequest, setSubmittingRequest] = useState<boolean>(false); const [submittingRequest, setSubmittingRequest] = useState<boolean>(false);
const [amountRangeEnabled, setAmountRangeEnabled] = useState<boolean>(true); const [amountRangeEnabled, setAmountRangeEnabled] = useState<boolean>(true);
const [limits, setLimits] = useState<LimitList>({});
const maxRangeAmountMultiple = 14.8; const maxRangeAmountMultiple = 14.8;
const minRangeAmountMultiple = 1.6; const minRangeAmountMultiple = 1.6;
const amountSafeThresholds = [1.03, 0.98]; const amountSafeThresholds = [1.03, 0.98];
useEffect(() => { useEffect(() => {
setCurrencyCode(currencyDict[fav.currency == 0 ? 1 : fav.currency]); setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
if (Object.keys(limits.list).length === 0) { if (focusedCoordinator) {
fetchLimits().then((data) => { const newLimits = federation.getCoordinator(focusedCoordinator).limits;
updateAmountLimits(data, fav.currency, maker.premium); if (Object.keys(newLimits).length !== 0) {
updateCurrentPrice(data, fav.currency, maker.premium); updateAmountLimits(newLimits, fav.currency, maker.premium);
updateSatoshisLimits(data); updateCurrentPrice(newLimits, fav.currency, maker.premium);
}); updateSatoshisLimits(newLimits);
} else { setLimits(newLimits);
updateAmountLimits(limits.list, fav.currency, maker.premium); }
updateCurrentPrice(limits.list, fav.currency, maker.premium);
updateSatoshisLimits(limits.list);
fetchLimits();
} }
}, []); }, [coordinatorUpdatedAt]);
const updateAmountLimits = function (limitList: LimitList, currency: number, premium: number) { const updateAmountLimits = function (
const index = currency == 0 ? 1 : currency; limitList: LimitList,
currency: number,
premium: number,
): void {
const index = currency === 0 ? 1 : currency;
let minAmountLimit: number = limitList[index].min_amount * (1 + premium / 100); let minAmountLimit: number = limitList[index].min_amount * (1 + premium / 100);
let maxAmountLimit: number = limitList[index].max_amount * (1 + premium / 100); let maxAmountLimit: number = limitList[index].max_amount * (1 + premium / 100);
@ -113,24 +119,28 @@ const MakerForm = ({
setAmountLimits([minAmountLimit, maxAmountLimit]); setAmountLimits([minAmountLimit, maxAmountLimit]);
}; };
const updateSatoshisLimits = function (limitList: LimitList) { const updateSatoshisLimits = function (limitList: LimitList): void {
const minAmount: number = limitList[1000].min_amount * 100000000; const minAmount: number = limitList[1000].min_amount * 100000000;
const maxAmount: number = limitList[1000].max_amount * 100000000; const maxAmount: number = limitList[1000].max_amount * 100000000;
setSatoshisLimits([minAmount, maxAmount]); setSatoshisLimits([minAmount, maxAmount]);
}; };
const updateCurrentPrice = function (limitsList: LimitList, currency: number, premium: number) { const updateCurrentPrice = function (
const index = currency == 0 ? 1 : currency; limitsList: LimitList,
currency: number,
premium: number,
): void {
const index = currency === 0 ? 1 : currency;
let price = '...'; let price = '...';
if (maker.isExplicit && maker.amount > 0 && maker.satoshis > 0) { if (maker.isExplicit && maker.amount > 0 && maker.satoshis > 0) {
price = maker.amount / (maker.satoshis / 100000000); price = maker.amount / (maker.satoshis / 100000000);
} else if (!maker.is_explicit) { } else if (!maker.isExplicit) {
price = limitsList[index].price * (1 + premium / 100); price = limitsList[index].price * (1 + premium / 100);
} }
setCurrentPrice(parseFloat(Number(price).toPrecision(5))); setCurrentPrice(parseFloat(Number(price).toPrecision(5)));
}; };
const handleCurrencyChange = function (newCurrency: number) { const handleCurrencyChange = function (newCurrency: number): void {
const currencyCode: string = currencyDict[newCurrency]; const currencyCode: string = currencyDict[newCurrency];
setCurrencyCode(currencyCode); setCurrencyCode(currencyCode);
setFav({ setFav({
@ -138,12 +148,12 @@ const MakerForm = ({
currency: newCurrency, currency: newCurrency,
mode: newCurrency === 1000 ? 'swap' : 'fiat', mode: newCurrency === 1000 ? 'swap' : 'fiat',
}); });
updateAmountLimits(limits.list, newCurrency, maker.premium); updateAmountLimits(limits, newCurrency, maker.premium);
updateCurrentPrice(limits.list, newCurrency, maker.premium); updateCurrentPrice(limits, newCurrency, maker.premium);
if (makerHasAmountRange) { if (makerHasAmountRange) {
const minAmount = parseFloat(Number(limits.list[newCurrency].min_amount).toPrecision(2)); const minAmount = parseFloat(Number(limits[newCurrency].min_amount).toPrecision(2));
const maxAmount = parseFloat(Number(limits.list[newCurrency].max_amount).toPrecision(2)); const maxAmount = parseFloat(Number(limits[newCurrency].max_amount).toPrecision(2));
if ( if (
parseFloat(maker.minAmount) < minAmount || parseFloat(maker.minAmount) < minAmount ||
parseFloat(maker.minAmount) > maxAmount || parseFloat(maker.minAmount) > maxAmount ||
@ -163,10 +173,12 @@ const MakerForm = ({
return maker.advancedOptions && amountRangeEnabled; return maker.advancedOptions && amountRangeEnabled;
}, [maker.advancedOptions, amountRangeEnabled]); }, [maker.advancedOptions, amountRangeEnabled]);
const handlePaymentMethodChange = function (paymentArray: { name: string; icon: string }[]) { const handlePaymentMethodChange = function (
let includeCoordinates = false; paymentArray: Array<{ name: string; icon: string }>,
): void {
let str = ''; let str = '';
const arrayLength = paymentArray.length; const arrayLength = paymentArray.length;
let includeCoordinates = false;
for (let i = 0; i < arrayLength; i++) { for (let i = 0; i < arrayLength; i++) {
str += paymentArray[i].name + ' '; str += paymentArray[i].name + ' ';
@ -190,14 +202,18 @@ const MakerForm = ({
}); });
}; };
const handleMinAmountChange = function (e) { const handleMinAmountChange = function (
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
): void {
setMaker({ setMaker({
...maker, ...maker,
minAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)), minAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
}); });
}; };
const handleMaxAmountChange = function (e) { const handleMaxAmountChange = function (
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
): void {
setMaker({ setMaker({
...maker, ...maker,
maxAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)), maxAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
@ -205,7 +221,7 @@ const MakerForm = ({
}; };
const handlePremiumChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = const handlePremiumChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> =
function ({ target: { value } }) { function ({ target: { value } }): void {
const max = fav.mode === 'fiat' ? 999 : 99; const max = fav.mode === 'fiat' ? 999 : 99;
const min = -100; const min = -100;
const newPremium = Math.floor(Number(value) * Math.pow(10, 2)) / Math.pow(10, 2); const newPremium = Math.floor(Number(value) * Math.pow(10, 2)) / Math.pow(10, 2);
@ -218,8 +234,8 @@ const MakerForm = ({
badPremiumText = t('Must be more than {{min}}%', { min }); badPremiumText = t('Must be more than {{min}}%', { min });
premium = -99.99; premium = -99.99;
} }
updateCurrentPrice(limits.list, fav.currency, premium); updateCurrentPrice(limits, fav.currency, premium);
updateAmountLimits(limits.list, fav.currency, premium); updateAmountLimits(limits, fav.currency, premium);
setMaker({ setMaker({
...maker, ...maker,
premium: isNaN(newPremium) || value === '' ? '' : premium, premium: isNaN(newPremium) || value === '' ? '' : premium,
@ -227,7 +243,7 @@ const MakerForm = ({
}); });
}; };
const handleSatoshisChange = function (e: object) { const handleSatoshisChange = function (e: object): void {
const newSatoshis = e.target.value; const newSatoshis = e.target.value;
let badSatoshisText: string = ''; let badSatoshisText: string = '';
let satoshis: string = newSatoshis; let satoshis: string = newSatoshis;
@ -247,14 +263,14 @@ const MakerForm = ({
}); });
}; };
const handleClickRelative = function () { const handleClickRelative = function (): void {
setMaker({ setMaker({
...maker, ...maker,
isExplicit: false, isExplicit: false,
}); });
}; };
const handleClickExplicit = function () { const handleClickExplicit = function (): void {
if (!maker.advancedOptions) { if (!maker.advancedOptions) {
setMaker({ setMaker({
...maker, ...maker,
@ -263,12 +279,24 @@ const MakerForm = ({
} }
}; };
const handleCreateOrder = function () { const handleCreateOrder = function (): void {
if (!disableRequest) { const { url, basePath } = federation
.getCoordinator(maker.coordinator)
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
const auth = {
tokenSHA256: garage.getRobot().tokenSHA256,
keys: {
pubKey: garage.getRobot().pubKey?.split('\n').join('\\'),
encPrivKey: garage.getRobot().encPrivKey?.split('\n').join('\\'),
},
};
if (!disableRequest && focusedCoordinator) {
setSubmittingRequest(true); setSubmittingRequest(true);
const body = { const body = {
type: fav.type == 0 ? 1 : 0, type: fav.type === 0 ? 1 : 0,
currency: fav.currency == 0 ? 1 : fav.currency, currency: fav.currency === 0 ? 1 : fav.currency,
amount: makerHasAmountRange ? null : maker.amount, amount: makerHasAmountRange ? null : maker.amount,
has_range: makerHasAmountRange, has_range: makerHasAmountRange,
min_amount: makerHasAmountRange ? maker.minAmount : null, min_amount: makerHasAmountRange ? maker.minAmount : null,
@ -276,7 +304,7 @@ const MakerForm = ({
payment_method: payment_method:
maker.paymentMethodsText === '' ? 'not specified' : maker.paymentMethodsText, maker.paymentMethodsText === '' ? 'not specified' : maker.paymentMethodsText,
is_explicit: maker.isExplicit, is_explicit: maker.isExplicit,
premium: maker.isExplicit ? null : maker.premium == '' ? 0 : maker.premium, premium: maker.isExplicit ? null : maker.premium === '' ? 0 : maker.premium,
satoshis: maker.isExplicit ? maker.satoshis : null, satoshis: maker.isExplicit ? maker.satoshis : null,
public_duration: maker.publicDuration, public_duration: maker.publicDuration,
escrow_duration: maker.escrowDuration, escrow_duration: maker.escrowDuration,
@ -284,48 +312,54 @@ const MakerForm = ({
latitude: maker.latitude, latitude: maker.latitude,
longitude: maker.longitude, longitude: maker.longitude,
}; };
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post(baseUrl, '/api/make/', body, { tokenSHA256: robot.tokenSHA256 }) .post(url, `${basePath}/api/make/`, body, auth)
.then((data: object) => { .then((data: any) => {
setBadRequest(data.bad_request); setBadRequest(data.bad_request);
if (data.id) { if (data.id !== undefined) {
onOrderCreated(data.id); onOrderCreated(maker.coordinator, data.id);
} }
setSubmittingRequest(false); setSubmittingRequest(false);
})
.catch(() => {
setBadRequest('Request error');
}); });
} }
setOpenDialogs(false); setOpenDialogs(false);
}; };
const handleChangePublicDuration = function (date: Date) { const handleChangePublicDuration = function (date: Date): void {
const d = new Date(date); const d = new Date(date);
const hours: number = d.getHours(); const hours: number = d.getHours();
const minutes: number = d.getMinutes(); const minutes: number = d.getMinutes();
const total_secs: number = hours * 60 * 60 + minutes * 60; const totalSecs: number = hours * 60 * 60 + minutes * 60;
setMaker({ setMaker({
...maker, ...maker,
publicExpiryTime: date, publicExpiryTime: date,
publicDuration: total_secs, publicDuration: totalSecs,
}); });
}; };
const handleChangeEscrowDuration = function (date: Date) { const handleChangeEscrowDuration = function (date: Date): void {
const d = new Date(date); const d = new Date(date);
const hours: number = d.getHours(); const hours: number = d.getHours();
const minutes: number = d.getMinutes(); const minutes: number = d.getMinutes();
const total_secs: number = hours * 60 * 60 + minutes * 60; const totalSecs: number = hours * 60 * 60 + minutes * 60;
setMaker({ setMaker({
...maker, ...maker,
escrowExpiryTime: date, escrowExpiryTime: date,
escrowDuration: total_secs, escrowDuration: totalSecs,
}); });
}; };
const handleClickAdvanced = function () { const handleClickAdvanced = function (): void {
if (maker.advancedOptions) { if (maker.advancedOptions) {
handleClickRelative(); handleClickRelative();
setMaker({ ...maker, advancedOptions: false }); setMaker({ ...maker, advancedOptions: false });
@ -352,14 +386,16 @@ const MakerForm = ({
); );
}, [maker.minAmount, maker.maxAmount, amountLimits]); }, [maker.minAmount, maker.maxAmount, amountLimits]);
const resetRange = function (advancedOptions: boolean) { const resetRange = function (advancedOptions: boolean): void {
const index = fav.currency === 0 ? 1 : fav.currency; const index = fav.currency === 0 ? 1 : fav.currency;
const minAmount = maker.amount const minAmount =
? parseFloat((maker.amount / 2).toPrecision(2)) maker.amount !== ''
: parseFloat(Number(limits.list[index].max_amount * 0.25).toPrecision(2)); ? parseFloat((maker.amount / 2).toPrecision(2))
const maxAmount = maker.amount : parseFloat(Number(limits[index].max_amount * 0.25).toPrecision(2));
? parseFloat(maker.amount) const maxAmount =
: parseFloat(Number(limits.list[index].max_amount * 0.75).toPrecision(2)); maker.amount !== ''
? parseFloat(maker.amount)
: parseFloat(Number(limits[index].max_amount * 0.75).toPrecision(2));
setMaker({ setMaker({
...maker, ...maker,
@ -369,7 +405,7 @@ const MakerForm = ({
}); });
}; };
const handleRangeAmountChange = function (e: any, newValue, activeThumb: number) { const handleRangeAmountChange = function (e: any, newValue, activeThumb: number): void {
let minAmount = e.target.value[0]; let minAmount = e.target.value[0];
let maxAmount = e.target.value[1]; let maxAmount = e.target.value[1];
@ -406,11 +442,14 @@ const MakerForm = ({
const handleClickAmountRangeEnabled = function ( const handleClickAmountRangeEnabled = function (
_e: React.ChangeEvent<HTMLInputElement>, _e: React.ChangeEvent<HTMLInputElement>,
checked: boolean, checked: boolean,
) { ): void {
setAmountRangeEnabled(checked); setAmountRangeEnabled(checked);
}; };
const amountLabel = useMemo(() => { const amountLabel = useMemo(() => {
if (!focusedCoordinator) return;
const info = federation.getCoordinator(focusedCoordinator)?.info;
const defaultRoutingBudget = 0.001; const defaultRoutingBudget = 0.001;
let label = t('Amount'); let label = t('Amount');
let helper = ''; let helper = '';
@ -420,7 +459,7 @@ const MakerForm = ({
swapSats = computeSats({ swapSats = computeSats({
amount: Number(maker.amount), amount: Number(maker.amount),
premium: Number(maker.premium), premium: Number(maker.premium),
fee: -info.maker_fee, fee: -(info?.maker_fee ?? 0),
routingBudget: defaultRoutingBudget, routingBudget: defaultRoutingBudget,
}); });
label = t('Onchain amount to send (BTC)'); label = t('Onchain amount to send (BTC)');
@ -431,7 +470,7 @@ const MakerForm = ({
swapSats = computeSats({ swapSats = computeSats({
amount: Number(maker.amount), amount: Number(maker.amount),
premium: Number(maker.premium), premium: Number(maker.premium),
fee: info.maker_fee, fee: info?.maker_fee ?? 0,
}); });
label = t('Onchain amount to receive (BTC)'); label = t('Onchain amount to receive (BTC)');
helper = t('You send approx {{swapSats}} LN Sats (fees might vary)', { helper = t('You send approx {{swapSats}} LN Sats (fees might vary)', {
@ -440,30 +479,30 @@ const MakerForm = ({
} }
} }
return { label, helper, swapSats }; return { label, helper, swapSats };
}, [fav, maker.amount, maker.premium, info]); }, [fav, maker.amount, maker.premium, federationUpdatedAt]);
const disableSubmit = useMemo(() => { const disableSubmit = useMemo(() => {
return ( return (
fav.type == null || fav.type == null ||
(!makerHasAmountRange && (!makerHasAmountRange &&
maker.amount != '' && maker.amount !== '' &&
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1])) || (maker.amount < amountLimits[0] || maker.amount > amountLimits[1])) ||
maker.badPaymentMethod || maker.badPaymentMethod ||
(maker.amount == null && (!makerHasAmountRange || limits.loading)) || (maker.amount == null && (!makerHasAmountRange || Object.keys(limits).lenght < 1)) ||
(makerHasAmountRange && (minAmountError || maxAmountError)) || (makerHasAmountRange && (minAmountError || maxAmountError)) ||
(!makerHasAmountRange && maker.amount <= 0) || (!makerHasAmountRange && maker.amount <= 0) ||
(maker.isExplicit && (maker.badSatoshisText != '' || maker.satoshis == '')) || (maker.isExplicit && (maker.badSatoshisText !== '' || maker.satoshis === '')) ||
(!maker.isExplicit && maker.badPremiumText != '') (!maker.isExplicit && maker.badPremiumText !== '')
); );
}, [maker, amountLimits, limits, fav.type, makerHasAmountRange]); }, [maker, amountLimits, coordinatorUpdatedAt, fav.type, makerHasAmountRange]);
const clearMaker = function () { const clearMaker = function (): void {
setFav({ ...fav, type: null }); setFav({ ...fav, type: null });
setMaker(defaultMaker); setMaker(defaultMaker);
}; };
const handleAddLocation = (pos: [number, number]) => { const handleAddLocation = (pos: [number, number]): void => {
if (pos && pos.length === 2) { if (pos?.length === 2) {
setMaker((maker) => { setMaker((maker) => {
return { return {
...maker, ...maker,
@ -471,10 +510,11 @@ const MakerForm = ({
longitude: parseFloat(pos[1].toPrecision(6)), longitude: parseFloat(pos[1].toPrecision(6)),
}; };
}); });
if (!maker.paymentMethods.find((method) => method.icon === 'cash')) { const cashMethod = maker.paymentMethods.find((method) => method.icon === 'cash');
if (cashMethod !== null) {
const newMethods = maker.paymentMethods; const newMethods = maker.paymentMethods;
const cash = fiatMethods.find((method) => method.icon === 'cash'); const cash = fiatMethods.find((method) => method.icon === 'cash');
if (cash) { if (cash !== null) {
newMethods.unshift(cash); newMethods.unshift(cash);
handlePaymentMethodChange(newMethods); handlePaymentMethodChange(newMethods);
} }
@ -482,7 +522,7 @@ const MakerForm = ({
} }
}; };
const SummaryText = function () { const SummaryText = (): JSX.Element => {
return ( return (
<Typography <Typography
component='h2' component='h2'
@ -494,7 +534,7 @@ const MakerForm = ({
? fav.mode === 'fiat' ? fav.mode === 'fiat'
? t('Order for ') ? t('Order for ')
: t('Swap of ') : t('Swap of ')
: fav.type == 1 : fav.type === 1
? fav.mode === 'fiat' ? fav.mode === 'fiat'
? t('Buy BTC for ') ? t('Buy BTC for ')
: t('Swap into LN ') : t('Swap into LN ')
@ -512,7 +552,7 @@ const MakerForm = ({
{' ' + (fav.mode === 'fiat' ? currencyCode : 'Sats')} {' ' + (fav.mode === 'fiat' ? currencyCode : 'Sats')}
{maker.isExplicit {maker.isExplicit
? t(' of {{satoshis}} Satoshis', { satoshis: pn(maker.satoshis) }) ? t(' of {{satoshis}} Satoshis', { satoshis: pn(maker.satoshis) })
: maker.premium == 0 : maker.premium === 0
? fav.mode === 'fiat' ? fav.mode === 'fiat'
? t(' at market price') ? t(' at market price')
: '' : ''
@ -531,7 +571,7 @@ const MakerForm = ({
setOpenDialogs(false); setOpenDialogs(false);
}} }}
onClickDone={handleCreateOrder} onClickDone={handleCreateOrder}
hasRobot={robot.avatarLoaded} hasRobot={garage.getRobot().avatarLoaded}
onClickGenerateRobot={onClickGenerateRobot} onClickGenerateRobot={onClickGenerateRobot}
/> />
<F2fMapDialog <F2fMapDialog
@ -542,19 +582,19 @@ const MakerForm = ({
message={t( message={t(
'To protect your privacy, the exact location you pin will be slightly randomized.', 'To protect your privacy, the exact location you pin will be slightly randomized.',
)} )}
orderType={fav.type || 0} orderType={fav?.type ?? 0}
onClose={(pos?: [number, number]) => { onClose={(pos?: [number, number]) => {
if (pos) handleAddLocation(pos); if (pos != null) handleAddLocation(pos);
setOpenWorldmap(false); setOpenWorldmap(false);
}} }}
zoom={maker.latitude && maker.longitude ? 6 : undefined} zoom={maker.latitude && maker.longitude ? 6 : undefined}
/> />
<Collapse in={limits.list.length == 0}> <Collapse in={Object.keys(limits).lenght === 0}>
<div style={{ display: limits.list.length == 0 ? '' : 'none' }}> <div style={{ display: Object.keys(limits) === 0 ? '' : 'none' }}>
<LinearProgress /> <LinearProgress />
</div> </div>
</Collapse> </Collapse>
<Collapse in={!(limits.list.length == 0 || collapseAll)}> <Collapse in={!(Object.keys(limits).lenght === 0 || collapseAll)}>
<Grid container justifyContent='space-between' spacing={0} sx={{ maxHeight: '1em' }}> <Grid container justifyContent='space-between' spacing={0} sx={{ maxHeight: '1em' }}>
<Grid item> <Grid item>
<IconButton <IconButton
@ -590,7 +630,7 @@ const MakerForm = ({
> >
<Switch <Switch
size='small' size='small'
disabled={limits.list.length == 0} disabled={Object.keys(limits).length === 0}
checked={maker.advancedOptions} checked={maker.advancedOptions}
onChange={handleClickAdvanced} onChange={handleClickAdvanced}
/> />
@ -611,9 +651,9 @@ const MakerForm = ({
<FormHelperText sx={{ textAlign: 'center' }}>{t('Swap?')}</FormHelperText> <FormHelperText sx={{ textAlign: 'center' }}>{t('Swap?')}</FormHelperText>
<Checkbox <Checkbox
sx={{ position: 'relative', bottom: '0.3em' }} sx={{ position: 'relative', bottom: '0.3em' }}
checked={fav.mode == 'swap'} checked={fav.mode === 'swap'}
onClick={() => { onClick={() => {
handleCurrencyChange(fav.mode == 'swap' ? 1 : 1000); handleCurrencyChange(fav.mode === 'swap' ? 1 : 1000);
}} }}
/> />
</FormControl> </FormControl>
@ -636,10 +676,10 @@ const MakerForm = ({
type: 1, type: 1,
}); });
}} }}
disableElevation={fav.type == 1} disableElevation={fav.type === 1}
sx={{ sx={{
backgroundColor: fav.type == 1 ? 'primary.main' : 'background.paper', backgroundColor: fav.type === 1 ? 'primary.main' : 'background.paper',
color: fav.type == 1 ? 'background.paper' : 'text.secondary', color: fav.type === 1 ? 'background.paper' : 'text.secondary',
':hover': { ':hover': {
color: 'background.paper', color: 'background.paper',
}, },
@ -656,11 +696,11 @@ const MakerForm = ({
type: 0, type: 0,
}); });
}} }}
disableElevation={fav.type == 0} disableElevation={fav.type === 0}
color='secondary' color='secondary'
sx={{ sx={{
backgroundColor: fav.type == 0 ? 'secondary.main' : 'background.paper', backgroundColor: fav.type === 0 ? 'secondary.main' : 'background.paper',
color: fav.type == 0 ? 'background.secondary' : 'text.secondary', color: fav.type === 0 ? 'background.secondary' : 'text.secondary',
':hover': { ':hover': {
color: 'background.paper', color: 'background.paper',
}, },
@ -730,15 +770,15 @@ const MakerForm = ({
disabled={makerHasAmountRange} disabled={makerHasAmountRange}
variant={makerHasAmountRange ? 'filled' : 'outlined'} variant={makerHasAmountRange ? 'filled' : 'outlined'}
error={ error={
maker.amount != '' && maker.amount !== '' &&
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1]) (maker.amount < amountLimits[0] || maker.amount > amountLimits[1])
} }
helperText={ helperText={
maker.amount < amountLimits[0] && maker.amount != '' maker.amount < amountLimits[0] && maker.amount !== ''
? t('Must be more than {{minAmount}}', { ? t('Must be more than {{minAmount}}', {
minAmount: pn(parseFloat(amountLimits[0].toPrecision(2))), minAmount: pn(parseFloat(amountLimits[0].toPrecision(2))),
}) })
: maker.amount > amountLimits[1] && maker.amount != '' : maker.amount > amountLimits[1] && maker.amount !== ''
? t('Must be less than {{maxAmount}}', { ? t('Must be less than {{maxAmount}}', {
maxAmount: pn(parseFloat(amountLimits[1].toPrecision(2))), maxAmount: pn(parseFloat(amountLimits[1].toPrecision(2))),
}) })
@ -761,7 +801,7 @@ const MakerForm = ({
}} }}
/> />
</Tooltip> </Tooltip>
{fav.mode === 'swap' && maker.amount != '' ? ( {fav.mode === 'swap' && maker.amount !== '' ? (
<FormHelperText sx={{ textAlign: 'center' }}> <FormHelperText sx={{ textAlign: 'center' }}>
{amountLabel.helper} {amountLabel.helper}
</FormHelperText> </FormHelperText>
@ -780,7 +820,7 @@ const MakerForm = ({
inputProps={{ inputProps={{
style: { textAlign: 'center' }, style: { textAlign: 'center' },
}} }}
value={fav.currency == 0 ? 1 : fav.currency} value={fav.currency === 0 ? 1 : fav.currency}
onChange={(e) => { onChange={(e) => {
handleCurrencyChange(e.target.value); handleCurrencyChange(e.target.value);
}} }}
@ -806,13 +846,15 @@ const MakerForm = ({
<Grid item xs={12}> <Grid item xs={12}>
<AutocompletePayments <AutocompletePayments
onAutocompleteChange={handlePaymentMethodChange} onAutocompleteChange={handlePaymentMethodChange}
onClick={() => setOpenWorldmap(true)} onClick={() => {
setOpenWorldmap(true);
}}
optionsType={fav.mode} optionsType={fav.mode}
error={maker.badPaymentMethod} error={maker.badPaymentMethod}
helperText={maker.badPaymentMethod ? t('Must be shorter than 65 characters') : ''} helperText={maker.badPaymentMethod ? t('Must be shorter than 65 characters') : ''}
label={fav.mode == 'swap' ? t('Swap Destination(s)') : t('Fiat Payment Method(s)')} label={fav.mode === 'swap' ? t('Swap Destination(s)') : t('Fiat Payment Method(s)')}
tooltipTitle={t( tooltipTitle={t(
fav.mode == 'swap' fav.mode === 'swap'
? t('Enter the destination of the Lightning swap') ? t('Enter the destination of the Lightning swap')
: 'Enter your preferred fiat payment methods. Fast methods are highly recommended.', : 'Enter your preferred fiat payment methods. Fast methods are highly recommended.',
)} )}
@ -844,7 +886,9 @@ const MakerForm = ({
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
borderColor: theme.palette.text.disabled, borderColor: theme.palette.text.disabled,
}} }}
onClick={() => setOpenWorldmap(true)} onClick={() => {
setOpenWorldmap(true);
}}
> >
{t('Face to Face Location')} {t('Face to Face Location')}
<Map style={{ paddingLeft: 5 }} /> <Map style={{ paddingLeft: 5 }} />
@ -913,7 +957,7 @@ const MakerForm = ({
<TextField <TextField
fullWidth fullWidth
label={t('Satoshis')} label={t('Satoshis')}
error={maker.badSatoshisText != ''} error={maker.badSatoshisText !== ''}
helperText={maker.badSatoshisText === '' ? null : maker.badSatoshisText} helperText={maker.badSatoshisText === '' ? null : maker.badSatoshisText}
type='number' type='number'
required={true} required={true}
@ -933,7 +977,7 @@ const MakerForm = ({
<div style={{ display: maker.isExplicit ? 'none' : '' }}> <div style={{ display: maker.isExplicit ? 'none' : '' }}>
<TextField <TextField
fullWidth fullWidth
error={maker.badPremiumText != ''} error={maker.badPremiumText !== ''}
helperText={maker.badPremiumText === '' ? null : maker.badPremiumText} helperText={maker.badPremiumText === '' ? null : maker.badPremiumText}
label={t('Premium over Market (%)')} label={t('Premium over Market (%)')}
type='number' type='number'
@ -1096,6 +1140,15 @@ const MakerForm = ({
</Grid> </Grid>
</Collapse> </Collapse>
<SelectCoordinator
coordinator={maker.coordinator}
setCoordinator={(coordinator) => {
setMaker((maker) => {
return { ...maker, coordinator };
});
}}
/>
<Grid container direction='column' alignItems='center'> <Grid container direction='column' alignItems='center'>
<Grid item> <Grid item>
<SummaryText /> <SummaryText />
@ -1152,7 +1205,7 @@ const MakerForm = ({
</Typography> </Typography>
</Grid> </Grid>
<Collapse in={!(limits.list.length == 0)}> <Collapse in={!(Object.keys(limits).length === 0)}>
<Tooltip <Tooltip
placement='top' placement='top'
enterTouchDelay={0} enterTouchDelay={0}

View File

@ -0,0 +1,125 @@
import React, { useContext } from 'react';
import {
Grid,
Select,
MenuItem,
Box,
Tooltip,
Typography,
type SelectChangeEvent,
} from '@mui/material';
import RobotAvatar from '../RobotAvatar';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { useTheme } from '@emotion/react';
import { useTranslation } from 'react-i18next';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
interface SelectCoordinatorProps {
coordinator: string;
setCoordinator: (coordinator: string) => void;
}
const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({ coordinator, setCoordinator }) => {
const { setOpen, hostUrl } = useContext<UseAppStoreType>(AppContext);
const { federation, setFocusedCoordinator, sortedCoordinators } =
useContext<UseFederationStoreType>(FederationContext);
const theme = useTheme();
const { t } = useTranslation();
const onClickCurrentCoordinator = function (shortAlias: string): void {
setFocusedCoordinator(shortAlias);
setOpen((open) => {
return { ...open, coordinator: true };
});
};
const handleCoordinatorChange = (e: SelectChangeEvent<string>): void => {
setCoordinator(e.target.value);
};
return (
<Grid item>
<Tooltip
placement='top'
enterTouchDelay={500}
enterDelay={700}
enterNextDelay={2000}
title={t(
'The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!',
)}
>
<Box
sx={{
backgroundColor: 'background.paper',
border: '1px solid',
borderRadius: '4px',
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
'&:hover': {
borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#2f2f2f',
},
}}
>
<Typography variant='caption' color='text.secondary'>
&nbsp;{t('Order Host')}
</Typography>
<Grid container>
<Grid
item
xs={3}
sx={{ cursor: 'pointer', position: 'relative', left: '0.3em', bottom: '0.1em' }}
onClick={() => {
onClickCurrentCoordinator(coordinator);
}}
>
<Grid item>
<RobotAvatar
nickname={coordinator}
coordinator={true}
style={{ width: '3em', height: '3em' }}
smooth={true}
flipHorizontally={true}
baseUrl={hostUrl}
small={true}
/>
</Grid>
</Grid>
<Grid item xs={9}>
<Select
variant='standard'
fullWidth
required={true}
inputProps={{
style: { textAlign: 'center' },
}}
value={coordinator}
onChange={handleCoordinatorChange}
disableUnderline
>
{sortedCoordinators.map((shortAlias: string): JSX.Element | null => {
let row: JSX.Element | null = null;
if (
shortAlias === coordinator ||
(federation.getCoordinator(shortAlias).enabled === true &&
federation.getCoordinator(shortAlias).info !== undefined)
) {
row = (
<MenuItem key={shortAlias} value={shortAlias}>
<Typography>{federation.getCoordinator(shortAlias).longAlias}</Typography>
</MenuItem>
);
}
return row;
})}
</Select>
</Grid>
</Grid>
</Box>
</Tooltip>
</Grid>
);
};
export default SelectCoordinator;

View File

@ -2,13 +2,13 @@ import React, { useContext, useEffect, useState } from 'react';
import { apiClient } from '../../services/api'; import { apiClient } from '../../services/api';
import { MapContainer, GeoJSON, useMapEvents, TileLayer, Tooltip, Marker } from 'react-leaflet'; import { MapContainer, GeoJSON, useMapEvents, TileLayer, Tooltip, Marker } from 'react-leaflet';
import { useTheme, LinearProgress } from '@mui/material'; import { useTheme, LinearProgress } from '@mui/material';
import { UseAppStoreType, AppContext } from '../../contexts/AppContext'; import { type GeoJsonObject } from 'geojson';
import { GeoJsonObject } from 'geojson'; import { DivIcon, type LeafletMouseEvent } from 'leaflet';
import { DivIcon, LeafletMouseEvent } from 'leaflet'; import { type PublicOrder } from '../../models';
import { PublicOrder } from '../../models';
import OrderTooltip from '../Charts/helpers/OrderTooltip'; import OrderTooltip from '../Charts/helpers/OrderTooltip';
import getWorldmapGeojson from '../../geo/Web'; import getWorldmapGeojson from '../../geo/Web';
import MarkerClusterGroup from '@christopherpickering/react-leaflet-markercluster'; import MarkerClusterGroup from '@christopherpickering/react-leaflet-markercluster';
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
interface MapPinProps { interface MapPinProps {
fillColor: string; fillColor: string;
@ -17,7 +17,6 @@ interface MapPinProps {
} }
const MapPin = ({ fillColor, outlineColor, eyesColor }: MapPinProps): string => { const MapPin = ({ fillColor, outlineColor, eyesColor }: MapPinProps): string => {
console.log(fillColor, outlineColor);
return `<svg id='robot_pin' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18.66 29.68'><defs><style>.body{fill:${fillColor};}.eyes{fill:${eyesColor};}.outline{fill:${outlineColor};}</style></defs><path class='body' d='M18,8A9.13,9.13,0,0,0,10.89.62,10.88,10.88,0,0,0,9.33.49,10.88,10.88,0,0,0,7.77.62,9.13,9.13,0,0,0,.66,8a12.92,12.92,0,0,0,1.19,8.25C2.68,18.09,7.47,27.6,9.07,29c0,.12.11.19.19.19l.07,0,.07,0c.08,0,.15-.07.19-.19,1.6-1.41,6.39-10.92,7.22-12.8A12.92,12.92,0,0,0,18,8Z'/><path class='outline' d='M9.23,29.6a.57.57,0,0,1-.5-.35C7,27.57,2.24,18.09,1.48,16.38A13.57,13.57,0,0,1,.26,7.87C1.18,3.78,4,.92,7.7.23h0A8.38,8.38,0,0,1,11,.24h0c3.74.69,6.52,3.55,7.44,7.64a13.57,13.57,0,0,1-1.22,8.51c-.76,1.71-5.5,11.19-7.25,12.87a.57.57,0,0,1-.55.35H9.23ZM8,1,7.85,1a8.68,8.68,0,0,0-6.8,7C.5,10.52.86,13,2.22,16.05c.9,2,5.62,11.32,7.11,12.65,1.49-1.33,6.21-10.63,7.11-12.65,1.36-3.07,1.72-5.53,1.17-8h0a8.68,8.68,0,0,0-6.8-7l-.12,0A10.47,10.47,0,0,0,9.33.89,10.3,10.3,0,0,0,8,1Z'/><rect class='outline' x='3.12' y='6.34' width='12.53' height='7.76' rx='3.88'/><rect class='eyes' x='5.02' y='7.82' width='2.16' height='2.34' rx='1.02'/><rect class='eyes' x='11.25' y='7.82' width='2.16' height='2.34' rx='1.02'/><path class='eyes' d='M9.24,12.76A3.57,3.57,0,0,1,7,12a.4.4,0,1,1,.53-.61,2.78,2.78,0,0,0,3.49,0,.4.4,0,0,1,.48.65A3.71,3.71,0,0,1,9.24,12.76Z'/></svg>`; return `<svg id='robot_pin' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18.66 29.68'><defs><style>.body{fill:${fillColor};}.eyes{fill:${eyesColor};}.outline{fill:${outlineColor};}</style></defs><path class='body' d='M18,8A9.13,9.13,0,0,0,10.89.62,10.88,10.88,0,0,0,9.33.49,10.88,10.88,0,0,0,7.77.62,9.13,9.13,0,0,0,.66,8a12.92,12.92,0,0,0,1.19,8.25C2.68,18.09,7.47,27.6,9.07,29c0,.12.11.19.19.19l.07,0,.07,0c.08,0,.15-.07.19-.19,1.6-1.41,6.39-10.92,7.22-12.8A12.92,12.92,0,0,0,18,8Z'/><path class='outline' d='M9.23,29.6a.57.57,0,0,1-.5-.35C7,27.57,2.24,18.09,1.48,16.38A13.57,13.57,0,0,1,.26,7.87C1.18,3.78,4,.92,7.7.23h0A8.38,8.38,0,0,1,11,.24h0c3.74.69,6.52,3.55,7.44,7.64a13.57,13.57,0,0,1-1.22,8.51c-.76,1.71-5.5,11.19-7.25,12.87a.57.57,0,0,1-.55.35H9.23ZM8,1,7.85,1a8.68,8.68,0,0,0-6.8,7C.5,10.52.86,13,2.22,16.05c.9,2,5.62,11.32,7.11,12.65,1.49-1.33,6.21-10.63,7.11-12.65,1.36-3.07,1.72-5.53,1.17-8h0a8.68,8.68,0,0,0-6.8-7l-.12,0A10.47,10.47,0,0,0,9.33.89,10.3,10.3,0,0,0,8,1Z'/><rect class='outline' x='3.12' y='6.34' width='12.53' height='7.76' rx='3.88'/><rect class='eyes' x='5.02' y='7.82' width='2.16' height='2.34' rx='1.02'/><rect class='eyes' x='11.25' y='7.82' width='2.16' height='2.34' rx='1.02'/><path class='eyes' d='M9.24,12.76A3.57,3.57,0,0,1,7,12a.4.4,0,1,1,.53-.61,2.78,2.78,0,0,0,3.49,0,.4.4,0,0,1,.48.65A3.71,3.71,0,0,1,9.24,12.76Z'/></svg>`;
}; };
@ -45,13 +44,17 @@ const Map = ({
interactive = false, interactive = false,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const { baseUrl } = useContext<UseAppStoreType>(AppContext); const { hostUrl } = useContext<UseAppStoreType>(AppContext);
const [worldmap, setWorldmap] = useState<GeoJsonObject | undefined>(); const [worldmap, setWorldmap] = useState<GeoJsonObject | undefined>();
useEffect(() => { useEffect(() => {
if (!worldmap) { getWorldmapGeojson(apiClient, hostUrl)
getWorldmapGeojson(apiClient, baseUrl).then(setWorldmap); .then((data) => {
} setWorldmap(data);
})
.catch((error) => {
console.error('Error:', error);
});
}, []); }, []);
const RobotMarker = ( const RobotMarker = (
@ -59,8 +62,8 @@ const Map = ({
position: [number, number], position: [number, number],
orderType: number, orderType: number,
order?: PublicOrder, order?: PublicOrder,
) => { ): JSX.Element => {
const fillColor = orderType == 1 ? theme.palette.primary.main : theme.palette.secondary.main; const fillColor = orderType === 1 ? theme.palette.primary.main : theme.palette.secondary.main;
const outlineColor = 'black'; const outlineColor = 'black';
const eyesColor = 'white'; const eyesColor = 'white';
@ -77,10 +80,12 @@ const Map = ({
}) })
} }
eventHandlers={{ eventHandlers={{
click: (_event: LeafletMouseEvent) => order?.id && onOrderClicked(order.id), click: (_event: LeafletMouseEvent) => {
order?.id && onOrderClicked(order.id);
},
}} }}
> >
{order && ( {order != null && (
<Tooltip direction='top'> <Tooltip direction='top'>
<OrderTooltip order={order} /> <OrderTooltip order={order} />
</Tooltip> </Tooltip>
@ -89,7 +94,7 @@ const Map = ({
); );
}; };
const LocationMarker = () => { const LocationMarker = (): JSX.Element => {
useMapEvents({ useMapEvents({
click(event: LeafletMouseEvent) { click(event: LeafletMouseEvent) {
if (interactive) { if (interactive) {
@ -98,16 +103,16 @@ const Map = ({
}, },
}); });
return position ? RobotMarker('marker', position, orderType || 0) : <></>; return position != null ? RobotMarker('marker', position, orderType ?? 0) : <></>;
}; };
const getOrderMarkers = () => { const getOrderMarkers = (): JSX.Element => {
if (orders.length < 1) return <></>; if (orders.length < 1) return <></>;
return ( return (
<MarkerClusterGroup showCoverageOnHover={true} disableClusteringAtZoom={14}> <MarkerClusterGroup showCoverageOnHover={true} disableClusteringAtZoom={14}>
{orders.map((order) => { {orders.map((order) => {
if (!order.latitude || !order.longitude) return <></>; if (!order?.latitude || !order?.longitude) return <></>;
return RobotMarker(order.id, [order.latitude, order.longitude], order.type || 0, order); return RobotMarker(order.id, [order.latitude, order.longitude], order.type ?? 0, order);
})} })}
</MarkerClusterGroup> </MarkerClusterGroup>
); );
@ -117,12 +122,12 @@ const Map = ({
<MapContainer <MapContainer
maxZoom={15} maxZoom={15}
center={center ?? [0, 0]} center={center ?? [0, 0]}
zoom={zoom ? zoom : 2} zoom={zoom ?? 2}
attributionControl={false} attributionControl={false}
style={{ height: '100%', width: '100%', backgroundColor: theme.palette.background.paper }} style={{ height: '100%', width: '100%', backgroundColor: theme.palette.background.paper }}
> >
{!useTiles && !worldmap && <LinearProgress />} {!useTiles && worldmap == null && <LinearProgress />}
{!useTiles && worldmap && ( {!useTiles && worldmap != null && (
<GeoJSON <GeoJSON
data={worldmap} data={worldmap}
style={{ style={{

View File

@ -1,21 +1,19 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Tooltip, Tooltip,
Alert, Alert,
useTheme,
IconButton, IconButton,
type TooltipProps, type TooltipProps,
styled, styled,
tooltipClasses, tooltipClasses,
} from '@mui/material'; } from '@mui/material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { type Order } from '../../models';
import Close from '@mui/icons-material/Close'; import Close from '@mui/icons-material/Close';
import { type Page } from '../../basic/NavBar'; import { type Page } from '../../basic/NavBar';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
interface NotificationsProps { interface NotificationsProps {
order: Order | undefined;
rewards: number | undefined; rewards: number | undefined;
page: Page; page: Page;
openProfile: () => void; openProfile: () => void;
@ -64,7 +62,6 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
})); }));
const Notifications = ({ const Notifications = ({
order,
rewards, rewards,
page, page,
windowWidth, windowWidth,
@ -72,6 +69,7 @@ const Notifications = ({
}: NotificationsProps): JSX.Element => { }: NotificationsProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const [message, setMessage] = useState<NotificationMessage>(emptyNotificationMessage); const [message, setMessage] = useState<NotificationMessage>(emptyNotificationMessage);
const [inFocus, setInFocus] = useState<boolean>(true); const [inFocus, setInFocus] = useState<boolean>(true);
@ -86,8 +84,8 @@ const Notifications = ({
const position = windowWidth > 60 ? { top: '4em', right: '0em' } : { top: '0.5em', left: '50%' }; const position = windowWidth > 60 ? { top: '4em', right: '0em' } : { top: '0.5em', left: '50%' };
const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange'); const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange');
const moveToOrderPage = function () { const moveToOrderPage = function (): void {
navigate(`/order/${order?.id}`); navigate(`/order/${String(garage.getOrder()?.id)}`);
setShow(false); setShow(false);
}; };
@ -108,7 +106,7 @@ const Notifications = ({
const Messages: MessagesProps = { const Messages: MessagesProps = {
bondLocked: { bondLocked: {
title: t(`${order?.is_maker ? 'Maker' : 'Taker'} bond locked`), title: t(`${garage.getOrder()?.is_maker === true ? 'Maker' : 'Taker'} bond locked`),
severity: 'info', severity: 'info',
onClick: moveToOrderPage, onClick: moveToOrderPage,
sound: audio.ding, sound: audio.ding,
@ -208,28 +206,32 @@ const Notifications = ({
}, },
}; };
const notify = function (message: NotificationMessage) { const notify = function (message: NotificationMessage): void {
if (message.title != '') { if (message.title !== '') {
setMessage(message); setMessage(message);
setShow(true); setShow(true);
setTimeout(() => { setTimeout(() => {
setShow(false); setShow(false);
}, message.timeout); }, message.timeout);
if (message.sound != null) { if (message.sound != null) {
message.sound.play(); void message.sound.play();
} }
if (!inFocus) { if (!inFocus) {
setTitleAnimation( setTitleAnimation(
setInterval(function () { setInterval(function () {
const title = document.title; const title = document.title;
document.title = title == basePageTitle ? message.pageTitle : basePageTitle; document.title = title === basePageTitle ? message.pageTitle : basePageTitle;
}, 1000), }, 1000),
); );
} }
} }
}; };
const handleStatusChange = function (oldStatus: number | undefined, status: number) { const handleStatusChange = function (oldStatus: number | undefined, status: number): void {
const order = garage.getOrder();
if (order === null) return;
let message = emptyNotificationMessage; let message = emptyNotificationMessage;
// Order status descriptions: // Order status descriptions:
@ -252,37 +254,35 @@ const Notifications = ({
// 17: 'Maker lost dispute' // 17: 'Maker lost dispute'
// 18: 'Taker lost dispute' // 18: 'Taker lost dispute'
if (status == 5 && oldStatus != 5) { if (status === 5 && oldStatus !== 5) {
message = Messages.expired; message = Messages.expired;
} else if (oldStatus == undefined) { } else if (oldStatus === undefined) {
message = emptyNotificationMessage; message = emptyNotificationMessage;
} else if (order?.is_maker && status > 0 && oldStatus == 0) { } else if (order.is_maker && status > 0 && oldStatus === 0) {
message = Messages.bondLocked; message = Messages.bondLocked;
} else if (order?.is_taker && status > 5 && oldStatus <= 5) { } else if (order.is_taker && status > 5 && oldStatus <= 5) {
message = Messages.bondLocked; message = Messages.bondLocked;
} else if (order?.is_maker && status > 5 && oldStatus <= 5) { } else if (order.is_maker && status > 5 && oldStatus <= 5) {
message = Messages.taken; message = Messages.taken;
} else if (order?.is_seller && status > 7 && oldStatus < 7) { } else if (order.is_seller && status > 7 && oldStatus < 7) {
message = Messages.escrowLocked; message = Messages.escrowLocked;
} else if ([9, 10].includes(status) && oldStatus < 9) { } else if ([9, 10].includes(status) && oldStatus < 9) {
message = Messages.chat; message = Messages.chat;
} else if (order?.is_seller && [13, 14, 15].includes(status) && oldStatus < 13) { } else if (order.is_seller && [13, 14, 15].includes(status) && oldStatus < 13) {
message = Messages.successful; message = Messages.successful;
} else if (order?.is_buyer && status == 14 && oldStatus != 14) { } else if (order.is_buyer && status === 14 && oldStatus !== 14) {
message = Messages.successful; message = Messages.successful;
} else if (order?.is_buyer && status == 15 && oldStatus < 14) { } else if (order.is_buyer && status === 15 && oldStatus < 14) {
message = Messages.routingFailed; message = Messages.routingFailed;
} else if (status == 11 && oldStatus < 11) { } else if (status === 11 && oldStatus < 11) {
message = Messages.dispute;
} else if (status == 11 && oldStatus < 11) {
message = Messages.dispute; message = Messages.dispute;
} else if ( } else if (
((order?.is_maker && status == 18) || (order?.is_taker && status == 17)) && ((order.is_maker && status === 18) || (order.is_taker && status === 17)) &&
oldStatus < 17 oldStatus < 17
) { ) {
message = Messages.disputeWinner; message = Messages.disputeWinner;
} else if ( } else if (
((order?.is_maker && status == 17) || (order?.is_taker && status == 18)) && ((order.is_maker && status === 17) || (order.is_taker && status === 18)) &&
oldStatus < 17 oldStatus < 17
) { ) {
message = Messages.disputeLoser; message = Messages.disputeLoser;
@ -293,20 +293,23 @@ const Notifications = ({
// Notify on order status change // Notify on order status change
useEffect(() => { useEffect(() => {
if (order != undefined && order.status != oldOrderStatus) { const order = garage.getOrder();
handleStatusChange(oldOrderStatus, order.status); if (order !== null) {
setOldOrderStatus(order.status); if (order.status !== oldOrderStatus) {
} else if (order != undefined && order.chat_last_index > oldChatIndex) { handleStatusChange(oldOrderStatus, order.status);
if (page != 'order') { setOldOrderStatus(order.status);
notify(Messages.chatMessage); } else if (order.chat_last_index > oldChatIndex) {
if (page !== 'order') {
notify(Messages.chatMessage);
}
setOldChatIndex(order.chat_last_index);
} }
setOldChatIndex(order.chat_last_index);
} }
}, [order]); }, [orderUpdatedAt]);
// Notify on rewards change // Notify on rewards change
useEffect(() => { useEffect(() => {
if (rewards != undefined) { if (rewards !== undefined) {
if (rewards > oldRewards) { if (rewards > oldRewards) {
notify(Messages.rewards); notify(Messages.rewards);
} }
@ -316,7 +319,7 @@ const Notifications = ({
// Set blinking page title and clear on visibility change > infocus // Set blinking page title and clear on visibility change > infocus
useEffect(() => { useEffect(() => {
if (titleAnimation != undefined && inFocus) { if (titleAnimation !== undefined && inFocus) {
clearInterval(titleAnimation); clearInterval(titleAnimation);
} }
}, [inFocus]); }, [inFocus]);

View File

@ -7,9 +7,9 @@ interface Props {
totalSecsExp: number; totalSecsExp: number;
} }
const LinearDeterminate = ({ expiresAt, totalSecsExp }: Props): JSX.Element => { const LinearDeterminate: React.FC<Props> = ({ expiresAt, totalSecsExp }) => {
const timePercentLeft = function () { const timePercentLeft = function (): number {
if (expiresAt && totalSecsExp) { if (Boolean(expiresAt) && Boolean(totalSecsExp)) {
const lapseTime = calcTimeDelta(new Date(expiresAt)).total / 1000; const lapseTime = calcTimeDelta(new Date(expiresAt)).total / 1000;
return (lapseTime / totalSecsExp) * 100; return (lapseTime / totalSecsExp) * 100;
} else { } else {

View File

@ -25,13 +25,13 @@ import { type Order, type Info } from '../../models';
import { ConfirmationDialog } from '../Dialogs'; import { ConfirmationDialog } from '../Dialogs';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { computeSats } from '../../utils'; import { computeSats } from '../../utils';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { UseAppStoreType, AppContext } from '../../contexts/AppContext';
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
interface TakeButtonProps { interface TakeButtonProps {
order: Order;
setOrder: (state: Order) => void;
baseUrl: string; baseUrl: string;
info: Info; info?: Info;
onClickGenerateRobot?: () => void; onClickGenerateRobot?: () => void;
} }
@ -42,15 +42,15 @@ interface OpenDialogsProps {
const closeAll = { inactiveMaker: false, confirmation: false }; const closeAll = { inactiveMaker: false, confirmation: false };
const TakeButton = ({ const TakeButton = ({
order,
setOrder,
baseUrl, baseUrl,
info, info,
onClickGenerateRobot = () => null, onClickGenerateRobot = () => null,
}: TakeButtonProps): JSX.Element => { }: TakeButtonProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const { robot } = useContext<UseAppStoreType>(AppContext); const { settings, origin, hostUrl } = useContext<UseAppStoreType>(AppContext);
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { federation, focusedCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const [takeAmount, setTakeAmount] = useState<string>(''); const [takeAmount, setTakeAmount] = useState<string>('');
const [badRequest, setBadRequest] = useState<string>(''); const [badRequest, setBadRequest] = useState<string>('');
@ -58,11 +58,15 @@ const TakeButton = ({
const [open, setOpen] = useState<OpenDialogsProps>(closeAll); const [open, setOpen] = useState<OpenDialogsProps>(closeAll);
const [satoshis, setSatoshis] = useState<string>(''); const [satoshis, setSatoshis] = useState<string>('');
const satoshisNow = () => { const satoshisNow = (): string | undefined => {
const order = garage.getOrder();
if (order === null) return;
const tradeFee = info?.taker_fee ?? 0; const tradeFee = info?.taker_fee ?? 0;
const defaultRoutingBudget = 0.001; const defaultRoutingBudget = 0.001;
const btc_now = order.satoshis_now / 100000000; const btcNow = order.satoshis_now / 100000000;
const rate = order.amount ? order.amount / btc_now : order.max_amount / btc_now; const rate = order.amount != null ? order.amount / btcNow : order.max_amount / btcNow;
const amount = order.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount); const amount = order.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
const satoshis = computeSats({ const satoshis = computeSats({
amount, amount,
@ -74,12 +78,13 @@ const TakeButton = ({
}; };
useEffect(() => { useEffect(() => {
setSatoshis(satoshisNow()); setSatoshis(satoshisNow() ?? '');
}, [order.satoshis_now, takeAmount, info]); }, [orderUpdatedAt, takeAmount, info]);
const currencyCode: string = order.currency == 1000 ? 'Sats' : currencies[`${order.currency}`]; const currencyCode: string =
garage.getOrder()?.currency === 1000 ? 'Sats' : currencies[`${garage.getOrder()?.currency}`];
const InactiveMakerDialog = function () { const InactiveMakerDialog = function (): JSX.Element {
return ( return (
<Dialog <Dialog
open={open.inactiveMaker} open={open.inactiveMaker}
@ -116,7 +121,15 @@ const TakeButton = ({
); );
}; };
const countdownTakeOrderRenderer = function ({ seconds, completed }) { interface countdownTakeOrderRendererProps {
seconds: number;
completed: boolean;
}
const countdownTakeOrderRenderer = function ({
seconds,
completed,
}: countdownTakeOrderRendererProps): JSX.Element {
if (isNaN(seconds) || completed) { if (isNaN(seconds) || completed) {
return takeOrderButton(); return takeOrderButton();
} else { } else {
@ -132,8 +145,8 @@ const TakeButton = ({
} }
}; };
const handleTakeAmountChange = function (e) { const handleTakeAmountChange = function (e: React.ChangeEvent<HTMLInputElement>): void {
if (e.target.value != '' && e.target.value != null) { if (e.target.value !== '' && e.target.value != null) {
setTakeAmount(`${parseFloat(e.target.value)}`); setTakeAmount(`${parseFloat(e.target.value)}`);
} else { } else {
setTakeAmount(e.target.value); setTakeAmount(e.target.value);
@ -141,18 +154,22 @@ const TakeButton = ({
}; };
const amountHelperText = useMemo(() => { const amountHelperText = useMemo(() => {
const amount = order.currency == 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount); const order = garage.getOrder();
if (amount < Number(order.min_amount) && takeAmount != '') {
if (order === null) return;
const amount = order.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
if (amount < Number(order.min_amount) && takeAmount !== '') {
return t('Too low'); return t('Too low');
} else if (amount > Number(order.max_amount) && takeAmount != '') { } else if (amount > Number(order.max_amount) && takeAmount !== '') {
return t('Too high'); return t('Too high');
} else { } else {
return null; return null;
} }
}, [order, takeAmount]); }, [orderUpdatedAt, takeAmount]);
const onTakeOrderClicked = function () { const onTakeOrderClicked = function (): void {
if (order.maker_status == 'Inactive') { if (garage.getOrder()?.maker_status === 'Inactive') {
setOpen({ inactiveMaker: true, confirmation: false }); setOpen({ inactiveMaker: true, confirmation: false });
} else { } else {
setOpen({ inactiveMaker: false, confirmation: true }); setOpen({ inactiveMaker: false, confirmation: true });
@ -160,17 +177,18 @@ const TakeButton = ({
}; };
const invalidTakeAmount = useMemo(() => { const invalidTakeAmount = useMemo(() => {
const amount = order.currency == 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount); const order = garage.getOrder();
const amount = order?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
return ( return (
amount < Number(order.min_amount) || amount < Number(order?.min_amount) ||
amount > Number(order.max_amount) || amount > Number(order?.max_amount) ||
takeAmount == '' || takeAmount === '' ||
takeAmount == null takeAmount == null
); );
}, [takeAmount, order]); }, [takeAmount, orderUpdatedAt]);
const takeOrderButton = function () { const takeOrderButton = function (): JSX.Element {
if (order.has_range) { if (garage.getOrder()?.has_range) {
return ( return (
<Box <Box
sx={{ sx={{
@ -209,8 +227,8 @@ const TakeButton = ({
required={true} required={true}
value={takeAmount} value={takeAmount}
inputProps={{ inputProps={{
min: order.min_amount, min: garage.getOrder()?.min_amount,
max: order.max_amount, max: garage.getOrder()?.max_amount,
style: { textAlign: 'center' }, style: { textAlign: 'center' },
}} }}
onChange={handleTakeAmountChange} onChange={handleTakeAmountChange}
@ -260,10 +278,10 @@ const TakeButton = ({
</div> </div>
</Grid> </Grid>
</Grid> </Grid>
{satoshis != '0' && satoshis != '' && !invalidTakeAmount ? ( {satoshis !== '0' && satoshis !== '' && !invalidTakeAmount ? (
<Grid item> <Grid item>
<FormHelperText sx={{ position: 'relative', top: '0.15em' }}> <FormHelperText sx={{ position: 'relative', top: '0.15em' }}>
{order.type === 1 {garage.getOrder()?.type === 1
? t('You will receive {{satoshis}} Sats (Approx)', { satoshis }) ? t('You will receive {{satoshis}} Sats (Approx)', { satoshis })
: t('You will send {{satoshis}} Sats (Approx)', { satoshis })} : t('You will send {{satoshis}} Sats (Approx)', { satoshis })}
</FormHelperText> </FormHelperText>
@ -296,33 +314,44 @@ const TakeButton = ({
} }
}; };
const takeOrder = function () { const takeOrder = function (): void {
if (!focusedCoordinator) return;
setLoadingTake(true); setLoadingTake(true);
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post( .post(
baseUrl, url,
'/api/order/?order_id=' + order.id, `/api/order/?order_id=${String(garage.getOrder()?.id)}`,
{ {
action: 'take', action: 'take',
amount: order.currency == 1000 ? takeAmount / 100000000 : takeAmount, amount: garage.getOrder()?.currency === 1000 ? takeAmount / 100000000 : takeAmount,
}, },
{ tokenSHA256: robot.tokenSHA256 }, { tokenSHA256: garage.getRobot().tokenSHA256 },
) )
.then((data) => { .then((data) => {
setLoadingTake(false); setLoadingTake(false);
if (data.bad_request) { if (data?.bad_request !== undefined) {
setBadRequest(data.bad_request); setBadRequest(data.bad_request);
} else { } else {
setOrder(data); garage.updateOrder(data as Order);
setBadRequest(''); setBadRequest('');
} }
})
.catch(() => {
setBadRequest('Request error');
}); });
}; };
return ( return (
<Box> <Box>
<Countdown date={new Date(order.penalty)} renderer={countdownTakeOrderRenderer} /> <Countdown
{badRequest != '' ? ( date={new Date(garage.getOrder()?.penalty ?? '')}
renderer={countdownTakeOrderRenderer}
/>
{badRequest !== '' ? (
<Box style={{ padding: '0.5em' }}> <Box style={{ padding: '0.5em' }}>
<Typography align='center' color='secondary'> <Typography align='center' color='secondary'>
{t(badRequest)} {t(badRequest)}
@ -342,7 +371,7 @@ const TakeButton = ({
setLoadingTake(true); setLoadingTake(true);
setOpen(closeAll); setOpen(closeAll);
}} }}
hasRobot={robot.avatarLoaded} hasRobot={garage.getRobot().avatarLoaded}
onClickGenerateRobot={onClickGenerateRobot} onClickGenerateRobot={onClickGenerateRobot}
/> />
<InactiveMakerDialog /> <InactiveMakerDialog />

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo, useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
List, List,
@ -15,7 +15,7 @@ import {
Typography, Typography,
IconButton, IconButton,
Tooltip, Tooltip,
Button, ListItemButton,
} from '@mui/material'; } from '@mui/material';
import Countdown, { type CountdownRenderProps, zeroPad } from 'react-countdown'; import Countdown, { type CountdownRenderProps, zeroPad } from 'react-countdown';
@ -36,42 +36,48 @@ import { PaymentStringAsIcons } from '../../components/PaymentMethods';
import { FlagWithProps, SendReceiveIcon } from '../Icons'; import { FlagWithProps, SendReceiveIcon } from '../Icons';
import LinearDeterminate from './LinearDeterminate'; import LinearDeterminate from './LinearDeterminate';
import { type Order, type Info } from '../../models'; import type { Order, Coordinator } from '../../models';
import { statusBadgeColor, pn, amountToString, computeSats } from '../../utils'; import { statusBadgeColor, pn, amountToString, computeSats } from '../../utils';
import TakeButton from './TakeButton'; import TakeButton from './TakeButton';
import { F2fMapDialog } from '../Dialogs'; import { F2fMapDialog } from '../Dialogs';
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
interface OrderDetailsProps { interface OrderDetailsProps {
order: Order; coordinator: Coordinator;
setOrder: (state: Order) => void; onClickCoordinator?: () => void;
info: Info;
baseUrl: string; baseUrl: string;
hasRobot: boolean;
onClickGenerateRobot?: () => void; onClickGenerateRobot?: () => void;
} }
const OrderDetails = ({ const OrderDetails = ({
order, coordinator,
info, onClickCoordinator = () => null,
setOrder,
baseUrl, baseUrl,
hasRobot,
onClickGenerateRobot = () => null, onClickGenerateRobot = () => null,
}: OrderDetailsProps): JSX.Element => { }: OrderDetailsProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme(); const theme = useTheme();
const { orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { currentOrder } = useContext<UseFederationStoreType>(FederationContext);
const currencyCode: string = currencies[`${order.currency}`]; const currencyCode: string = currencies[(currentOrder.order?.currency ?? 1).toString()];
const [showSatsDetails, setShowSatsDetails] = useState<boolean>(false); const [showSatsDetails, setShowSatsDetails] = useState<boolean>(false);
const [openWorldmap, setOpenWorldmap] = useState<boolean>(false); const [openWorldmap, setOpenWorldmap] = useState<boolean>(false);
const amountString = useMemo(() => { const amountString = useMemo(() => {
// precision to 8 decimal if currency is BTC otherwise 4 decimals // precision to 8 decimal if currency is BTC otherwise 4 decimals
if (order.currency == 1000) { const order = currentOrder.order;
if (order === null) return;
if (order.currency === 1000) {
return ( return (
amountToString( amountToString(
order.amount * 100000000, (order.amount * 100000000).toString(),
order.amount ? false : order.has_range, order.amount > 0 ? false : order.has_range,
order.min_amount * 100000000, order.min_amount * 100000000,
order.max_amount * 100000000, order.max_amount * 100000000,
) + ' Sats' ) + ' Sats'
@ -79,14 +85,14 @@ const OrderDetails = ({
} else { } else {
return ( return (
amountToString( amountToString(
order.amount, order.amount.toString(),
order.amount ? false : order.has_range, order.amount > 0 ? false : order.has_range,
order.min_amount, order.min_amount,
order.max_amount, order.max_amount,
) + ` ${currencyCode}` ) + ` ${currencyCode}`
); );
} }
}, [order.currency, order.amount, order.min_amount, order.max_amount, order.has_range]); }, [orderUpdatedAt]);
// Countdown Renderer callback with condition // Countdown Renderer callback with condition
const countdownRenderer = function ({ const countdownRenderer = function ({
@ -95,13 +101,13 @@ const OrderDetails = ({
minutes, minutes,
seconds, seconds,
completed, completed,
}: CountdownRenderProps) { }: CountdownRenderProps): JSX.Element {
if (completed) { if (completed) {
// Render a completed state // Render a completed state
return <span> {t('The order has expired')}</span>; return <span> {t('The order has expired')}</span>;
} else { } else {
let color = 'inherit'; let color = 'inherit';
const fraction_left = total / 1000 / order.total_secs_exp; const fraction_left = total / 1000 / (currentOrder.order?.total_secs_exp ?? 1);
// Make orange at 25% of time left // Make orange at 25% of time left
if (fraction_left < 0.25) { if (fraction_left < 0.25) {
color = theme.palette.warning.main; color = theme.palette.warning.main;
@ -121,18 +127,26 @@ const OrderDetails = ({
} }
}; };
const timerRenderer = function (seconds: number) { const timerRenderer = function (seconds: number): JSX.Element {
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds - hours * 3600) / 60); const minutes = Math.floor((seconds - hours * 3600) / 60);
return ( return (
<span> <span>
{hours > 0 ? hours + 'h' : ''} {minutes > 0 ? zeroPad(minutes) + 'm' : ''}{' '} {hours > 0 ? `${hours}h` : ''} {minutes > 0 ? `${zeroPad(minutes)}m` : ''}{' '}
</span> </span>
); );
}; };
// Countdown Renderer callback with condition // Countdown Renderer callback with condition
const countdownPenaltyRenderer = function ({ minutes, seconds, completed }) { const countdownPenaltyRenderer = ({
minutes,
seconds,
completed,
}: {
minutes: number;
seconds: number;
completed: boolean;
}): JSX.Element => {
if (completed) { if (completed) {
// Render a completed state // Render a completed state
return <span> {t('Penalty lifted, good to go!')}</span>; return <span> {t('Penalty lifted, good to go!')}</span>;
@ -153,15 +167,20 @@ const OrderDetails = ({
let send: string = ''; let send: string = '';
let receive: string = ''; let receive: string = '';
let sats: string = ''; let sats: string = '';
const order = currentOrder.order;
const isBuyer = (order.type == 0 && order.is_maker) || (order.type == 1 && !order.is_maker); if (order === null) return {};
const tradeFee = order.is_maker ? info?.maker_fee ?? 0 : info?.taker_fee ?? 0;
const isBuyer = (order.type === 0 && order.is_maker) || (order.type === 1 && !order.is_maker);
const tradeFee = order.is_maker
? coordinator.info?.maker_fee ?? 0
: coordinator.info?.taker_fee ?? 0;
const defaultRoutingBudget = 0.001; const defaultRoutingBudget = 0.001;
const btc_now = order.satoshis_now / 100000000; const btc_now = order.satoshis_now / 100000000;
const rate = order.amount ? order.amount / btc_now : order.max_amount / btc_now; const rate = order.amount > 0 ? order.amount / btc_now : Number(order.max_amount) / btc_now;
if (isBuyer) { if (isBuyer) {
if (order.amount) { if (order.amount > 0) {
sats = computeSats({ sats = computeSats({
amount: order.amount, amount: order.amount,
fee: -tradeFee, fee: -tradeFee,
@ -181,7 +200,7 @@ const OrderDetails = ({
routingBudget: defaultRoutingBudget, routingBudget: defaultRoutingBudget,
rate, rate,
}); });
sats = `${min}-${max}`; sats = `${String(min)}-${String(max)}`;
} }
send = t('You send via {{method}} {{amount}}', { send = t('You send via {{method}} {{amount}}', {
amount: amountString, amount: amountString,
@ -192,7 +211,7 @@ const OrderDetails = ({
amount: sats, amount: sats,
}); });
} else { } else {
if (order.amount) { if (order.amount > 0) {
sats = computeSats({ sats = computeSats({
amount: order.amount, amount: order.amount,
fee: tradeFee, fee: tradeFee,
@ -209,7 +228,7 @@ const OrderDetails = ({
fee: tradeFee, fee: tradeFee,
rate, rate,
}); });
sats = `${min}-${max}`; sats = `${String(min)}-${String(max)}`;
} }
send = t('You send via Lightning {{amount}} Sats (Approx)', { amount: sats }); send = t('You send via Lightning {{amount}} Sats (Approx)', { amount: sats });
receive = t('You receive via {{method}} {{amount}}', { receive = t('You receive via {{method}} {{amount}}', {
@ -218,62 +237,93 @@ const OrderDetails = ({
}); });
} }
return { send, receive }; return { send, receive };
}, [order.currency, order.satoshis_now, order.amount, order.has_range]); }, [orderUpdatedAt]);
return ( return (
<Grid container spacing={0}> <Grid container spacing={0}>
<F2fMapDialog <F2fMapDialog
latitude={order.latitude} latitude={currentOrder.order?.latitude}
longitude={order.longitude} longitude={currentOrder.order?.longitude}
open={openWorldmap} open={openWorldmap}
orderType={order.type || 0} orderType={currentOrder.order?.type ?? 0}
zoom={6} zoom={6}
message={t( message={t(
'The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.', 'The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.',
)} )}
onClose={() => setOpenWorldmap(false)} onClose={() => {
setOpenWorldmap(false);
}}
/> />
<Grid item xs={12}> <Grid item xs={12}>
<List dense={true}> <List dense={true}>
<ListItemButton
onClick={() => {
onClickCoordinator();
}}
>
{' '}
<Grid container direction='row' justifyContent='center' alignItems='center'>
<Grid item xs={2}>
<RobotAvatar
nickname={coordinator.shortAlias}
coordinator={true}
baseUrl={hostUrl}
small={true}
/>
</Grid>
<Grid item xs={4}>
<ListItemText primary={coordinator.longAlias} secondary={t('Order host')} />
</Grid>
</Grid>
</ListItemButton>
<Divider />
<ListItem> <ListItem>
<ListItemAvatar sx={{ width: '4em', height: '4em' }}> <ListItemAvatar sx={{ width: '4em', height: '4em' }}>
<RobotAvatar <RobotAvatar
statusColor={statusBadgeColor(order.maker_status)} statusColor={statusBadgeColor(currentOrder.order?.maker_status ?? '')}
nickname={order.maker_nick} nickname={currentOrder.order?.maker_nick}
tooltip={t(order.maker_status)} tooltip={t(currentOrder.order?.maker_status ?? '')}
orderType={order.type} orderType={currentOrder.order?.type}
baseUrl={baseUrl} baseUrl={baseUrl}
small={true} small={true}
/> />
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={`${order.maker_nick} (${ primary={`${currentOrder.order?.maker_nick} (${
order.type currentOrder.order?.type === 1
? t(order.currency == 1000 ? 'Swapping Out' : 'Seller') ? t(currentOrder.order?.currency === 1000 ? 'Swapping Out' : 'Seller')
: t(order.currency == 1000 ? 'Swapping In' : 'Buyer') : t(currentOrder.order?.currency === 1000 ? 'Swapping In' : 'Buyer')
})`} })`}
secondary={t('Order maker')} secondary={t('Order maker')}
/> />
</ListItem> </ListItem>
<Collapse in={order.is_participant && order.taker_nick !== 'None'}> <Collapse
in={currentOrder.order?.is_participant && currentOrder.order?.taker_nick !== 'None'}
>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={`${order.taker_nick} (${ primary={`${currentOrder.order?.taker_nick} (${
order.type currentOrder.order?.type === 1
? t(order.currency == 1000 ? 'Swapping In' : 'Buyer') ? t(currentOrder.order?.currency === 1000 ? 'Swapping In' : 'Buyer')
: t(order.currency == 1000 ? 'Swapping Out' : 'Seller') : t(currentOrder.order?.currency === 1000 ? 'Swapping Out' : 'Seller')
})`} })`}
secondary={t('Order taker')} secondary={t('Order taker')}
/> />
<ListItemAvatar> <ListItemAvatar>
<RobotAvatar <RobotAvatar
avatarClass='smallAvatar' avatarClass='smallAvatar'
statusColor={statusBadgeColor(order.taker_status)} statusColor={statusBadgeColor(currentOrder.order?.taker_status ?? '')}
nickname={order.taker_nick == 'None' ? undefined : order.taker_nick} nickname={
tooltip={t(order.taker_status)} currentOrder.order?.taker_nick === 'None'
orderType={order.type === 0 ? 1 : 0} ? undefined
: currentOrder.order?.taker_nick
}
tooltip={t(currentOrder.order?.taker_status ?? '')}
orderType={currentOrder.order?.type === 0 ? 1 : 0}
baseUrl={baseUrl} baseUrl={baseUrl}
small={true} small={true}
/> />
@ -284,12 +334,15 @@ const OrderDetails = ({
<Chip label={t('Order Details')} /> <Chip label={t('Order Details')} />
</Divider> </Divider>
<Collapse in={order.is_participant}> <Collapse in={currentOrder.order?.is_participant}>
<ListItem> <ListItem>
<ListItemIcon> <ListItemIcon>
<Article /> <Article />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t(order.status_message)} secondary={t('Order status')} /> <ListItemText
primary={t(currentOrder.order?.status_message ?? '')}
secondary={t('Order status')}
/>
</ListItem> </ListItem>
<Divider /> <Divider />
</Collapse> </Collapse>
@ -311,7 +364,7 @@ const OrderDetails = ({
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={amountString} primary={amountString}
secondary={order.amount ? 'Amount' : 'Amount Range'} secondary={(currentOrder.order?.amount ?? 0) > 0 ? 'Amount' : 'Amount Range'}
/> />
<ListItemIcon> <ListItemIcon>
<IconButton <IconButton
@ -360,18 +413,24 @@ const OrderDetails = ({
size={1.42 * theme.typography.fontSize} size={1.42 * theme.typography.fontSize}
othersText={t('Others')} othersText={t('Others')}
verbose={true} verbose={true}
text={order.payment_method} text={currentOrder.order?.payment_method}
/> />
} }
secondary={ secondary={
order.currency == 1000 ? t('Swap destination') : t('Accepted payment methods') currentOrder.order?.currency === 1000
? t('Swap destination')
: t('Accepted payment methods')
} }
/> />
{order.payment_method.includes('Cash F2F') && ( {currentOrder.order?.payment_method.includes('Cash F2F') && (
<ListItemIcon> <ListItemIcon>
<Tooltip enterTouchDelay={0} title={t('F2F location')}> <Tooltip enterTouchDelay={0} title={t('F2F location')}>
<div> <div>
<IconButton onClick={() => setOpenWorldmap(true)}> <IconButton
onClick={() => {
setOpenWorldmap(true);
}}
>
<Map /> <Map />
</IconButton> </IconButton>
</div> </div>
@ -387,24 +446,27 @@ const OrderDetails = ({
<PriceChange /> <PriceChange />
</ListItemIcon> </ListItemIcon>
{order.price_now !== undefined ? ( {currentOrder.order?.price_now !== undefined ? (
<ListItemText <ListItemText
primary={t('{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%', { primary={t('{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%', {
price: pn(order.price_now), price: pn(currentOrder.order?.price_now),
currencyCode, currencyCode,
premium: order.premium_now, premium: currentOrder.order?.premium_now,
})} })}
secondary={t('Price and Premium')} secondary={t('Price and Premium')}
/> />
) : null} ) : null}
{!order.price_now && order.is_explicit ? ( {currentOrder.order?.price_now === undefined && currentOrder.order?.is_explicit ? (
<ListItemText primary={pn(order.satoshis)} secondary={t('Amount of Satoshis')} /> <ListItemText
primary={pn(currentOrder.order?.satoshis)}
secondary={t('Amount of Satoshis')}
/>
) : null} ) : null}
{!order.price_now && !order.is_explicit ? ( {currentOrder.order?.price_now === undefined && !currentOrder.order?.is_explicit ? (
<ListItemText <ListItemText
primary={parseFloat(Number(order.premium).toFixed(2)) + '%'} primary={`${parseFloat(Number(currentOrder.order?.premium).toFixed(2))}%`}
secondary={t('Premium over market price')} secondary={t('Premium over market price')}
/> />
) : null} ) : null}
@ -418,7 +480,7 @@ const OrderDetails = ({
</ListItemIcon> </ListItemIcon>
<Grid container> <Grid container>
<Grid item xs={4.5}> <Grid item xs={4.5}>
<ListItemText primary={order.id} secondary={t('Order ID')} /> <ListItemText primary={currentOrder.order?.id} secondary={t('Order ID')} />
</Grid> </Grid>
<Grid item xs={7.5}> <Grid item xs={7.5}>
<Grid container> <Grid container>
@ -429,7 +491,7 @@ const OrderDetails = ({
</Grid> </Grid>
<Grid item xs={10}> <Grid item xs={10}>
<ListItemText <ListItemText
primary={timerRenderer(order.escrow_duration)} primary={timerRenderer(currentOrder.order?.escrow_duration)}
secondary={t('Deposit timer')} secondary={t('Deposit timer')}
></ListItemText> ></ListItemText>
</Grid> </Grid>
@ -439,39 +501,47 @@ const OrderDetails = ({
</ListItem> </ListItem>
{/* if order is in a status that does not expire, do not show countdown */} {/* if order is in a status that does not expire, do not show countdown */}
<Collapse in={![4, 5, 12, 13, 14, 15, 16, 17, 18].includes(order.status)}> <Collapse
in={![4, 5, 12, 13, 14, 15, 16, 17, 18].includes(currentOrder.order?.status ?? 0)}
>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemIcon> <ListItemIcon>
<AccessTime /> <AccessTime />
</ListItemIcon> </ListItemIcon>
<ListItemText secondary={t('Expires in')}> <ListItemText secondary={t('Expires in')}>
<Countdown date={new Date(order.expires_at)} renderer={countdownRenderer} /> <Countdown
date={new Date(currentOrder.order?.expires_at ?? '')}
renderer={countdownRenderer}
/>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
<LinearDeterminate totalSecsExp={order.total_secs_exp} expiresAt={order.expires_at} /> <LinearDeterminate
totalSecsExp={currentOrder.order?.total_secs_exp ?? 0}
expiresAt={currentOrder.order?.expires_at ?? ''}
/>
</Collapse> </Collapse>
</List> </List>
{/* If the user has a penalty/limit */} {/* If the user has a penalty/limit */}
{order.penalty !== undefined ? ( {currentOrder.order?.penalty !== undefined ? (
<Grid item xs={12}> <Grid item xs={12}>
<Alert severity='warning' sx={{ borderRadius: '0' }}> <Alert severity='warning' sx={{ borderRadius: '0' }}>
<Countdown date={new Date(order.penalty)} renderer={countdownPenaltyRenderer} /> <Countdown
date={new Date(currentOrder.order?.penalty ?? '')}
renderer={countdownPenaltyRenderer}
/>
</Alert> </Alert>
</Grid> </Grid>
) : ( ) : (
<></> <></>
)} )}
{!order.is_participant ? ( {!currentOrder.order?.is_participant ? (
<Grid item xs={12}> <Grid item xs={12}>
<TakeButton <TakeButton
order={order}
setOrder={setOrder}
baseUrl={baseUrl} baseUrl={baseUrl}
hasRobot={hasRobot} info={coordinator.info}
info={info}
onClickGenerateRobot={onClickGenerateRobot} onClickGenerateRobot={onClickGenerateRobot}
/> />
</Grid> </Grid>

View File

@ -19,11 +19,11 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
const parsedText = useMemo(() => { const parsedText = useMemo(() => {
const rows = []; const rows = [];
let custom_methods = text; let customMethods = text;
// Adds icons for each PaymentMethod that matches // Adds icons for each PaymentMethod that matches
methods.forEach((method, i) => { methods.forEach((method, i) => {
if (text.includes(method.name)) { if (text.includes(method.name)) {
custom_methods = custom_methods.replace(method.name, ''); customMethods = customMethods.replace(method.name, '');
rows.push( rows.push(
<Tooltip <Tooltip
key={`${method.name}-${i}`} key={`${method.name}-${i}`}
@ -46,20 +46,20 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
}); });
// Adds a Custom icon if there are words that do not match // Adds a Custom icon if there are words that do not match
const chars_left = custom_methods const charsLeft = customMethods
.replace(' ', '') .replace(' ', '')
.replace(' ', '') .replace(' ', '')
.replace(' ', '') .replace(' ', '')
.replace(' ', '') .replace(' ', '')
.replace(' ', ''); .replace(' ', '');
if (chars_left.length > 0) { if (charsLeft.length > 0) {
rows.push( rows.push(
<Tooltip <Tooltip
key={'pushed'} key={'pushed'}
placement='top' placement='top'
enterTouchDelay={0} enterTouchDelay={0}
title={verbose ? othersText : othersText + ': ' + custom_methods} title={verbose ? othersText : othersText + ': ' + customMethods}
> >
<div <div
style={{ style={{
@ -82,7 +82,7 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
{rows}{' '} {rows}{' '}
<div style={{ display: 'inline-block' }}> <div style={{ display: 'inline-block' }}>
{' '} {' '}
<span>{custom_methods}</span> <span>{customMethods}</span>
</div> </div>
</> </>
); );

View File

@ -1,13 +1,16 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import SmoothImage from 'react-smooth-image'; import SmoothImage from 'react-smooth-image';
import { Avatar, Badge, Tooltip, useTheme } from '@mui/material'; import { Avatar, Badge, Tooltip } from '@mui/material';
import { SendReceiveIcon } from '../Icons'; import { SendReceiveIcon } from '../Icons';
import { apiClient } from '../../services/api'; import { apiClient } from '../../services/api';
import placeholder from './placeholder.json'; import placeholder from './placeholder.json';
import { UseAppStoreType, AppContext } from '../../contexts/AppContext';
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
interface Props { interface Props {
nickname: string | undefined; nickname: string | undefined;
smooth?: boolean; smooth?: boolean;
coordinator?: boolean;
small?: boolean; small?: boolean;
flipHorizontally?: boolean; flipHorizontally?: boolean;
style?: object; style?: object;
@ -41,27 +44,35 @@ const RobotAvatar: React.FC<Props> = ({
avatarClass = 'flippedSmallAvatar', avatarClass = 'flippedSmallAvatar',
imageStyle = {}, imageStyle = {},
onLoad = () => {}, onLoad = () => {},
coordinator = false,
baseUrl, baseUrl,
}) => { }) => {
const { settings, origin, hostUrl } = useContext<UseAppStoreType>(AppContext);
const { federation, focusedCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const [avatarSrc, setAvatarSrc] = useState<string>(); const [avatarSrc, setAvatarSrc] = useState<string>();
const [nicknameReady, setNicknameReady] = useState<boolean>(false); const [nicknameReady, setNicknameReady] = useState<boolean>(false);
const [activeBackground, setActiveBackground] = useState<boolean>(true); const [activeBackground, setActiveBackground] = useState<boolean>(true);
const path = coordinator ? '/static/federation/' : '/static/assets/avatars/';
const [backgroundData] = useState<BackgroundData>( const [backgroundData] = useState<BackgroundData>(
placeholderType == 'generating' ? placeholder.generating : placeholder.loading, placeholderType === 'generating' ? placeholder.generating : placeholder.loading,
); );
const backgroundImage = `url(data:${backgroundData.mime};base64,${backgroundData.data})`; const backgroundImage = `url(data:${backgroundData.mime};base64,${backgroundData.data})`;
const className = placeholderType == 'loading' ? 'loadingAvatar' : 'generatingAvatar'; const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar';
useEffect(() => { useEffect(() => {
if (nickname != undefined) { if (nickname !== undefined) {
if (window.NativeRobosats === undefined) { if (window.NativeRobosats === undefined) {
setAvatarSrc(`${baseUrl}/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`); setAvatarSrc(`${baseUrl}${path}${nickname}${small ? '.small' : ''}.webp`);
setNicknameReady(true); setNicknameReady(true);
} else { } else if (focusedCoordinator) {
setNicknameReady(true); setNicknameReady(true);
apiClient const { url } = federation
.fileImageUrl(baseUrl, `/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`) .getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
void apiClient
.fileImageUrl(url, `${path}${nickname}${small ? '.small' : ''}.webp`)
.then(setAvatarSrc); .then(setAvatarSrc);
} }
} else { } else {
@ -133,7 +144,7 @@ const RobotAvatar: React.FC<Props> = ({
const getAvatarWithBadges = useCallback(() => { const getAvatarWithBadges = useCallback(() => {
let component = avatar; let component = avatar;
if (statusColor) { if (statusColor !== undefined) {
component = ( component = (
<Badge variant='dot' overlap='circular' badgeContent='' color={statusColor}> <Badge variant='dot' overlap='circular' badgeContent='' color={statusColor}>
{component} {component}
@ -153,7 +164,7 @@ const RobotAvatar: React.FC<Props> = ({
); );
} }
if (tooltip) { if (tooltip !== undefined) {
component = ( component = (
<Tooltip placement={tooltipPosition} enterTouchDelay={0} title={tooltip}> <Tooltip placement={tooltipPosition} enterTouchDelay={0} title={tooltip}>
{component} {component}

View File

@ -0,0 +1,337 @@
import React, { useContext, useEffect, useMemo, useState } from 'react';
import {
Tooltip,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Grid,
useTheme,
Divider,
Typography,
Badge,
Button,
Switch,
FormControlLabel,
TextField,
CircularProgress,
Accordion,
AccordionDetails,
AccordionSummary,
} from '@mui/material';
import { Numbers, Send, EmojiEvents, ExpandMore } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { type Coordinator, type Robot } from '../../models';
import { useTranslation } from 'react-i18next';
import { EnableTelegramDialog } from '../Dialogs';
import { UserNinjaIcon } from '../Icons';
import { getWebln } from '../../utils';
import { signCleartextMessage } from '../../pgp';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
interface Props {
robot: Robot;
slotIndex: number;
coordinator: Coordinator;
onClose: () => void;
}
const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }: Props) => {
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
const [rewardInvoice, setRewardInvoice] = useState<string>('');
const [showRewardsSpinner, setShowRewardsSpinner] = useState<boolean>(false);
const [withdrawn, setWithdrawn] = useState<boolean>(false);
const [badInvoice, setBadInvoice] = useState<string>('');
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
const handleWebln = async (): Promise<void> => {
void getWebln()
.then(() => {
setWeblnEnabled(true);
})
.catch(() => {
setWeblnEnabled(false);
console.log('WebLN not available');
});
};
useEffect(() => {
handleWebln();
}, []);
const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
e.preventDefault();
if (robot.earnedRewards > 0) {
const webln = await getWebln();
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
if (invoice != null) {
handleSubmitInvoiceClicked(e, invoice.paymentRequest);
}
});
}
};
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string): void => {
setBadInvoice('');
setShowRewardsSpinner(true);
const robot = garage.getRobot(slotIndex);
if (robot.encPrivKey && robot.token) {
void signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then(
(signedInvoice) => {
coordinator.fetchReward(signedInvoice, garage, slotIndex).then((data) => {
setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal);
setOpenClaimRewards(!(data.successful_withdrawal !== undefined));
});
},
);
}
e.preventDefault();
};
const setStealthInvoice = (wantsStealth: boolean): void => {
coordinator.fetchStealth(wantsStealth, garage, slotIndex);
};
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>
{`${coordinator.longAlias}:`}
{garage.getRobot(slotIndex).earnedRewards > 0 && (
<Typography color='success'>&nbsp;{t('Claim Sats!')} </Typography>
)}
{(garage.getRobot(slotIndex).activeOrderId ?? 0) > 0 && (
<Typography color='success'>
&nbsp;<b>{t('Active order!')}</b>
</Typography>
)}
{(garage.getRobot(slotIndex).lastOrderId ?? 0) > 0 && robot.activeOrderId === undefined && (
<Typography color='warning'>&nbsp;{t('finished order')}</Typography>
)}
</AccordionSummary>
<AccordionDetails>
<List dense disablePadding={true}>
{(garage.getRobot(slotIndex).activeOrderId ?? 0) > 0 ? (
<ListItemButton
onClick={() => {
navigate(`/order/${coordinator.shortAlias}/${String(robot.activeOrderId)}`);
onClose();
}}
>
<ListItemIcon>
<Badge badgeContent='' color='primary'>
<Numbers color='primary' />
</Badge>
</ListItemIcon>
<ListItemText
primary={t('One active order #{{orderID}}', { orderID: robot.activeOrderId })}
secondary={t('Your current order')}
/>
</ListItemButton>
) : (garage.getRobot(slotIndex).lastOrderId ?? 0) > 0 ? (
<ListItemButton
onClick={() => {
navigate(`/order/${coordinator.shortAlias}/${String(robot.lastOrderId)}`);
onClose();
}}
>
<ListItemIcon>
<Numbers color='primary' />
</ListItemIcon>
<ListItemText
primary={t('Your last order #{{orderID}}', { orderID: robot.lastOrderId })}
secondary={t('Inactive order')}
/>
</ListItemButton>
) : (
<ListItem>
<ListItemIcon>
<Numbers />
</ListItemIcon>
<ListItemText
primary={t('No active orders')}
secondary={t('You do not have previous orders')}
/>
</ListItem>
)}
<Divider />
<EnableTelegramDialog
open={openEnableTelegram}
onClose={() => {
setOpenEnableTelegram(false);
}}
tgBotName={robot.tgBotName}
tgToken={robot.tgToken}
/>
<ListItem>
<ListItemIcon>
<Send />
</ListItemIcon>
<ListItemText>
{robot.tgEnabled ? (
<Typography color={theme.palette.success.main}>
<b>{t('Telegram enabled')}</b>
</Typography>
) : (
<Button
color='primary'
onClick={() => {
setOpenEnableTelegram(true);
}}
>
{t('Enable Telegram Notifications')}
</Button>
)}
</ListItemText>
</ListItem>
<ListItem>
<ListItemIcon>
<UserNinjaIcon />
</ListItemIcon>
<ListItemText>
<Tooltip
placement='bottom'
enterTouchDelay={0}
title={t(
"Stealth lightning invoices do not contain details about the trade except an order reference. Enable this setting if you don't want to disclose details to a custodial lightning wallet.",
)}
>
<Grid item>
<FormControlLabel
labelPlacement='end'
label={t('Use stealth invoices')}
control={
<Switch
checked={robot.stealthInvoices}
onChange={() => {
setStealthInvoice(!robot.stealthInvoices);
}}
/>
}
/>
</Grid>
</Tooltip>
</ListItemText>
</ListItem>
<ListItem>
<ListItemIcon>
<EmojiEvents />
</ListItemIcon>
{!openClaimRewards ? (
<ListItemText secondary={t('Your compensations')}>
<Grid container>
<Grid item xs={9}>
<Typography>{`${robot.earnedRewards} Sats`}</Typography>
</Grid>
<Grid item xs={3}>
<Button
disabled={robot.earnedRewards === 0}
onClick={() => {
setOpenClaimRewards(true);
}}
variant='contained'
size='small'
>
{t('Claim')}
</Button>
</Grid>
</Grid>
</ListItemText>
) : (
<form noValidate style={{ maxWidth: 270 }}>
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
<Grid item style={{ display: 'flex', maxWidth: 160 }}>
<TextField
error={Boolean(badInvoice)}
helperText={badInvoice ?? ''}
label={t('Invoice for {{amountSats}} Sats', {
amountSats: robot.earnedRewards,
})}
size='small'
value={rewardInvoice}
onChange={(e) => {
setRewardInvoice(e.target.value);
}}
/>
</Grid>
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 80 }}>
<Button
sx={{ maxHeight: 38 }}
onClick={(e) => {
handleSubmitInvoiceClicked(e, rewardInvoice);
}}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Submit')}
</Button>
</Grid>
</Grid>
{weblnEnabled ? (
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 240 }}>
<Button
sx={{ maxHeight: 38, minWidth: 230 }}
onClick={(e) => {
handleWeblnInvoiceClicked(e);
}}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Generate with Webln')}
</Button>
</Grid>
</Grid>
) : (
<></>
)}
</form>
)}
</ListItem>
{showRewardsSpinner && (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</div>
)}
{withdrawn && (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Typography color='primary' variant='body2'>
<b>{t('There it goes!')}</b>
</Typography>
</div>
)}
</List>
</AccordionDetails>
</Accordion>
);
};
export default RobotInfo;

View File

@ -1,10 +1,17 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Select, MenuItem, useTheme, Grid, Typography } from '@mui/material'; import {
import type Language from '../../models/Language.model'; Select,
MenuItem,
useTheme,
Grid,
Typography,
type SelectChangeEvent,
} from '@mui/material';
import Flags from 'country-flag-icons/react/3x2'; import Flags from 'country-flag-icons/react/3x2';
import { CataloniaFlag, BasqueCountryFlag } from '../Icons'; import { CataloniaFlag, BasqueCountryFlag } from '../Icons';
import type { Language } from '../../models';
const menuLanuguages = [ const menuLanuguages = [
{ name: 'English', i18nCode: 'en', flag: Flags.US }, { name: 'English', i18nCode: 'en', flag: Flags.US },
@ -31,18 +38,18 @@ interface SelectLanguageProps {
setLanguage: (lang: Language) => void; setLanguage: (lang: Language) => void;
} }
const SelectLanguage = ({ language, setLanguage }: SelectLanguageProps): JSX.Element => { const SelectLanguage: React.FC<SelectLanguageProps> = ({ language, setLanguage }) => {
const theme = useTheme(); const theme = useTheme();
const { t, i18n } = useTranslation(); const { i18n } = useTranslation();
const flagProps = { const flagProps = {
width: 1.5 * theme.typography.fontSize, width: 1.5 * theme.typography.fontSize,
height: 1.5 * theme.typography.fontSize, height: 1.5 * theme.typography.fontSize,
}; };
const handleChangeLang = function (e: any) { const handleChangeLang = function (e: SelectChangeEvent): void {
setLanguage(e.target.value); setLanguage(e.target.value);
i18n.changeLanguage(e.target.value); void i18n.changeLanguage(e.target.value);
}; };
return ( return (

View File

@ -32,10 +32,9 @@ import SwapCalls from '@mui/icons-material/SwapCalls';
interface SettingsFormProps { interface SettingsFormProps {
dense?: boolean; dense?: boolean;
showNetwork?: boolean;
} }
const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps): JSX.Element => { const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
const { fav, setFav, settings, setSettings } = useContext<UseAppStoreType>(AppContext); const { fav, setFav, settings, setSettings } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@ -176,8 +175,8 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
</ListItemIcon> </ListItemIcon>
<Slider <Slider
value={settings.fontSize} value={settings.fontSize}
min={settings.frontend == 'basic' ? 12 : 10} min={settings.frontend === 'basic' ? 12 : 10}
max={settings.frontend == 'basic' ? 16 : 14} max={settings.frontend === 'basic' ? 16 : 14}
step={1} step={1}
onChange={(e) => { onChange={(e) => {
const fontSize = e.target.value; const fontSize = e.target.value;
@ -215,30 +214,26 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
</ToggleButtonGroup> </ToggleButtonGroup>
</ListItem> </ListItem>
{showNetwork ? ( <ListItem>
<ListItem> <ListItemIcon>
<ListItemIcon> <Link />
<Link /> </ListItemIcon>
</ListItemIcon> <ToggleButtonGroup
<ToggleButtonGroup exclusive={true}
exclusive={true} value={settings.network}
value={settings.network} onChange={(e, network) => {
onChange={(e, network) => { setSettings({ ...settings, network });
setSettings({ ...settings, network }); systemClient.setItem('settings_network', network);
systemClient.setItem('settings_network', network); }}
}} >
> <ToggleButton value='mainnet' color='primary'>
<ToggleButton value='mainnet' color='primary'> {t('Mainnet')}
{t('Mainnet')} </ToggleButton>
</ToggleButton> <ToggleButton value='testnet' color='secondary'>
<ToggleButton value='testnet' color='secondary'> {t('Testnet')}
{t('Testnet')} </ToggleButton>
</ToggleButton> </ToggleButtonGroup>
</ToggleButtonGroup> </ListItem>
</ListItem>
) : (
<></>
)}
</List> </List>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -0,0 +1,97 @@
import React, { useContext } from 'react';
import { Box, CircularProgress, Tooltip } from '@mui/material';
import { TorIcon } from '../Icons';
import { useTranslation } from 'react-i18next';
import { AppContext, type AppContextProps } from '../contexts/AppContext';
interface TorIndicatorProps {
color: 'inherit' | 'error' | 'warning' | 'success' | 'primary' | 'secondary' | 'info' | undefined;
tooltipOpen?: boolean | undefined;
title: string;
progress: boolean;
}
const TorIndicator = ({
color,
tooltipOpen = undefined,
title,
progress,
}: TorIndicatorProps): JSX.Element => {
return (
<Tooltip
open={tooltipOpen}
enterTouchDelay={0}
enterDelay={1000}
placement='right'
title={title}
>
<Box sx={{ display: 'inline-flex', position: 'fixed', left: '0.5em', top: '0.5em' }}>
{progress ? (
<>
<CircularProgress color={color} thickness={6} size={22} />
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<TorIcon color={color} sx={{ width: 20, height: 20 }} />
</Box>
</>
) : (
<Box>
<TorIcon color={color} sx={{ width: 20, height: 20 }} />
</Box>
)}
</Box>
</Tooltip>
);
};
const TorConnectionBadge = (): JSX.Element => {
const { torStatus } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
if (window?.NativeRobosats == null) {
return <></>;
}
if (torStatus === 'NOTINIT') {
return (
<TorIndicator
color='primary'
progress={true}
tooltipOpen={true}
title={t('Initializing TOR daemon')}
/>
);
} else if (torStatus === 'STARTING') {
return (
<TorIndicator
color='warning'
progress={true}
tooltipOpen={true}
title={t('Connecting to TOR network')}
/>
);
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
} else {
return (
<TorIndicator
color='error'
progress={false}
tooltipOpen={true}
title={t('Connection error')}
/>
);
}
};
export default TorConnectionBadge;

View File

@ -16,10 +16,10 @@ const BondStatus = ({ status, isMaker }: BondStatusProps): JSX.Element => {
let color = 'primary'; let color = 'primary';
if (status === 'unlocked') { if (status === 'unlocked') {
Icon = LockOpen; Icon = LockOpen;
color = theme.palette.mode == 'dark' ? 'lightgreen' : 'green'; color = theme.palette.mode === 'dark' ? 'lightgreen' : 'green';
} else if (status === 'settled') { } else if (status === 'settled') {
Icon = Balance; Icon = Balance;
color = theme.palette.mode == 'dark' ? 'lightred' : 'red'; color = theme.palette.mode === 'dark' ? 'lightred' : 'red';
} }
if (status === 'hide') { if (status === 'hide') {

View File

@ -5,7 +5,7 @@ import { type Order } from '../../models';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
interface CancelButtonProps { interface CancelButtonProps {
order: Order; order: Order | null;
onClickCancel: () => void; onClickCancel: () => void;
openCancelDialog: () => void; openCancelDialog: () => void;
openCollabCancelDialog: () => void; openCollabCancelDialog: () => void;
@ -22,10 +22,12 @@ const CancelButton = ({
const { t } = useTranslation(); const { t } = useTranslation();
const showCancelButton = const showCancelButton =
(order.is_maker && [0, 1, 2].includes(order.status)) || [3, 6, 7].includes(order.status); (order?.is_maker && [0, 1, 2].includes(order?.status)) ||
const showCollabCancelButton = [8, 9].includes(order.status) && !order.asked_for_cancel; [3, 6, 7].includes(order?.status ?? -1);
const showCollabCancelButton = [8, 9].includes(order?.status ?? -1) && !order?.asked_for_cancel;
const noConfirmation = const noConfirmation =
(order.is_maker && [0, 1, 2].includes(order.status)) || (order.is_taker && order.status === 3); (order?.is_maker && [0, 1, 2].includes(order?.status ?? -1)) ||
(order?.is_taker && order?.status === 3);
return ( return (
<Box> <Box>

View File

@ -4,21 +4,21 @@ import { Alert } from '@mui/material';
import { type Order } from '../../models'; import { type Order } from '../../models';
interface CollabCancelAlertProps { interface CollabCancelAlertProps {
order: Order; order: Order | null;
} }
const CollabCancelAlert = ({ order }: CollabCancelAlertProps): JSX.Element => { const CollabCancelAlert = ({ order }: CollabCancelAlertProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
let text = ''; let text = '';
if (order.pending_cancel) { if (order?.pending_cancel) {
text = t('{{nickname}} is asking for a collaborative cancel', { text = t('{{nickname}} is asking for a collaborative cancel', {
nickname: order.is_maker ? order.taker_nick : order.maker_nick, nickname: order?.is_maker ? order?.taker_nick : order?.maker_nick,
}); });
} else if (order.asked_for_cancel) { } else if (order?.asked_for_cancel) {
text = t('You asked for a collaborative cancellation'); text = t('You asked for a collaborative cancellation');
} }
return text != '' ? ( return text !== '' ? (
<Alert severity='warning' style={{ width: '100%' }}> <Alert severity='warning' style={{ width: '100%' }}>
{text} {text}
</Alert> </Alert>

View File

@ -16,7 +16,7 @@ import { LoadingButton } from '@mui/lab';
interface ConfirmFiatReceivedDialogProps { interface ConfirmFiatReceivedDialogProps {
open: boolean; open: boolean;
loadingButton: boolean; loadingButton: boolean;
order: Order; order: Order | null;
onClose: () => void; onClose: () => void;
onConfirmClick: () => void; onConfirmClick: () => void;
} }
@ -29,8 +29,10 @@ export const ConfirmFiatReceivedDialog = ({
onConfirmClick, onConfirmClick,
}: ConfirmFiatReceivedDialogProps): JSX.Element => { }: ConfirmFiatReceivedDialogProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const currencyCode = currencies[order.currency.toString()]; const currencyCode = currencies[order?.currency.toString()];
const amount = pn(parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4))); const amount = pn(
parseFloat(parseFloat(order?.amount).toFixed(order?.currency === 1000 ? 8 : 4)),
);
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>

View File

@ -16,7 +16,7 @@ import { LoadingButton } from '@mui/lab';
interface ConfirmFiatSentDialogProps { interface ConfirmFiatSentDialogProps {
open: boolean; open: boolean;
loadingButton: boolean; loadingButton: boolean;
order: Order; order: Order | null;
onClose: () => void; onClose: () => void;
onConfirmClick: () => void; onConfirmClick: () => void;
} }
@ -29,8 +29,10 @@ export const ConfirmFiatSentDialog = ({
onConfirmClick, onConfirmClick,
}: ConfirmFiatSentDialogProps): JSX.Element => { }: ConfirmFiatSentDialogProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const currencyCode = currencies[order.currency.toString()]; const currencyCode = currencies[order?.currency.toString()];
const amount = pn(parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4))); const amount = pn(
parseFloat(parseFloat(order?.amount).toFixed(order?.currency === 1000 ? 8 : 4)),
);
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>

View File

@ -14,7 +14,6 @@ import { LoadingButton } from '@mui/lab';
interface ConfirmUndoFiatSentDialogProps { interface ConfirmUndoFiatSentDialogProps {
open: boolean; open: boolean;
loadingButton: boolean; loadingButton: boolean;
order: Order;
onClose: () => void; onClose: () => void;
onConfirmClick: () => void; onConfirmClick: () => void;
} }
@ -36,7 +35,7 @@ export const ConfirmUndoFiatSentDialog = ({
} }
}, [time, open]); }, [time, open]);
const onClick = () => { const onClick = (): void => {
onConfirmClick(); onConfirmClick();
setTime(300); setTime(300);
}; };

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React from 'react';
import { Grid, useTheme, Tooltip, Button } from '@mui/material'; import { Grid, Tooltip, Button } from '@mui/material';
import { ExportIcon } from '../../../Icons'; import { ExportIcon } from '../../../Icons';
import KeyIcon from '@mui/icons-material/Key'; import KeyIcon from '@mui/icons-material/Key';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -15,7 +15,6 @@ interface Props {
const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile }) => { const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme();
return ( return (
<Grid <Grid
@ -62,7 +61,7 @@ const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile
color='primary' color='primary'
variant='outlined' variant='outlined'
onClick={() => { onClick={() => {
saveAsJson('complete_log_chat_' + orderId + '.json', createJsonFile()); saveAsJson(`complete_log_chat_${orderId}.json`, createJsonFile());
}} }}
> >
<div style={{ width: '1.4em', height: '1.4em' }}> <div style={{ width: '1.4em', height: '1.4em' }}>

View File

@ -1,6 +1,6 @@
import React, { useEffect, useLayoutEffect, useState } from 'react'; import React, { useEffect, useLayoutEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Tooltip, TextField, Grid, Paper, Typography } from '@mui/material'; import { Button, TextField, Grid, Paper, Typography } from '@mui/material';
import { encryptMessage, decryptMessage } from '../../../../pgp'; import { encryptMessage, decryptMessage } from '../../../../pgp';
import { AuditPGPDialog } from '../../../Dialogs'; import { AuditPGPDialog } from '../../../Dialogs';
import { websocketClient, type WebsocketConnection } from '../../../../services/Websocket'; import { websocketClient, type WebsocketConnection } from '../../../../services/Websocket';
@ -48,6 +48,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
}: Props): JSX.Element => { }: Props): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`)); const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`));
const [connected, setConnected] = useState<boolean>(false); const [connected, setConnected] = useState<boolean>(false);
@ -64,7 +65,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
useEffect(() => { useEffect(() => {
if (!connected && robot.avatarLoaded) { if (!connected && garage.getRobot().avatarLoaded) {
connectWebsocket(); connectWebsocket();
} }
}, [connected, robot]); }, [connected, robot]);
@ -87,18 +88,18 @@ const EncryptedSocketChat: React.FC<Props> = ({
useEffect(() => { useEffect(() => {
if (messages.length > messageCount) { if (messages.length > messageCount) {
audio.play(); void audio.play();
setMessageCount(messages.length); setMessageCount(messages.length);
} }
}, [messages, messageCount]); }, [messages, messageCount]);
useEffect(() => { useEffect(() => {
if (serverMessages) { if (serverMessages.length > 0) {
serverMessages.forEach(onMessage); serverMessages.forEach(onMessage);
} }
}, [serverMessages]); }, [serverMessages]);
const connectWebsocket = () => { const connectWebsocket = (): void => {
websocketClient websocketClient
.open( .open(
`ws://${window.location.host}/ws/chat/${orderId}/?token_sha256_hex=${sha256(robot.token)}`, `ws://${window.location.host}/ws/chat/${orderId}/?token_sha256_hex=${sha256(robot.token)}`,
@ -121,6 +122,9 @@ const EncryptedSocketChat: React.FC<Props> = ({
connection.onError(() => { connection.onError(() => {
setConnected(false); setConnected(false);
}); });
})
.catch(() => {
setConnected(false);
}); });
}; };
@ -139,14 +143,14 @@ const EncryptedSocketChat: React.FC<Props> = ({
const onMessage: (message: any) => void = (message) => { const onMessage: (message: any) => void = (message) => {
const dataFromServer = JSON.parse(message.data); const dataFromServer = JSON.parse(message.data);
if (dataFromServer && !receivedIndexes.includes(dataFromServer.index)) { if (dataFromServer != null && !receivedIndexes.includes(dataFromServer.index)) {
setReceivedIndexes((prev) => [...prev, dataFromServer.index]); setReceivedIndexes((prev) => [...prev, dataFromServer.index]);
setPeerConnected(dataFromServer.peer_connected); setPeerConnected(dataFromServer.peer_connected);
// If we receive a public key other than ours (our peer key!) // If we receive a public key other than ours (our peer key!)
if ( if (
connection != null && connection != null &&
dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` && dataFromServer.message.substring(0, 36) === `-----BEGIN PGP PUBLIC KEY BLOCK-----` &&
dataFromServer.message != robot.pubKey dataFromServer.message !== robot.pubKey
) { ) {
setPeerPubKey(dataFromServer.message); setPeerPubKey(dataFromServer.message);
connection.send({ connection.send({
@ -155,10 +159,10 @@ const EncryptedSocketChat: React.FC<Props> = ({
}); });
} }
// If we receive an encrypted message // If we receive an encrypted message
else if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) { else if (dataFromServer.message.substring(0, 27) === `-----BEGIN PGP MESSAGE-----`) {
decryptMessage( void decryptMessage(
dataFromServer.message.split('\\').join('\n'), dataFromServer.message.split('\\').join('\n'),
dataFromServer.user_nick == userNick ? robot.pubKey : peerPubKey, dataFromServer.user_nick === userNick ? robot.pubKey : peerPubKey,
robot.encPrivKey, robot.encPrivKey,
robot.token, robot.token,
).then((decryptedData) => { ).then((decryptedData) => {
@ -166,27 +170,25 @@ const EncryptedSocketChat: React.FC<Props> = ({
setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent); setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent);
setMessages((prev) => { setMessages((prev) => {
const existingMessage = prev.find((item) => item.index === dataFromServer.index); const existingMessage = prev.find((item) => item.index === dataFromServer.index);
if (existingMessage) { if (existingMessage != null) {
return prev; return prev;
} else { } else {
return [ const x: EncryptedChatMessage = {
...prev, index: dataFromServer.index,
{ encryptedMessage: dataFromServer.message.split('\\').join('\n'),
index: dataFromServer.index, plainTextMessage: String(decryptedData.decryptedMessage),
encryptedMessage: dataFromServer.message.split('\\').join('\n'), validSignature: decryptedData.validSignature,
plainTextMessage: decryptedData.decryptedMessage, userNick: dataFromServer.user_nick,
validSignature: decryptedData.validSignature, time: dataFromServer.time,
userNick: dataFromServer.user_nick, };
time: dataFromServer.time, return [...prev, x].sort((a, b) => a.index - b.index);
} as EncryptedChatMessage,
].sort((a, b) => a.index - b.index);
} }
}); });
}); });
} }
// We allow plaintext communication. The user must write # to start // We allow plaintext communication. The user must write # to start
// If we receive an plaintext message // If we receive an plaintext message
else if (dataFromServer.message.substring(0, 1) == '#') { else if (dataFromServer.message.substring(0, 1) === '#') {
setMessages((prev: EncryptedChatMessage[]) => { setMessages((prev: EncryptedChatMessage[]) => {
const existingMessage = prev.find( const existingMessage = prev.find(
(item) => item.plainTextMessage === dataFromServer.message, (item) => item.plainTextMessage === dataFromServer.message,
@ -194,32 +196,30 @@ const EncryptedSocketChat: React.FC<Props> = ({
if (existingMessage != null) { if (existingMessage != null) {
return prev; return prev;
} else { } else {
return [ const x: EncryptedChatMessage = {
...prev, index: prev.length + 0.001,
{ encryptedMessage: dataFromServer.message,
index: prev.length + 0.001, plainTextMessage: dataFromServer.message,
encryptedMessage: dataFromServer.message, validSignature: false,
plainTextMessage: dataFromServer.message, userNick: dataFromServer.user_nick,
validSignature: false, time: new Date().toString(),
userNick: dataFromServer.user_nick, };
time: new Date().toString(), return [...prev, x].sort((a, b) => a.index - b.index);
} as EncryptedChatMessage,
].sort((a, b) => a.index - b.index);
} }
}); });
} }
} }
}; };
const onButtonClicked = (e: any) => { const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => {
if (robot.token && value.includes(robot.token)) { if (robot.token !== undefined && value.includes(robot.token)) {
alert( alert(
`Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, `Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
); );
setValue(''); setValue('');
} }
// If input string contains '#' send unencrypted and unlogged message // If input string contains '#' send unencrypted and unlogged message
else if (connection != null && value.substring(0, 1) == '#') { else if (connection != null && value.substring(0, 1) === '#') {
connection.send({ connection.send({
message: value, message: value,
nick: userNick, nick: userNick,
@ -228,7 +228,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
} }
// Else if message is not empty send message // Else if message is not empty send message
else if (value != '') { else if (value !== '') {
setValue(''); setValue('');
setWaitingEcho(true); setWaitingEcho(true);
setLastSent(value); setLastSent(value);
@ -236,7 +236,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
.then((encryptedMessage) => { .then((encryptedMessage) => {
if (connection != null) { if (connection != null) {
connection.send({ connection.send({
message: encryptedMessage.toString().split('\n').join('\\'), message: String(encryptedMessage).split('\n').join('\\'),
nick: userNick, nick: userNick,
}); });
} }
@ -263,17 +263,17 @@ const EncryptedSocketChat: React.FC<Props> = ({
}} }}
orderId={Number(orderId)} orderId={Number(orderId)}
messages={messages} messages={messages}
own_pub_key={robot.pubKey || ''} ownPubKey={robot.pubKey ?? ''}
own_enc_priv_key={robot.encPrivKey || ''} ownEncPrivKey={robot.encPrivKey ?? ''}
peer_pub_key={peerPubKey || 'Not received yet'} peerPubKey={peerPubKey ?? 'Not received yet'}
passphrase={robot.token || ''} passphrase={robot.token ?? ''}
onClickBack={() => { onClickBack={() => {
setAudit(false); setAudit(false);
}} }}
/> />
<Grid item> <Grid item>
<ChatHeader <ChatHeader
connected={connected && (peerPubKey ? true : false)} connected={connected && Boolean(peerPubKey)}
peerConnected={peerConnected} peerConnected={peerConnected}
turtleMode={turtleMode} turtleMode={turtleMode}
setTurtleMode={setTurtleMode} setTurtleMode={setTurtleMode}
@ -326,7 +326,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
}} }}
helperText={ helperText={
connected connected
? peerPubKey ? peerPubKey !== undefined
? null ? null
: t('Waiting for peer public key...') : t('Waiting for peer public key...')
: t('Connecting...') : t('Connecting...')
@ -341,7 +341,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}> <Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}>
<Button <Button
fullWidth={true} fullWidth={true}
disabled={!connected || waitingEcho || !peerPubKey} disabled={!connected || waitingEcho || peerPubKey === undefined}
type='submit' type='submit'
variant='contained' variant='contained'
color='primary' color='primary'

View File

@ -47,6 +47,8 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}: Props): JSX.Element => { }: Props): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const { origin, hostUrl, settings } = useContext<UseAppStoreType>(AppContext);
const { federation, focusedCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`)); const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`));
const [peerConnected, setPeerConnected] = useState<boolean>(false); const [peerConnected, setPeerConnected] = useState<boolean>(false);
@ -62,13 +64,13 @@ const EncryptedTurtleChat: React.FC<Props> = ({
useEffect(() => { useEffect(() => {
if (messages.length > messageCount) { if (messages.length > messageCount) {
audio.play(); void audio.play();
setMessageCount(messages.length); setMessageCount(messages.length);
} }
}, [messages, messageCount]); }, [messages, messageCount]);
useEffect(() => { useEffect(() => {
if (serverMessages.length > 0 && peerPubKey) { if (serverMessages.length > 0 && peerPubKey !== undefined) {
serverMessages.forEach(onMessage); serverMessages.forEach(onMessage);
} }
}, [serverMessages, peerPubKey]); }, [serverMessages, peerPubKey]);
@ -80,20 +82,26 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}, [chatOffset]); }, [chatOffset]);
const loadMessages: () => void = () => { const loadMessages: () => void = () => {
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.get(baseUrl, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, { .get(url, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, {
tokenSHA256: robot.tokenSHA256, tokenSHA256: robot.tokenSHA256,
}) })
.then((results: any) => { .then((results: any) => {
if (results) { if (results != null) {
setPeerConnected(results.peer_connected); setPeerConnected(results.peer_connected);
setPeerPubKey(results.peer_pubkey.split('\\').join('\n')); setPeerPubKey(results.peer_pubkey.split('\\').join('\n'));
setServerMessages(results.messages); setServerMessages(results.messages);
} }
})
.catch((error) => {
setError(error.toString());
}); });
}; };
const createJsonFile: () => object = () => { const createJsonFile = (): object => {
return { return {
credentials: { credentials: {
own_public_key: robot.pubKey, own_public_key: robot.pubKey,
@ -105,13 +113,13 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}; };
}; };
const onMessage: (dataFromServer: ServerMessage) => void = (dataFromServer) => { const onMessage = (dataFromServer: ServerMessage): void => {
if (dataFromServer) { if (dataFromServer != null) {
// If we receive an encrypted message // If we receive an encrypted message
if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) { if (dataFromServer.message.substring(0, 27) === `-----BEGIN PGP MESSAGE-----`) {
decryptMessage( void decryptMessage(
dataFromServer.message.split('\\').join('\n'), dataFromServer.message.split('\\').join('\n'),
dataFromServer.nick == userNick ? robot.pubKey : peerPubKey, dataFromServer.nick === userNick ? robot.pubKey : peerPubKey,
robot.encPrivKey, robot.encPrivKey,
robot.token, robot.token,
).then((decryptedData) => { ).then((decryptedData) => {
@ -122,60 +130,59 @@ const EncryptedTurtleChat: React.FC<Props> = ({
if (existingMessage != null) { if (existingMessage != null) {
return prev; return prev;
} else { } else {
return [ const x: EncryptedChatMessage = {
...prev, index: dataFromServer.index,
{ encryptedMessage: dataFromServer.message.split('\\').join('\n'),
index: dataFromServer.index, plainTextMessage: decryptedData.decryptedMessage,
encryptedMessage: dataFromServer.message.split('\\').join('\n'), validSignature: decryptedData.validSignature,
plainTextMessage: decryptedData.decryptedMessage, userNick: dataFromServer.nick,
validSignature: decryptedData.validSignature, time: dataFromServer.time,
userNick: dataFromServer.nick, };
time: dataFromServer.time, return [...prev, x].sort((a, b) => a.index - b.index);
} as EncryptedChatMessage,
].sort((a, b) => a.index - b.index);
} }
}); });
}); });
} }
// We allow plaintext communication. The user must write # to start // We allow plaintext communication. The user must write # to start
// If we receive an plaintext message // If we receive an plaintext message
else if (dataFromServer.message.substring(0, 1) == '#') { else if (dataFromServer.message.substring(0, 1) === '#') {
setMessages((prev) => { setMessages((prev: EncryptedChatMessage[]) => {
const existingMessage = prev.find( const existingMessage = prev.find(
(item) => item.plainTextMessage === dataFromServer.message, (item) => item.plainTextMessage === dataFromServer.message,
); );
if (existingMessage) { if (existingMessage != null) {
return prev; return prev;
} else { } else {
return [ const x: EncryptedChatMessage = {
...prev, index: prev.length + 0.001,
{ encryptedMessage: dataFromServer.message,
index: prev.length + 0.001, plainTextMessage: dataFromServer.message,
encryptedMessage: dataFromServer.message, validSignature: false,
plainTextMessage: dataFromServer.message, userNick: dataFromServer.nick,
validSignature: false, time: new Date().toString(),
userNick: dataFromServer.nick, };
time: new Date().toString(), return [...prev, x].sort((a, b) => a.index - b.index);
} as EncryptedChatMessage,
].sort((a, b) => a.index - b.index);
} }
}); });
} }
} }
}; };
const onButtonClicked = (e: any) => { const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => {
if (robot.token && value.includes(robot.token)) { if (robot.token !== undefined && value.includes(robot.token)) {
alert( alert(
`Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, `Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
); );
setValue(''); setValue('');
} }
// If input string contains '#' send unencrypted and unlogged message // If input string contains '#' send unencrypted and unlogged message
else if (value.substring(0, 1) == '#') { else if (value.substring(0, 1) === '#') {
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post( .post(
baseUrl, url,
`/api/chat/`, `/api/chat/`,
{ {
PGP_message: value, PGP_message: value,
@ -186,7 +193,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
) )
.then((response) => { .then((response) => {
if (response != null) { if (response != null) {
if (response.messages) { if (response.messages != null) {
setPeerConnected(response.peer_connected); setPeerConnected(response.peer_connected);
setServerMessages(response.messages); setServerMessages(response.messages);
} }
@ -198,17 +205,20 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}); });
} }
// Else if message is not empty send message // Else if message is not empty send message
else if (value != '') { else if (value !== '') {
setWaitingEcho(true); setWaitingEcho(true);
setLastSent(value); setLastSent(value);
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, robot.token) encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, robot.token)
.then((encryptedMessage) => { .then((encryptedMessage) => {
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post( .post(
baseUrl, url,
`/api/chat/`, `/api/chat/`,
{ {
PGP_message: encryptedMessage.toString().split('\n').join('\\'), PGP_message: String(encryptedMessage).split('\n').join('\\'),
order_id: orderId, order_id: orderId,
offset: lastIndex, offset: lastIndex,
}, },
@ -217,7 +227,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
.then((response) => { .then((response) => {
if (response != null) { if (response != null) {
setPeerConnected(response.peer_connected); setPeerConnected(response.peer_connected);
if (response.messages) { if (response.messages != null) {
setServerMessages(response.messages); setServerMessages(response.messages);
} }
} }
@ -249,10 +259,10 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}} }}
orderId={Number(orderId)} orderId={Number(orderId)}
messages={messages} messages={messages}
own_pub_key={robot.pubKey || ''} ownPubKey={robot.pubKey ?? ''}
own_enc_priv_key={robot.encPrivKey || ''} ownEncPrivKey={robot.encPrivKey ?? ''}
peer_pub_key={peerPubKey || 'Not received yet'} peerPubKey={peerPubKey ?? 'Not received yet'}
passphrase={robot.token || ''} passphrase={robot.token ?? ''}
onClickBack={() => { onClickBack={() => {
setAudit(false); setAudit(false);
}} }}
@ -260,7 +270,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
<Grid item> <Grid item>
<ChatHeader <ChatHeader
connected={peerPubKey ? true : false} connected={Boolean(peerPubKey)}
peerConnected={peerConnected} peerConnected={peerConnected}
turtleMode={turtleMode} turtleMode={turtleMode}
setTurtleMode={setTurtleMode} setTurtleMode={setTurtleMode}
@ -320,7 +330,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
</Grid> </Grid>
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}> <Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}>
<Button <Button
disabled={waitingEcho || !peerPubKey} disabled={waitingEcho || peerPubKey === undefined}
type='submit' type='submit'
variant='contained' variant='contained'
color='primary' color='primary'

View File

@ -19,7 +19,7 @@ interface Props {
} }
const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected, baseUrl }) => { const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected, baseUrl }) => {
const [showPGP, setShowPGP] = useState<boolean>(); const [showPGP, setShowPGP] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
@ -122,7 +122,7 @@ const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected, baseUrl
showPGP ? ( showPGP ? (
<a> <a>
{' '} {' '}
{message.time} <br /> {'Valid signature: ' + message.validSignature} <br />{' '} {message.time} <br /> {`Valid signature: ${String(message.validSignature)}`} <br />{' '}
{message.encryptedMessage}{' '} {message.encryptedMessage}{' '}
</a> </a>
) : ( ) : (

View File

@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Grid, TextField, Checkbox, Tooltip, FormControlLabel } from '@mui/material'; import { Grid, TextField, Checkbox, Tooltip, FormControlLabel } from '@mui/material';
import { Order } from '../../../models';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { EncryptedChatMessage } from '../EncryptedChat';
export interface DisputeForm { export interface DisputeForm {
statement: string; statement: string;
@ -43,7 +41,7 @@ export const DisputeStatementForm = ({
> >
<Grid item> <Grid item>
<TextField <TextField
error={dispute.badStatement != ''} error={dispute.badStatement !== ''}
helperText={dispute.badStatement} helperText={dispute.badStatement}
label={t('Submit dispute statement')} label={t('Submit dispute statement')}
required required

View File

@ -33,7 +33,7 @@ import { apiClient } from '../../../services/api';
import { systemClient } from '../../../services/System'; import { systemClient } from '../../../services/System';
import lnproxies from '../../../../static/lnproxies.json'; import lnproxies from '../../../../static/lnproxies.json';
let filteredProxies: { [key: string]: any }[] = []; let filteredProxies: Array<Record<string, any>> = [];
export interface LightningForm { export interface LightningForm {
invoice: string; invoice: string;
amount: number; amount: number;
@ -95,23 +95,23 @@ export const LightningPayoutForm = ({
const [loadingLnproxy, setLoadingLnproxy] = useState<boolean>(false); const [loadingLnproxy, setLoadingLnproxy] = useState<boolean>(false);
const [noMatchingLnProxies, setNoMatchingLnProxies] = useState<string>(''); const [noMatchingLnProxies, setNoMatchingLnProxies] = useState<string>('');
const computeInvoiceAmount = function () { const computeInvoiceAmount = (): number => {
const tradeAmount = order.trade_satoshis; const tradeAmount = order.trade_satoshis;
return Math.floor(tradeAmount - tradeAmount * (lightning.routingBudgetPPM / 1000000)); return Math.floor(tradeAmount - tradeAmount * (lightning.routingBudgetPPM / 1000000));
}; };
const validateInvoice = function (invoice: string, targetAmount: number) { const validateInvoice = (invoice: string, targetAmount: number): string => {
try { try {
const decoded = decode(invoice); const decoded = decode(invoice);
const invoiceAmount = Math.floor(decoded.sections[2].value / 1000); const invoiceAmount = Math.floor(decoded.sections[2].value / 1000);
if (targetAmount != invoiceAmount) { if (targetAmount !== invoiceAmount) {
return 'Invalid invoice amount'; return 'Invalid invoice amount';
} else { } else {
return ''; return '';
} }
} catch (err) { } catch (err) {
const error = err.toString(); const error = err.toString();
return `${error.substring(0, 100)}${error.length > 100 ? '...' : ''}`; return `${String(error).substring(0, 100)}${error.length > 100 ? '...' : ''}`;
} }
}; };
@ -122,14 +122,14 @@ export const LightningPayoutForm = ({
amount, amount,
lnproxyAmount: amount - lightning.lnproxyBudgetSats, lnproxyAmount: amount - lightning.lnproxyBudgetSats,
routingBudgetSats: routingBudgetSats:
lightning.routingBudgetSats == undefined lightning.routingBudgetSats === undefined
? Math.ceil((amount / 1000000) * lightning.routingBudgetPPM) ? Math.ceil((amount / 1000000) * lightning.routingBudgetPPM)
: lightning.routingBudgetSats, : lightning.routingBudgetSats,
}); });
}, [lightning.routingBudgetPPM]); }, [lightning.routingBudgetPPM]);
useEffect(() => { useEffect(() => {
if (lightning.invoice != '') { if (lightning.invoice !== '') {
setLightning({ setLightning({
...lightning, ...lightning,
badInvoice: validateInvoice(lightning.invoice, lightning.amount), badInvoice: validateInvoice(lightning.invoice, lightning.amount),
@ -138,7 +138,7 @@ export const LightningPayoutForm = ({
}, [lightning.invoice, lightning.amount]); }, [lightning.invoice, lightning.amount]);
useEffect(() => { useEffect(() => {
if (lightning.lnproxyInvoice != '') { if (lightning.lnproxyInvoice !== '') {
setLightning({ setLightning({
...lightning, ...lightning,
badLnproxy: validateInvoice(lightning.lnproxyInvoice, lightning.lnproxyAmount), badLnproxy: validateInvoice(lightning.lnproxyInvoice, lightning.lnproxyAmount),
@ -146,23 +146,23 @@ export const LightningPayoutForm = ({
} }
}, [lightning.lnproxyInvoice, lightning.lnproxyAmount]); }, [lightning.lnproxyInvoice, lightning.lnproxyAmount]);
//filter lnproxies when the network settings are updated // filter lnproxies when the network settings are updated
let bitcoinNetwork: string = 'mainnet'; let bitcoinNetwork: string = 'mainnet';
let internetNetwork: 'Clearnet' | 'I2P' | 'TOR' = 'Clearnet'; let internetNetwork: 'Clearnet' | 'I2P' | 'TOR' = 'Clearnet';
useEffect(() => { useEffect(() => {
bitcoinNetwork = settings?.network ?? 'mainnet'; bitcoinNetwork = settings?.network ?? 'mainnet';
if (settings.host?.includes('.i2p')) { if (settings.host?.includes('.i2p') === true) {
internetNetwork = 'I2P'; internetNetwork = 'I2P';
} else if (settings.host?.includes('.onion') || window.NativeRobosats != undefined) { } else if (settings.host?.includes('.onion') === true || window.NativeRobosats !== undefined) {
internetNetwork = 'TOR'; internetNetwork = 'TOR';
} }
filteredProxies = lnproxies filteredProxies = lnproxies
.filter((node) => node.relayType == internetNetwork) .filter((node) => node.relayType === internetNetwork)
.filter((node) => node.network == bitcoinNetwork); .filter((node) => node.network === bitcoinNetwork);
}, [settings]); }, [settings]);
//if "use lnproxy" checkbox is enabled, but there are no matching proxies, enter error state // if "use lnproxy" checkbox is enabled, but there are no matching proxies, enter error state
useEffect(() => { useEffect(() => {
setNoMatchingLnProxies(''); setNoMatchingLnProxies('');
if (filteredProxies.length === 0) { if (filteredProxies.length === 0) {
@ -175,21 +175,21 @@ export const LightningPayoutForm = ({
} }
}, [lightning.useLnproxy]); }, [lightning.useLnproxy]);
const fetchLnproxy = function () { const fetchLnproxy = function (): void {
setLoadingLnproxy(true); setLoadingLnproxy(true);
let body: { invoice: string; description: string; routing_msat?: string } = { const body: { invoice: string; description: string; routing_msat?: string } = {
invoice: lightning.lnproxyInvoice, invoice: lightning.lnproxyInvoice,
description: '', description: '',
}; };
if (lightning.lnproxyBudgetSats > 0) { if (lightning.lnproxyBudgetSats > 0) {
body['routing_msat'] = String(lightning.lnproxyBudgetSats * 1000); body.routing_msat = String(lightning.lnproxyBudgetSats * 1000);
} }
apiClient apiClient
.post(filteredProxies[lightning.lnproxyServer]['url'], '', body) .post(filteredProxies[lightning.lnproxyServer].url, '', body)
.then((data) => { .then((data) => {
if (data.reason) { if (data.reason !== undefined) {
setLightning({ ...lightning, badLnproxy: data.reason }); setLightning({ ...lightning, badLnproxy: data.reason });
} else if (data.proxy_invoice) { } else if (data.proxy_invoice !== undefined) {
setLightning({ ...lightning, invoice: data.proxy_invoice, badLnproxy: '' }); setLightning({ ...lightning, invoice: data.proxy_invoice, badLnproxy: '' });
} else { } else {
setLightning({ ...lightning, badLnproxy: 'Unknown lnproxy response' }); setLightning({ ...lightning, badLnproxy: 'Unknown lnproxy response' });
@ -203,7 +203,7 @@ export const LightningPayoutForm = ({
}); });
}; };
const handleAdvancedOptions = function (checked: boolean) { const handleAdvancedOptions = function (checked: boolean): void {
if (checked) { if (checked) {
setLightning({ setLightning({
...lightning, ...lightning,
@ -218,7 +218,7 @@ export const LightningPayoutForm = ({
} }
}; };
const onProxyBudgetChange = function (e) { const onProxyBudgetChange = function (e: React.ChangeEventHandler<HTMLInputElement>): void {
if (isFinite(e.target.value) && e.target.value >= 0) { if (isFinite(e.target.value) && e.target.value >= 0) {
let lnproxyBudgetSats; let lnproxyBudgetSats;
let lnproxyBudgetPPM; let lnproxyBudgetPPM;
@ -238,7 +238,7 @@ export const LightningPayoutForm = ({
} }
}; };
const onRoutingBudgetChange = function (e) { const onRoutingBudgetChange = function (e: React.ChangeEventHandler<HTMLInputElement>): void {
const tradeAmount = order.trade_satoshis; const tradeAmount = order.trade_satoshis;
if (isFinite(e.target.value) && e.target.value >= 0) { if (isFinite(e.target.value) && e.target.value >= 0) {
let routingBudgetSats; let routingBudgetSats;
@ -261,7 +261,7 @@ export const LightningPayoutForm = ({
} }
}; };
const lnProxyBudgetHelper = function () { const lnProxyBudgetHelper = function (): string {
let text = ''; let text = '';
if (lightning.lnproxyBudgetSats < 0) { if (lightning.lnproxyBudgetSats < 0) {
text = 'Must be positive'; text = 'Must be positive';
@ -271,7 +271,7 @@ export const LightningPayoutForm = ({
return text; return text;
}; };
const routingBudgetHelper = function () { const routingBudgetHelper = function (): string {
let text = ''; let text = '';
if (lightning.routingBudgetSats < 0) { if (lightning.routingBudgetSats < 0) {
text = 'Must be positive'; text = 'Must be positive';
@ -334,12 +334,12 @@ export const LightningPayoutForm = ({
<TextField <TextField
sx={{ width: '14em' }} sx={{ width: '14em' }}
disabled={!lightning.advancedOptions} disabled={!lightning.advancedOptions}
error={routingBudgetHelper() != ''} error={routingBudgetHelper() !== ''}
helperText={routingBudgetHelper()} helperText={routingBudgetHelper()}
label={t('Routing Budget')} label={t('Routing Budget')}
required={true} required={true}
value={ value={
lightning.routingBudgetUnit == 'PPM' lightning.routingBudgetUnit === 'PPM'
? lightning.routingBudgetPPM ? lightning.routingBudgetPPM
: lightning.routingBudgetSats : lightning.routingBudgetSats
} }
@ -353,7 +353,7 @@ export const LightningPayoutForm = ({
setLightning({ setLightning({
...lightning, ...lightning,
routingBudgetUnit: routingBudgetUnit:
lightning.routingBudgetUnit == 'PPM' ? 'Sats' : 'PPM', lightning.routingBudgetUnit === 'PPM' ? 'Sats' : 'PPM',
}); });
}} }}
> >
@ -382,11 +382,11 @@ export const LightningPayoutForm = ({
> >
<div> <div>
<FormControlLabel <FormControlLabel
onChange={(e) => { onChange={(e, checked) => {
setLightning({ setLightning({
...lightning, ...lightning,
useLnproxy: e.target.checked, useLnproxy: checked,
invoice: e.target.checked ? '' : lightning.invoice, invoice: checked ? '' : lightning.invoice,
}); });
}} }}
checked={lightning.useLnproxy} checked={lightning.useLnproxy}
@ -419,7 +419,7 @@ export const LightningPayoutForm = ({
spacing={1} spacing={1}
> >
<Grid item> <Grid item>
<FormControl error={noMatchingLnProxies != ''}> <FormControl error={noMatchingLnProxies !== ''}>
<InputLabel id='select-label'>{t('Server')}</InputLabel> <InputLabel id='select-label'>{t('Server')}</InputLabel>
<Select <Select
sx={{ width: '14em' }} sx={{ width: '14em' }}
@ -436,7 +436,7 @@ export const LightningPayoutForm = ({
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{noMatchingLnProxies != '' ? ( {noMatchingLnProxies !== '' ? (
<FormHelperText>{t(noMatchingLnProxies)}</FormHelperText> <FormHelperText>{t(noMatchingLnProxies)}</FormHelperText>
) : ( ) : (
<></> <></>
@ -448,11 +448,11 @@ export const LightningPayoutForm = ({
<TextField <TextField
sx={{ width: '14em' }} sx={{ width: '14em' }}
disabled={!lightning.useLnproxy} disabled={!lightning.useLnproxy}
error={lnProxyBudgetHelper() != ''} error={lnProxyBudgetHelper() !== ''}
helperText={lnProxyBudgetHelper()} helperText={lnProxyBudgetHelper()}
label={t('Proxy Budget')} label={t('Proxy Budget')}
value={ value={
lightning.lnproxyBudgetUnit == 'PPM' lightning.lnproxyBudgetUnit === 'PPM'
? lightning.lnproxyBudgetPPM ? lightning.lnproxyBudgetPPM
: lightning.lnproxyBudgetSats : lightning.lnproxyBudgetSats
} }
@ -466,7 +466,7 @@ export const LightningPayoutForm = ({
setLightning({ setLightning({
...lightning, ...lightning,
lnproxyBudgetUnit: lnproxyBudgetUnit:
lightning.lnproxyBudgetUnit == 'PPM' ? 'Sats' : 'PPM', lightning.lnproxyBudgetUnit === 'PPM' ? 'Sats' : 'PPM',
}); });
}} }}
> >
@ -520,9 +520,9 @@ export const LightningPayoutForm = ({
<TextField <TextField
fullWidth={true} fullWidth={true}
disabled={!lightning.useLnproxy} disabled={!lightning.useLnproxy}
error={lightning.badLnproxy != ''} error={lightning.badLnproxy !== ''}
FormHelperTextProps={{ style: { wordBreak: 'break-all' } }} FormHelperTextProps={{ style: { wordBreak: 'break-all' } }}
helperText={lightning.badLnproxy ? t(lightning.badLnproxy) : ''} helperText={lightning.badLnproxy !== '' ? t(lightning.badLnproxy) : ''}
label={t('Invoice to wrap')} label={t('Invoice to wrap')}
required required
value={lightning.lnproxyInvoice} value={lightning.lnproxyInvoice}
@ -541,8 +541,8 @@ export const LightningPayoutForm = ({
fullWidth={true} fullWidth={true}
sx={lightning.useLnproxy ? { borderRadius: 0 } : {}} sx={lightning.useLnproxy ? { borderRadius: 0 } : {}}
disabled={lightning.useLnproxy} disabled={lightning.useLnproxy}
error={lightning.badInvoice != ''} error={lightning.badInvoice !== ''}
helperText={lightning.badInvoice ? t(lightning.badInvoice) : ''} helperText={lightning.badInvoice !== '' ? t(lightning.badInvoice) : ''}
FormHelperTextProps={{ style: { wordBreak: 'break-all' } }} FormHelperTextProps={{ style: { wordBreak: 'break-all' } }}
label={lightning.useLnproxy ? t('Wrapped invoice') : t('Payout Lightning Invoice')} label={lightning.useLnproxy ? t('Wrapped invoice') : t('Payout Lightning Invoice')}
required required
@ -566,8 +566,8 @@ export const LightningPayoutForm = ({
loading={loadingLnproxy} loading={loadingLnproxy}
disabled={ disabled={
lightning.lnproxyInvoice.length < 20 || lightning.lnproxyInvoice.length < 20 ||
noMatchingLnProxies != '' || noMatchingLnProxies !== '' ||
lightning.badLnproxy != '' lightning.badLnproxy !== ''
} }
onClick={fetchLnproxy} onClick={fetchLnproxy}
variant='outlined' variant='outlined'
@ -580,7 +580,7 @@ export const LightningPayoutForm = ({
)} )}
<LoadingButton <LoadingButton
loading={loading} loading={loading}
disabled={lightning.invoice.length < 20 || lightning.badInvoice != ''} disabled={lightning.invoice.length < 20 || lightning.badInvoice !== ''}
onClick={() => { onClick={() => {
onClickSubmit(lightning.invoice); onClickSubmit(lightning.invoice);
}} }}

View File

@ -39,7 +39,7 @@ export const OnchainPayoutForm = ({
const invalidFee = onchain.miningFee < minMiningFee || onchain.miningFee > maxMiningFee; const invalidFee = onchain.miningFee < minMiningFee || onchain.miningFee > maxMiningFee;
const costPerVByte = 280; const costPerVByte = 280;
const handleMiningFeeChange = (e) => { const handleMiningFeeChange = (e: React.ChangeEventHandler<HTMLInputElement>): void => {
const miningFee = Number(e.target.value); const miningFee = Number(e.target.value);
setOnchain({ ...onchain, miningFee }); setOnchain({ ...onchain, miningFee });
}; };
@ -67,12 +67,9 @@ export const OnchainPayoutForm = ({
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={ primary={`${pn(Math.floor((order.invoice_amount * order.swap_fee_rate) / 100))} Sats (${
pn(Math.floor((order.invoice_amount * order.swap_fee_rate) / 100)) + order.swap_fee_rate
' Sats (' + }%)`}
order.swap_fee_rate +
'%)'
}
secondary={t('Swap fee')} secondary={t('Swap fee')}
/> />
</ListItem> </ListItem>
@ -81,12 +78,9 @@ export const OnchainPayoutForm = ({
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={ primary={`${pn(
pn(Math.floor(Math.max(minMiningFee, onchain.miningFee) * costPerVByte)) + Math.floor(Math.max(minMiningFee, onchain.miningFee) * costPerVByte),
' Sats (' + )} Sats (${Math.max(minMiningFee, onchain.miningFee)} Sats/vByte)`}
Math.max(minMiningFee, onchain.miningFee) +
' Sats/vByte)'
}
secondary={t('Mining fee')} secondary={t('Mining fee')}
/> />
</ListItem> </ListItem>
@ -115,8 +109,8 @@ export const OnchainPayoutForm = ({
<Grid container direction='row' justifyContent='center' alignItems='flex-start' spacing={0}> <Grid container direction='row' justifyContent='center' alignItems='flex-start' spacing={0}>
<Grid item xs={7}> <Grid item xs={7}>
<TextField <TextField
error={onchain.badAddress != ''} error={onchain.badAddress !== ''}
helperText={onchain.badAddress ? t(onchain.badAddress) : ''} helperText={onchain.badAddress !== '' ? t(onchain.badAddress) : ''}
label={t('Bitcoin Address')} label={t('Bitcoin Address')}
required required
value={onchain.address} value={onchain.address}

Some files were not shown because too many files have changed in this diff Show More