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
paths:
- frontend
pull_request:
pull_request_target:
branches:
- main
paths:
@ -41,7 +41,5 @@ jobs:
with:
prettier: true
prettier_dir: frontend
## Disabled due to error
# eslint: true
# eslint_dir: frontend
eslint: true
eslint_dir: frontend

View File

@ -61,6 +61,7 @@ services:
volumes:
- ./nodeapp/:/usr/src/robosats/
- ./nodeapp/nginx.conf:/etc/nginx/nginx.conf
- ./nodeapp/coordinators/:/etc/nginx/conf.d/
- ./frontend/static:/usr/src/robosats/static
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.
- 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.
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

View File

@ -19,12 +19,21 @@
"sourceType": "module",
"project": "./tsconfig.json"
},
"ignorePatterns": ["index.js", "**/PaymentMethods/Icons/code/code.js"],
"plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react-hooks/exhaustive-deps": "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": {
"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 Main from './basic/Main';
import { CssBaseline, ThemeProvider } from '@mui/material';
@ -11,20 +11,29 @@ import i18n from './i18n/Web';
import { systemClient } from './services/System';
import ErrorBoundary from './components/ErrorBoundary';
import { GarageContext, useGarageStore } from './contexts/GarageContext';
import { FederationContext, useFederationStore } from './contexts/FederationContext';
const App = (): JSX.Element => {
const store = useAppStore();
const appStore = useAppStore();
const garageStore = useGarageStore();
const federationStore = useFederationStore();
return (
<StrictMode>
<ErrorBoundary>
<Suspense fallback='loading'>
<I18nextProvider i18n={i18n}>
<AppContext.Provider value={store}>
<ThemeProvider theme={store.theme}>
<AppContext.Provider value={appStore}>
<GarageContext.Provider value={garageStore}>
<FederationContext.Provider value={federationStore}>
<ThemeProvider theme={appStore.theme}>
<CssBaseline />
{window.NativeRobosats === undefined ? <HostAlert /> : <TorConnectionBadge />}
<Main />
</ThemeProvider>
</FederationContext.Provider>
</GarageContext.Provider>
</AppContext.Provider>
</I18nextProvider>
</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
if (systemClient.loading) {
setTimeout(loadApp, 200);

View File

@ -12,10 +12,13 @@ import BookTable from '../../components/BookTable';
import { BarChart, FormatListBulleted, Map } from '@mui/icons-material';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import MapChart from '../../components/Charts/MapChart';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
const BookPage = (): JSX.Element => {
const { robot, fetchBook, windowSize, setDelay, setOrder } =
useContext<UseAppStoreType>(AppContext);
const { windowSize } = useContext<UseAppStoreType>(AppContext);
const { setDelay } = useContext<UseFederationStoreType>(FederationContext);
const { garage, clearOrder } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const navigate = useNavigate();
const [view, setView] = useState<'list' | 'depth' | 'map'>('list');
@ -27,25 +30,17 @@ const BookPage = (): JSX.Element => {
const maxBookTableWidth = 85;
const chartWidthEm = width - maxBookTableWidth;
useEffect(() => {
fetchBook();
}, []);
const onViewOrder = function () {
setOrder(undefined);
const onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getRobot().avatarLoaded) {
clearOrder();
setDelay(10000);
};
const onOrderClicked = function (id: number) {
if (robot.avatarLoaded) {
navigate('/order/' + id);
onViewOrder();
navigate(`/order/${shortAlias}/${id}`);
} else {
setOpenNoRobot(true);
}
};
const NavButtons = function () {
const NavButtons = function (): JSX.Element {
return (
<ButtonGroup variant='contained' color='inherit'>
<Button
@ -60,13 +55,25 @@ const BookPage = (): JSX.Element => {
<></>
) : (
<>
<Button onClick={() => setView('list')}>
<Button
onClick={() => {
setView('list');
}}
>
<FormatListBulleted /> {t('List')}
</Button>
<Button onClick={() => setView('depth')}>
<Button
onClick={() => {
setView('depth');
}}
>
<BarChart /> {t('Chart')}
</Button>
<Button onClick={() => setView('map')}>
<Button
onClick={() => {
setView('map');
}}
>
<Map /> {t('Map')}
</Button>
</>
@ -82,7 +89,9 @@ const BookPage = (): JSX.Element => {
onClose={() => {
setOpenNoRobot(false);
}}
onClickGenerateRobot={() => navigate('/robot')}
onClickGenerateRobot={() => {
navigate('/robot');
}}
/>
{openMaker ? (
<Dialog
@ -94,9 +103,11 @@ const BookPage = (): JSX.Element => {
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
<MakerForm
onOrderCreated={(id) => {
navigate('/order/' + id);
navigate(`/order/${id}`);
}}
onClickGenerateRobot={() => {
navigate('/robot');
}}
onClickGenerateRobot={() => navigate('/robot')}
/>
</Box>
</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 { Box, Slide, Typography, styled } from '@mui/material';
import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext';
import RobotPage from './RobotPage';
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 { RobotPage, MakerPage, BookPage, OrderPage, SettingsPage, NavBar, MainDialogs } from './';
import RobotAvatar from '../components/RobotAvatar';
import Notifications from '../components/Notifications';
import { useTranslation } from 'react-i18next';
import Notifications from '../components/Notifications';
import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext';
import { FederationContext, UseFederationStoreType } from '../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../contexts/GarageContext';
const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter;
@ -35,39 +30,31 @@ const MainBox = styled(Box)<MainBoxProps>((props) => ({
const Main: React.FC = () => {
const { t } = useTranslation();
const {
settings,
robot,
setRobot,
baseUrl,
order,
page,
slideDirection,
setOpen,
windowSize,
navbarHeight,
} = useContext<UseAppStoreType>(AppContext);
const { settings, page, slideDirection, setOpen, windowSize, navbarHeight, hostUrl, origin } =
useContext<UseAppStoreType>(AppContext);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const [avatarBaseUrl, setAvatarBaseUrl] = useState<string>(hostUrl);
useEffect(() => {
setAvatarBaseUrl(federation.getCoordinator(sortedCoordinators[0]).getBaseUrl());
}, [settings.network, settings.selfhostedClient, federation, sortedCoordinators]);
return (
<Router>
<RobotAvatar
style={{ display: 'none' }}
nickname={robot.nickname}
baseUrl={baseUrl}
onLoad={() => {
setRobot((robot) => {
return { ...robot, avatarLoaded: true };
});
}}
nickname={garage.getRobot().nickname}
baseUrl={federation.getCoordinator(sortedCoordinators[0]).getBaseUrl()}
onLoad={() => garage.updateRobot({ avatarLoaded: true })}
/>
<Notifications
order={order}
page={page}
openProfile={() => {
setOpen({ ...closeAll, profile: true });
}}
rewards={robot.earnedRewards}
windowWidth={windowSize.width}
rewards={garage.getRobot().earnedRewards}
windowWidth={windowSize?.width}
/>
{settings.network === 'testnet' ? (
<TestnetTypography color='secondary' align='center'>
@ -87,10 +74,10 @@ const Main: React.FC = () => {
<Slide
direction={page === 'robot' ? slideDirection.in : slideDirection.out}
in={page === 'robot'}
appear={slideDirection.in != undefined}
appear={slideDirection.in !== undefined}
>
<div>
<RobotPage />
<RobotPage avatarBaseUrl={avatarBaseUrl} />
</div>
</Slide>
}
@ -105,7 +92,7 @@ const Main: React.FC = () => {
<Slide
direction={page === 'offers' ? slideDirection.in : slideDirection.out}
in={page === 'offers'}
appear={slideDirection.in != undefined}
appear={slideDirection.in !== undefined}
>
<div>
<BookPage />
@ -120,7 +107,7 @@ const Main: React.FC = () => {
<Slide
direction={page === 'create' ? slideDirection.in : slideDirection.out}
in={page === 'create'}
appear={slideDirection.in != undefined}
appear={slideDirection.in !== undefined}
>
<div>
<MakerPage />
@ -130,12 +117,12 @@ const Main: React.FC = () => {
/>
<Route
path='/order/:orderId'
path='/order/:shortAlias/:orderId'
element={
<Slide
direction={page === 'order' ? slideDirection.in : slideDirection.out}
in={page === 'order'}
appear={slideDirection.in != undefined}
appear={slideDirection.in !== undefined}
>
<div>
<OrderPage />
@ -150,7 +137,7 @@ const Main: React.FC = () => {
<Slide
direction={page === 'settings' ? slideDirection.in : slideDirection.out}
in={page === 'settings'}
appear={slideDirection.in != undefined}
appear={slideDirection.in !== undefined}
>
<div>
<SettingsPage />
@ -160,7 +147,7 @@ const Main: React.FC = () => {
/>
</Routes>
</MainBox>
<NavBar width={windowSize.width} height={navbarHeight} />
<NavBar />
<MainDialogs />
</Router>
);

View File

@ -1,16 +1,17 @@
import React, { useState, useContext, useEffect } from 'react';
import {
CommunityDialog,
CoordinatorSummaryDialog,
InfoDialog,
ExchangeDialog,
CoordinatorDialog,
AboutDialog,
LearnDialog,
ProfileDialog,
StatsDialog,
UpdateClientDialog,
NoticeDialog,
ClientDialog,
UpdateDialog,
} from '../../components/Dialogs';
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 {
more: boolean;
@ -18,100 +19,96 @@ export interface OpenDialogs {
community: boolean;
info: boolean;
coordinator: boolean;
stats: boolean;
exchange: boolean;
client: boolean;
update: boolean;
profile: boolean;
notice: boolean;
}
const MainDialogs = (): JSX.Element => {
const { open, setOpen, info, limits, robot, setRobot, setCurrentOrder, baseUrl } =
const { open, setOpen, settings, clientVersion, hostUrl } =
useContext<UseAppStoreType>(AppContext);
const { federation, focusedCoordinator, coordinatorUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const [maxAmount, setMaxAmount] = useState<string>('...loading...');
useEffect(() => {
if (limits.list[1000]) {
setMaxAmount(pn(limits.list[1000].max_amount * 100000000));
if (focusedCoordinator) {
const limits = federation.getCoordinator(focusedCoordinator).limits;
if (limits[1000] !== undefined) {
setMaxAmount(pn(limits[1000].max_amount * 100000000));
}
}, [limits.list]);
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]);
}, [coordinatorUpdatedAt]);
return (
<>
<UpdateClientDialog
open={open.update}
coordinatorVersion={info.coordinatorVersion}
clientVersion={info.clientVersion}
<UpdateDialog
coordinatorVersion={federation.exchange.info.version}
clientVersion={clientVersion.semver}
onClose={() => {
setOpen({ ...open, update: false });
setOpen((open) => {
return { ...open, update: false };
});
}}
/>
<NoticeDialog
open={open.notice}
severity={info.notice_severity}
message={info.notice_message}
onClose={() => {
setOpen({ ...open, notice: false });
}}
/>
<InfoDialog
<AboutDialog
open={open.info}
maxAmount={maxAmount}
onClose={() => {
setOpen({ ...open, info: false });
setOpen((open) => {
return { ...open, info: false };
});
}}
/>
<LearnDialog
open={open.learn}
onClose={() => {
setOpen({ ...open, learn: false });
setOpen((open) => {
return { ...open, learn: false };
});
}}
/>
<CommunityDialog
open={open.community}
onClose={() => {
setOpen({ ...open, community: false });
setOpen((open) => {
return { ...open, community: false };
});
}}
/>
<CoordinatorSummaryDialog
open={open.coordinator}
<ExchangeDialog
open={open.exchange}
onClose={() => {
setOpen({ ...open, coordinator: false });
setOpen((open) => {
return { ...open, exchange: false };
});
}}
info={info}
/>
<StatsDialog
open={open.stats}
<ClientDialog
open={open.client}
onClose={() => {
setOpen({ ...open, stats: false });
setOpen((open) => {
return { ...open, client: false };
});
}}
info={info}
/>
<ProfileDialog
open={open.profile}
baseUrl={baseUrl}
baseUrl={hostUrl}
onClose={() => {
setOpen({ ...open, profile: false });
}}
robot={robot}
setRobot={setRobot}
setCurrentOrder={setCurrentOrder}
/>
<CoordinatorDialog
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 { useNavigate } from 'react-router-dom';
import { Grid, Paper, Collapse, Typography } from '@mui/material';
import { filterOrders } from '../../utils';
import MakerForm from '../../components/MakerForm';
@ -10,10 +9,13 @@ import BookTable from '../../components/BookTable';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { NoRobotDialog } from '../../components/Dialogs';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
const MakerPage = (): JSX.Element => {
const { robot, book, fav, maker, windowSize, navbarHeight, setOrder, setDelay } =
useContext<UseAppStoreType>(AppContext);
const { fav, windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { setDelay, federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, maker } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const navigate = useNavigate();
@ -23,7 +25,7 @@ const MakerPage = (): JSX.Element => {
const matches = useMemo(() => {
return filterOrders({
orders: book.orders,
orders: federation.book,
baseFilter: {
currency: fav.currency === 0 ? 1 : fav.currency,
type: fav.type,
@ -39,7 +41,7 @@ const MakerPage = (): JSX.Element => {
},
});
}, [
book.orders,
federation.book,
fav,
maker.premium,
maker.amount,
@ -48,14 +50,14 @@ const MakerPage = (): JSX.Element => {
maker.paymentMethods,
]);
const onViewOrder = function () {
setOrder(undefined);
const onViewOrder = function (): void {
garage.updateOrder(null);
setDelay(10000);
};
const onOrderClicked = function (id: number) {
if (robot.avatarLoaded) {
navigate('/order/' + id);
const onOrderClicked = function (id: number): void {
if (garage.getRobot().avatarLoaded) {
navigate(`/order/${id}`);
onViewOrder();
} else {
setOpenNoRobot(true);
@ -69,7 +71,9 @@ const MakerPage = (): JSX.Element => {
onClose={() => {
setOpenNoRobot(false);
}}
onClickGenerateRobot={() => navigate('/robot')}
onClickGenerateRobot={() => {
navigate('/robot');
}}
/>
<Grid item>
<Collapse in={matches.length > 0 && showMatches}>
@ -103,8 +107,8 @@ const MakerPage = (): JSX.Element => {
}}
>
<MakerForm
onOrderCreated={(id) => {
navigate('/order/' + id);
onOrderCreated={(shortAlias, id) => {
navigate(`/order/${shortAlias}/${id}`);
}}
disableRequest={matches.length > 0 && !showMatches}
collapseAll={showMatches}
@ -115,7 +119,9 @@ const MakerPage = (): JSX.Element => {
setShowMatches(false);
}}
submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'}
onClickGenerateRobot={() => navigate('/robot')}
onClickGenerateRobot={() => {
navigate('/robot');
}}
/>
</Paper>
</Grid>

View File

@ -1,9 +1,8 @@
import React from 'react';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useTheme, styled, Grid, IconButton } from '@mui/material';
import Tooltip, { type TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import { closeAll } from '../../contexts/AppContext';
import { type OpenDialogs } from '../MainDialogs';
import { closeAll, type UseAppStoreType, AppContext } from '../../contexts/AppContext';
import { BubbleChart, Info, People, PriceChange, School } from '@mui/icons-material';
@ -20,13 +19,13 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
}));
interface MoreTooltipProps {
open: OpenDialogs;
setOpen: (state: OpenDialogs) => void;
children: JSX.Element;
}
const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element => {
const MoreTooltip = ({ children }: MoreTooltipProps): JSX.Element => {
const { t } = useTranslation();
const { open, setOpen } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
return (
<StyledTooltip
@ -89,15 +88,13 @@ const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element
</Grid>
<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
sx={{
color: open.coordinator
? theme.palette.primary.main
: theme.palette.text.secondary,
color: open.exchange ? theme.palette.primary.main : theme.palette.text.secondary,
}}
onClick={() => {
setOpen({ ...closeAll, coordinator: !open.coordinator });
setOpen({ ...closeAll, exchange: !open.exchange });
}}
>
<PriceChange />
@ -106,13 +103,13 @@ const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element
</Grid>
<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
sx={{
color: open.stats ? theme.palette.primary.main : theme.palette.text.secondary,
color: open.client ? theme.palette.primary.main : theme.palette.text.secondary,
}}
onClick={() => {
setOpen({ ...closeAll, stats: !open.stats });
setOpen({ ...closeAll, client: !open.client });
}}
>
<BubbleChart />

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import {
Box,
useTheme,
Tooltip,
type SelectChangeEvent,
} from '@mui/material';
import { Bolt, Add, DeleteSweep, Logout, Download } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar';
@ -20,6 +21,8 @@ import { type Slot, type Robot } from '../../models';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { genBase62Token } from '../../utils';
import { LoadingButton } from '@mui/lab';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
interface RobotProfileProps {
robot: Robot;
@ -29,23 +32,22 @@ interface RobotProfileProps {
inputToken: string;
logoutRobot: () => void;
setInputToken: (state: string) => void;
baseUrl: string;
width: number;
baseUrl: string;
}
const RobotProfile = ({
robot,
setRobot,
inputToken,
getGenerateRobot,
setInputToken,
logoutRobot,
setView,
baseUrl,
width,
}: RobotProfileProps): JSX.Element => {
const { currentSlot, garage, setCurrentSlot, windowSize } =
useContext<UseAppStoreType>(AppContext);
const { windowSize, hostUrl } = useContext<UseAppStoreType>(AppContext);
const { currentOrder } = useContext<UseFederationStoreType>(FederationContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
@ -53,19 +55,18 @@ const RobotProfile = ({
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (robot.nickname && robot.avatarLoaded) {
if (garage.getRobot().nickname && garage.getRobot().avatarLoaded) {
setLoading(false);
}
}, [robot]);
}, [robotUpdatedAt]);
const handleAddRobot = () => {
getGenerateRobot(genBase62Token(36), garage.slots.length);
const handleAddRobot = (): void => {
getGenerateRobot(genBase62Token(36));
setLoading(true);
};
const handleChangeSlot = (e) => {
const slot = e.target.value;
getGenerateRobot(garage.slots[slot].robot.token, slot);
const handleChangeSlot = (e: SelectChangeEvent<number | 'loading'>): void => {
garage.currentSlot = Number(e.target.value);
setLoading(true);
};
@ -80,7 +81,7 @@ const RobotProfile = ({
sx={{ width: '100%' }}
>
<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'>
<div
style={{
@ -99,7 +100,7 @@ const RobotProfile = ({
}}
/>
)}
<b>{robot.nickname}</b>
<b>{garage.getRobot().nickname}</b>
{width < 19 ? null : (
<Bolt
sx={{
@ -121,7 +122,7 @@ const RobotProfile = ({
<Grid item sx={{ width: `13.5em` }}>
<RobotAvatar
nickname={robot.nickname}
nickname={garage.getRobot().nickname}
smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating'
@ -134,9 +135,9 @@ const RobotProfile = ({
}}
tooltip={t('This is your trading avatar')}
tooltipPosition='top'
baseUrl={baseUrl}
baseUrl={hostUrl}
/>
{robot.found && !robot.lastOrderId ? (
{garage.getRobot().found && Number(garage.getRobot().lastOrderId) > 0 ? (
<Typography align='center' variant='h6'>
{t('Welcome back!')}
</Typography>
@ -145,27 +146,31 @@ const RobotProfile = ({
)}
</Grid>
{robot.activeOrderId && robot.avatarLoaded && robot.nickname ? (
{Boolean(garage.getRobot().activeOrderId) &&
garage.getRobot().avatarLoaded &&
Boolean(garage.getRobot().nickname) ? (
<Grid item>
<Button
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>
</Grid>
) : 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>
<Button
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>
</Grid>
<Grid item>
@ -221,8 +226,6 @@ const RobotProfile = ({
editable={false}
label={t('Store your token safely')}
setInputToken={setInputToken}
setRobot={setRobot}
robot={robot}
onPressEnter={() => null}
/>
</Grid>
@ -246,7 +249,7 @@ const RobotProfile = ({
inputProps={{
style: { textAlign: 'center' },
}}
value={loading ? 'loading' : currentSlot}
value={loading ? 'loading' : garage.currentSlot}
onChange={handleChangeSlot}
>
{loading ? (
@ -271,7 +274,7 @@ const RobotProfile = ({
smooth={true}
style={{ width: '2.6em', height: '2.6em' }}
placeholderType='loading'
baseUrl={baseUrl}
baseUrl={hostUrl}
small={true}
/>
</Grid>
@ -314,7 +317,7 @@ const RobotProfile = ({
color='primary'
onClick={() => {
garage.delete();
setCurrentSlot(0);
garage.currentSlot = 0;
logoutRobot();
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 { IconButton, LinearProgress, TextField, Tooltip } from '@mui/material';
import { type Robot } from '../../models';
import { ContentCopy } from '@mui/icons-material';
import { systemClient } from '../../services/System';
import { UseGarageStoreType, GarageContext } from '../../contexts/GarageContext';
interface TokenInputProps {
robot: Robot;
editable?: boolean;
fullWidth?: boolean;
loading?: boolean;
setRobot: (state: Robot) => void;
inputToken: string;
autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
onPressEnter: () => void;
@ -21,11 +19,9 @@ interface TokenInputProps {
}
const TokenInput = ({
robot,
editable = true,
showCopy = true,
label,
setRobot,
fullWidth = true,
onPressEnter,
autoFocusTarget = 'textfield',
@ -35,6 +31,7 @@ const TokenInput = ({
setInputToken,
}: TokenInputProps): JSX.Element => {
const { t } = useTranslation();
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const [showCopied, setShowCopied] = useState<boolean>(false);
useEffect(() => {
@ -46,12 +43,12 @@ const TokenInput = ({
} else {
return (
<TextField
error={inputToken.length > 20 ? !!badToken : false}
error={inputToken.length > 20 ? Boolean(badToken) : false}
disabled={!editable}
required={true}
label={label || undefined}
label={label ?? ''}
value={inputToken}
autoFocus={autoFocusTarget == 'textfield'}
autoFocus={autoFocusTarget === 'textfield'}
fullWidth={fullWidth}
sx={{ borderColor: 'primary' }}
variant={editable ? 'outlined' : 'filled'}
@ -69,17 +66,15 @@ const TokenInput = ({
endAdornment: showCopy ? (
<Tooltip open={showCopied} title={t('Copied!')}>
<IconButton
autoFocus={autoFocusTarget == 'copyButton'}
color={robot.copiedToken ? 'inherit' : 'primary'}
autoFocus={autoFocusTarget === 'copyButton'}
color={garage.getRobot().copiedToken ? 'inherit' : 'primary'}
onClick={() => {
systemClient.copyToClipboard(inputToken);
setShowCopied(true);
setTimeout(() => {
setShowCopied(false);
}, 1000);
setRobot((robot) => {
return { ...robot, copiedToken: true };
});
garage.updateRobot({ copiedToken: true });
}}
>
<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 { Box, Button, Grid, Typography, useTheme } from '@mui/material';
import { RoboSatsTextIcon } from '../../components/Icons';

View File

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

View File

@ -1,10 +1,13 @@
import React, { useContext } from 'react';
import { Grid, Paper } from '@mui/material';
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 { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { windowSize, navbarHeight, settings, setOpen, open, hostUrl } =
useContext<UseAppStoreType>(AppContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
return (
@ -12,7 +15,7 @@ const SettingsPage = (): JSX.Element => {
elevation={12}
sx={{
padding: '0.6em',
width: '21em',
width: '20.5em',
maxHeight: `${maxHeight}em`,
overflow: 'auto',
overflowX: 'clip',
@ -22,6 +25,16 @@ const SettingsPage = (): JSX.Element => {
<Grid item>
<SettingsForm showNetwork={!(window.NativeRobosats === undefined)} />
</Grid>
<Grid item>
<FederationTable
openCoordinator={() => {
setOpen({ ...open, coordinator: true });
}}
baseUrl={hostUrl}
maxHeight={14}
network={settings.network}
/>
</Grid>
</Grid>
</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 {
Typography,
@ -26,7 +26,7 @@ interface BookControlProps {
fav: Favorites;
setFav: (state: Favorites) => void;
paymentMethod: string[];
setPaymentMethods: () => void;
setPaymentMethods: (state: string[]) => void;
}
const BookControl = ({
@ -43,22 +43,22 @@ const BookControl = ({
const typeZeroText = fav.mode === 'fiat' ? t('Buy') : t('Swap In');
const typeOneText = fav.mode === 'fiat' ? t('Sell') : t('Swap Out');
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 large = medium + (t('and use').length + t('pay with').length) * 0.6 + 5;
return [typeZeroText, typeOneText, small, medium, large];
}, [i18n.language, fav.mode]);
const handleCurrencyChange = function (e) {
const currency = e.target.value;
const handleCurrencyChange = function (e: React.ChangeEvent<HTMLInputElement>): void {
const currency = Number(e.target.value);
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 });
};
const handleModeChange = function (mouseEvent, val) {
const handleModeChange = function (mouseEvent: React.MouseEvent, val: number): void {
const mode = fav.mode === 'fiat' ? 'swap' : 'fiat';
const currency = fav.mode === 'fiat' ? 1000 : 0;
setFav({ ...fav, mode, currency });
@ -197,7 +197,7 @@ const BookControl = ({
{width > large ? (
<Grid item sx={{ position: 'relative', top: '0.5em' }}>
<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>
</Grid>
) : null}
@ -218,10 +218,10 @@ const BookControl = ({
listBoxProps={{ sx: { width: '13em' } }}
onAutocompleteChange={setPaymentMethods}
value={paymentMethod}
optionsType={fav.currency == 1000 ? 'swap' : 'fiat'}
optionsType={fav.currency === 1000 ? 'swap' : 'fiat'}
error={false}
helperText={''}
label={fav.currency == 1000 ? t('DESTINATION') : t('METHOD')}
label={fav.currency === 1000 ? t('DESTINATION') : t('METHOD')}
tooltipTitle=''
listHeaderText=''
addNewButtonText=''
@ -247,7 +247,7 @@ const BookControl = ({
label={t('Select Payment Method')}
required={true}
renderValue={(value) =>
value == 'ANY' ? (
value === 'ANY' ? (
<CheckBoxOutlineBlankIcon style={{ position: 'relative', top: '0.1em' }} />
) : (
<div style={{ display: 'flex', alignItems: 'center' }}>
@ -258,9 +258,9 @@ const BookControl = ({
inputProps={{
style: { textAlign: 'center' },
}}
value={paymentMethod[0] ? paymentMethod[0] : 'ANY'}
value={paymentMethod[0] ?? 'ANY'}
onChange={(e) => {
setPaymentMethods(e.target.value == 'ANY' ? [] : [e.target.value]);
setPaymentMethods(e.target.value === 'ANY' ? [] : [e.target.value]);
}}
>
<MenuItem value={'ANY'}>

View File

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

View File

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

View File

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

View File

@ -6,17 +6,24 @@ import { amountToString, statusBadgeColor } from '../../../../utils';
import currencyDict from '../../../../../static/assets/currencies.json';
import { PaymentStringAsIcons } from '../../../PaymentMethods';
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 {
order: PublicOrder;
}
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();
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 }}>
<Grid container justifyContent='space-between'>
<Grid item xs={3}>

View File

@ -22,7 +22,7 @@ interface Props {
onClose: () => void;
}
const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
const AboutDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
const { t } = useTranslation();
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 { ExportIcon, NewTabIcon } from '../Icons';
function CredentialTextfield(props) {
function CredentialTextfield(props): JSX.Element {
return (
<Grid item align='center' xs={12}>
<Tooltip placement='top' enterTouchDelay={200} enterDelay={200} title={props.tooltipTitle}>
@ -58,9 +58,9 @@ interface Props {
onClose: () => void;
orderId: number;
messages: array;
own_pub_key: string;
own_enc_priv_key: string;
peer_pub_key: string;
ownPubKey: string;
ownEncPrivKey: string;
peerPubKey: string;
passphrase: string;
onClickBack: () => void;
}
@ -70,9 +70,9 @@ const AuditPGPDialog = ({
onClose,
orderId,
messages,
own_pub_key,
own_enc_priv_key,
peer_pub_key,
ownPubKey,
ownEncPrivKey,
peerPubKey,
passphrase,
onClickBack,
}: Props): JSX.Element => {
@ -104,7 +104,7 @@ const AuditPGPDialog = ({
'Your PGP public key. Your peer uses it to encrypt messages only you can read.',
)}
label={t('Your public key')}
value={own_pub_key}
value={ownPubKey}
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.',
)}
label={t('Peer public key')}
value={peer_pub_key}
value={peerPubKey}
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.',
)}
label={t('Your encrypted private key')}
value={own_enc_priv_key}
value={ownEncPrivKey}
copiedTitle={t('Copied!')}
/>
@ -149,10 +149,10 @@ const AuditPGPDialog = ({
color='primary'
variant='contained'
onClick={() => {
saveAsJson('keys_' + orderId + '.json', {
own_public_key: own_pub_key,
peer_public_key: peer_pub_key,
encrypted_private_key: own_enc_priv_key,
saveAsJson(`keys_${orderId}.json`, {
own_public_key: ownPubKey,
peer_public_key: peerPubKey,
encrypted_private_key: ownEncPrivKey,
passphrase,
});
}}
@ -181,7 +181,7 @@ const AuditPGPDialog = ({
color='primary'
variant='contained'
onClick={() => {
saveAsJson('messages_' + orderId + '.json', messages);
saveAsJson(`messages_${orderId}.json`, messages);
}}
>
<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>
<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
component='a'
target='_blank'
@ -167,7 +167,7 @@ const CommunityDialog = ({ open = false, onClose }: Props): JSX.Element => {
</IconButton>
</Tooltip>
<Tooltip title={t('Join RoboSats English speaking community!') || ''}>
<Tooltip title={t('Join RoboSats English speaking community!')}>
<IconButton
component='a'
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 theme = useTheme();
const handleClickOpenBrowser = () => {
const handleClickOpenBrowser = (): void => {
window.open(`https://t.me/${tgBotName}?start=${tgToken}`, '_blank').focus();
setOpenEnableTelegram(false);
};
const handleOpenTG = () => {
const handleOpenTG = (): void => {
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 = () => {},
latitude,
longitude,
interactive,
interactive = false,
zoom,
message = '',
}: Props): JSX.Element => {
@ -41,8 +41,8 @@ const F2fMapDialog = ({
const [acceptedTilesWarning, setAcceptedTilesWarning] = useState<boolean>(false);
const [openWarningDialog, setOpenWarningDialog] = useState<boolean>(false);
const onSave = () => {
if (position && position[0] && position[1]) {
const onSave: () => void = () => {
if (position?.[0] && position?.[1]) {
onClose([position[0] + Math.random() * 0.1 - 0.05, position[1] + Math.random() * 0.1 - 0.05]);
}
};
@ -59,7 +59,9 @@ const F2fMapDialog = ({
<Dialog
open={open}
fullWidth
onClose={() => onClose()}
onClose={() => {
onClose();
}}
aria-labelledby='worldmap-dialog-title'
aria-describedby='worldmap-description'
maxWidth={false}
@ -154,15 +156,22 @@ const F2fMapDialog = ({
</Grid>
<Grid item>
{interactive ? (
<Button color='primary' variant='contained' onClick={onSave} disabled={!position}>
<Button
color='primary'
variant='contained'
onClick={onSave}
disabled={position == null}
>
{t('Save')}
</Button>
) : (
<Button
color='primary'
variant='contained'
onClick={() => onClose()}
disabled={!position}
onClick={() => {
onClose();
}}
disabled={position == null}
>
{t('Close')}
</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 { useTheme } from '@mui/material/styles';
import { useNavigate } from 'react-router-dom';
import {
Badge,
Button,
CircularProgress,
Dialog,
DialogContent,
Divider,
FormControlLabel,
Grid,
List,
ListItemAvatar,
ListItemButton,
ListItemText,
ListItem,
ListItemIcon,
Switch,
TextField,
Tooltip,
Typography,
LinearProgress,
} from '@mui/material';
import { EnableTelegramDialog } from '.';
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 { apiClient } from '../../services/api';
import { type Robot } from '../../models';
import { signCleartextMessage } from '../../pgp';
import RobotInfo from '../RobotInfo';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { Coordinator } from '../../models';
interface Props {
open: boolean;
onClose: () => void;
robot: Robot;
setRobot: (state: Robot) => void;
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 navigate = useNavigate();
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;
};
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
handleWebln();
}, []);
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 });
});
};
setLoading(garage.getRobot().loading);
}, [robotUpdatedAt]);
return (
<Dialog
@ -129,7 +43,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
aria-labelledby='profile-title'
aria-describedby='profile-description'
>
<div style={robot.loading ? {} : { display: 'none' }}>
<div style={loading ? {} : { display: 'none' }}>
<LinearProgress />
</div>
<DialogContent>
@ -143,7 +57,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
<ListItem className='profileNickname'>
<ListItemText secondary={t('Your robot')}>
<Typography component='h6' variant='h6'>
{robot.nickname ? (
{garage.getRobot().nickname !== undefined && (
<div style={{ position: 'relative', left: '-7px' }}>
<div
style={{
@ -156,12 +70,12 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
>
<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' }} />
</div>
</div>
) : null}
)}
</Typography>
</ListItemText>
@ -169,219 +83,35 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
<RobotAvatar
avatarClass='profileAvatar'
style={{ width: 65, height: 65 }}
nickname={robot.nickname}
nickname={garage.getRobot().nickname}
baseUrl={baseUrl}
/>
</ListItemAvatar>
</ListItem>
<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>
<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>
</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';
import { systemClient } from '../../services/System';
import ContentCopy from '@mui/icons-material/ContentCopy';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
interface Props {
open: boolean;
@ -24,7 +24,7 @@ interface Props {
}
const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): JSX.Element => {
const { robot } = useContext<UseAppStoreType>(AppContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
return (
@ -43,7 +43,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
sx={{ width: '100%', maxWidth: '550px' }}
disabled
label={t('Back it up!')}
value={robot.token}
value={garage.getRobot().token}
variant='filled'
size='small'
InputProps={{
@ -51,7 +51,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton
onClick={() => {
systemClient.copyToClipboard(robot.token);
systemClient.copyToClipboard(garage.getRobot().token);
}}
>
<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 {
@ -17,24 +17,32 @@ import {
import WebIcon from '@mui/icons-material/Web';
import AndroidIcon from '@mui/icons-material/Android';
import UpcomingIcon from '@mui/icons-material/Upcoming';
import { checkVer } from '../../utils';
import { type Version } from '../../models';
interface Props {
open: boolean;
clientVersion: string;
coordinatorVersion: string;
coordinatorVersion: Version;
clientVersion: Version;
onClose: () => void;
}
const UpdateClientDialog = ({
open = false,
clientVersion,
coordinatorVersion,
onClose,
}: Props): JSX.Element => {
const UpdateDialog = ({ coordinatorVersion, clientVersion }: Props): JSX.Element => {
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 (
<Dialog open={open} onClose={onClose}>
<Dialog
open={open}
onClose={() => {
setOpen(false);
}}
>
<DialogContent>
<Typography component='h5' variant='h5'>
{t('Update your RoboSats client')}
@ -45,7 +53,7 @@ const UpdateClientDialog = ({
<Typography>
{t(
'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>
@ -53,7 +61,7 @@ const UpdateClientDialog = ({
<ListItemButton
component='a'
target='_blank'
href={`https://github.com/RoboSats/robosats/releases/tag/${coordinatorVersion}-alpha`}
href={`https://github.com/RoboSats/robosats/releases/tag/${coordinatorString}-alpha`}
rel='noreferrer'
>
<ListItemIcon>
@ -107,7 +115,13 @@ const UpdateClientDialog = ({
</ListItemButton>
<DialogActions>
<Button onClick={onClose}>{t('Go away!')}</Button>
<Button
onClick={() => {
setOpen(false);
}}
>
{t('Go away!')}
</Button>
</DialogActions>
</List>
</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 CommunityDialog } from './Community';
export { default as InfoDialog } from './Info';
export { default as AboutDialog } from './About';
export { default as LearnDialog } from './Learn';
export { default as NoRobotDialog } from './NoRobot';
export { default as StoreTokenDialog } from './StoreToken';
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 StatsDialog } from './Stats';
export { default as ClientDialog } from './Client';
export { default as EnableTelegramDialog } from './EnableTelegram';
export { default as UpdateClientDialog } from './UpdateClient';
export { default as NoticeDialog } from './Notice';
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);
}
render() {
render(): React.ReactNode {
if (this.state.hasError) {
return (
<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 { 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 { t } = useTranslation();

View File

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

View File

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

View File

@ -1,10 +1,12 @@
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 (
<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' />
</SvgIcon>
);
}
};
export default Bitcoin;

View File

@ -1,10 +1,12 @@
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 (
<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' />
</SvgIcon>
);
}
};
export default BitcoinSign;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g>
@ -134,4 +134,6 @@ export default function BuySatsIcon(props) {
</g>
</SvgIcon>
);
}
};
export default BuySats;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g>
@ -76,4 +76,6 @@ export default function BuySatsCheckedIcon(props) {
</g>
</SvgIcon>
);
}
};
export default BuySatsChecked;

View File

@ -1,11 +1,13 @@
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 (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 810 540'>
<rect width='810' height='540' fill='#FCDD09' />
<path stroke='#DA121A' strokeWidth='60' d='M0,90H810m0,120H0m0,120H810m0,120H0' />
</SvgIcon>
);
}
};
export default CataloniaFlag;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 440.45 440.45'>
<g id='XMLID_34_'>
@ -61,11 +61,13 @@ export default function EarthIcon(props) {
</g>
</g>
<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
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'
/>
</SvgIcon>
);
}
};
export default Earth;

View File

@ -1,10 +1,12 @@
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 (
<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' />
</SvgIcon>
);
}
};
export default Export;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 511.882 511.882'>
<polygon
@ -45,4 +45,6 @@ export default function GoldIcon(props) {
/>
</SvgIcon>
);
}
};
export default Gold;

View File

@ -1,10 +1,12 @@
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 (
<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' />
</SvgIcon>
);
}
};
export default NewTab;

View File

@ -1,11 +1,13 @@
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
export default function NostrIcon(props) {
const Nostr: React.FC<SvgIconProps> = (props) => {
return (
<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' />
</SvgIcon>
);
}
};
export default Nostr;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon {...props} x='0px' y='0px' width='1000px' height='1000px' viewBox='0 0 1000 900'>
<g>
@ -102,4 +102,6 @@ export default function RoboSatsIcon(props) {
</g>
</SvgIcon>
);
}
};
export default RoboSats;

View File

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

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon {...props} x='0px' y='0px' width='2000px' height='500px' viewBox='0 620 2000 1'>
<g>
@ -95,4 +95,6 @@ export default function RoboSatsTextIcon(props) {
</g>
</SvgIcon>
);
}
};
export default RoboSatsText;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g>
@ -135,4 +135,6 @@ export default function SellSatsIcon(props) {
</g>
</SvgIcon>
);
}
};
export default SellSats;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g>
@ -79,4 +79,6 @@ export default function SellSatsCheckedIcon(props) {
</g>
</SvgIcon>
);
}
};
export default SellSatsChecked;

View File

@ -1,7 +1,7 @@
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 (
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
<g>
@ -20,4 +20,6 @@ export default function SendReceiveIcon(props) {
</g>
</SvgIcon>
);
}
};
export default SendReceive;

View File

@ -1,7 +1,7 @@
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 (
<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)'>
@ -18,4 +18,6 @@ export default function SimplexIcon(props) {
</g>
</SvgIcon>
);
}
};
export default Simplex;

View File

@ -1,10 +1,12 @@
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 (
<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>
</SvgIcon>
);
}
};
export default Tor;

View File

@ -1,10 +1,12 @@
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 (
<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' />
</SvgIcon>
);
}
};
export default UserNinja;

View File

@ -1,6 +1,7 @@
export { default as AmbossIcon } from './Amboss';
export { default as BitcoinIcon } from './Bitcoin';
export { default as BitcoinSignIcon } from './BitcoinSign';
export { default as NostrIcon } from './Nostr';
export { default as BuySatsIcon } from './BuySats';
export { default as BuySatsCheckedIcon } from './BuySatsChecked';
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 TorIcon } from './Tor';
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
export { default as FlagWithProps } from './WorldFlags';

View File

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

View File

@ -2,7 +2,16 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import useAutocomplete from '@mui/base/useAutocomplete';
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';
// Icons
@ -20,12 +29,18 @@ const Root = styled('div')(
const Label = styled('label')(
({ theme, error, sx }) => `
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;
position: relative;
left: 1em;
top: ${sx.top};
top: ${String(sx.top) ?? '0.72em'};
maxHeight: 0em;
height: 0em;
white-space: no-wrap;
@ -35,14 +50,20 @@ const Label = styled('label')(
const InputWrapper = styled('div')(
({ theme, error, sx }) => `
min-height: ${sx.minHeight};
max-height: ${sx.maxHeight};
min-height: ${String(sx.minHeight)};
max-height: ${String(sx.maxHeight)};
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'};
border-radius: 4px;
border-color: ${sx.borderColor ? `border-color ${sx.borderColor}` : ''}
border-color: ${sx.borderColor !== undefined ? `border-color ${String(sx.borderColor)}` : ''}
padding: 1px;
display: flex;
flex-wrap: wrap;
@ -52,10 +73,10 @@ const InputWrapper = styled('div')(
&:hover {
border-color: ${
theme.palette.mode === 'dark'
? error
? error === true
? '#f44336'
: sx.hoverBorderColor
: error
: String(sx.hoverBorderColor)
: error === true
? '#dd0000'
: '#2f2f2f'
};
@ -64,10 +85,10 @@ const InputWrapper = styled('div')(
&.focused {
border: 2px solid ${
theme.palette.mode === 'dark'
? error
? error === true
? '#f44336'
: '#90caf9'
: error
: error === true
? '#dd0000'
: '#1976d2'
};
@ -97,7 +118,8 @@ interface TagProps {
onDelete: () => 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 iconSize = 1.5 * theme.typography.fontSize;
return (
@ -117,7 +139,7 @@ const StyledTag = styled(Tag)(
({ theme, sx }) => `
display: flex;
align-items: center;
height: ${sx.height};
height: ${String(sx?.height ?? '2.1em')};
margin: 2px;
line-height: 1.5em;
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')(
({ theme, sx }) => `
width: ${sx != null ? sx.width : '15.6em'};
width: ${String(sx?.width ?? '15.6em')};
margin: 2px 0 0;
padding: 0;
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 {
getRootProps,
@ -220,19 +258,21 @@ export default function AutocompletePayments(props) {
getOptionProps,
groupedOptions,
value,
focused = 'true',
focused = true,
setAnchorEl,
} = useAutocomplete({
fullWidth: true,
id: 'payment-methods',
multiple: true,
value: props.value,
options: props.optionsType == 'fiat' ? fiatMethods : swapMethods,
options: props.optionsType === 'fiat' ? fiatMethods : swapMethods,
getOptionLabel: (option) => option.name,
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: () => {
setVal(() => '');
},
@ -243,36 +283,35 @@ export default function AutocompletePayments(props) {
const theme = useTheme();
const iconSize = 1.5 * theme.typography.fontSize;
function handleAddNew(inputProps) {
function handleAddNew(inputProps: any): void {
fiatMethods.push({ name: inputProps.value, icon: 'custom' });
const a = value.push({ name: inputProps.value, icon: 'custom' });
setVal(() => '');
if (a || a == null) {
if (a !== undefined) {
props.onAutocompleteChange(value);
}
return false;
}
return (
<Root>
<Tooltip
placement='top'
enterTouchDelay={props.tooltipTitle == '' ? 99999 : 300}
enterDelay={props.tooltipTitle == '' ? 99999 : 700}
enterTouchDelay={props.tooltipTitle === '' ? 99999 : 300}
enterDelay={props.tooltipTitle === '' ? 99999 : 700}
enterNextDelay={2000}
title={props.tooltipTitle}
>
<div {...getRootProps()}>
<Fade
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' }}>
<Label
{...getInputLabelProps()}
sx={{ top: '0.72em', ...(props.labelProps ? props.labelProps.sx : {}) }}
error={props.error ? 'error' : null}
sx={{ top: '0.72em', ...(props.labelProps?.sx ?? {}) }}
error={Boolean(props.error)}
>
{props.label}
</Label>
@ -280,7 +319,7 @@ export default function AutocompletePayments(props) {
</Fade>
<InputWrapper
ref={setAnchorEl}
error={props.error ? 'error' : null}
error={Boolean(props.error)}
className={focused ? 'focused' : ''}
sx={{
minHeight: '2.9em',
@ -291,10 +330,11 @@ export default function AutocompletePayments(props) {
>
{value.map((option, index) => (
<StyledTag
key={index}
label={t(option.name)}
icon={option.icon}
sx={{ height: '2.1em', ...(props.tagProps ? props.tagProps.sx : {}) }}
onClick={props.onClick}
sx={{ height: '2.1em', ...(props.tagProps ?? {}) }}
{...getTagProps({ index })}
/>
))}
@ -303,7 +343,7 @@ export default function AutocompletePayments(props) {
</div>
</Tooltip>
<Grow in={fewerOptions.length > 0}>
<Listbox sx={props.listBoxProps ? props.listBoxProps.sx : undefined} {...getListboxProps()}>
<Listbox sx={props.listBoxProps?.sx ?? undefined} {...getListboxProps()}>
{!props.isFilter ? (
<div
style={{
@ -341,7 +381,13 @@ export default function AutocompletePayments(props) {
))}
{val != null || !props.isFilter ? (
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' }} />
{props.addNewButtonText}
</Button>
@ -353,7 +399,12 @@ export default function AutocompletePayments(props) {
{/* Here goes what happens if there is no fewerOptions */}
<Grow in={getInputProps().value.length > 0 && !props.isFilter && fewerOptions.length === 0}>
<Listbox {...getListboxProps()}>
<Button fullWidth={true} onClick={() => handleAddNew(getInputProps())}>
<Button
fullWidth={true}
onClick={() => {
handleAddNew(getInputProps());
}}
>
<DashboardCustomizeIcon sx={{ width: '1.28em', height: '1.28em' }} />
{props.addNewButtonText}
</Button>
@ -361,4 +412,6 @@ export default function AutocompletePayments(props) {
</Grow>
</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 { LoadingButton } from '@mui/lab';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
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 {
disableRequest?: boolean;
@ -50,7 +53,7 @@ interface MakerFormProps {
onSubmit?: () => void;
onReset?: () => void;
submitButtonLabel?: string;
onOrderCreated?: (id: number) => void;
onOrderCreated?: (shortAlias: string, id: number) => void;
onClickGenerateRobot?: () => void;
}
@ -64,8 +67,10 @@ const MakerForm = ({
onOrderCreated = () => null,
onClickGenerateRobot = () => null,
}: MakerFormProps): JSX.Element => {
const { fav, setFav, limits, fetchLimits, info, maker, setMaker, baseUrl, robot } =
useContext<UseAppStoreType>(AppContext);
const { fav, setFav, settings, hostUrl, origin } = useContext<UseAppStoreType>(AppContext);
const { federation, focusedCoordinator, coordinatorUpdatedAt, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { maker, setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const theme = useTheme();
@ -80,30 +85,31 @@ const MakerForm = ({
const [openWorldmap, setOpenWorldmap] = useState<boolean>(false);
const [submittingRequest, setSubmittingRequest] = useState<boolean>(false);
const [amountRangeEnabled, setAmountRangeEnabled] = useState<boolean>(true);
const [limits, setLimits] = useState<LimitList>({});
const maxRangeAmountMultiple = 14.8;
const minRangeAmountMultiple = 1.6;
const amountSafeThresholds = [1.03, 0.98];
useEffect(() => {
setCurrencyCode(currencyDict[fav.currency == 0 ? 1 : fav.currency]);
if (Object.keys(limits.list).length === 0) {
fetchLimits().then((data) => {
updateAmountLimits(data, fav.currency, maker.premium);
updateCurrentPrice(data, fav.currency, maker.premium);
updateSatoshisLimits(data);
});
} else {
updateAmountLimits(limits.list, fav.currency, maker.premium);
updateCurrentPrice(limits.list, fav.currency, maker.premium);
updateSatoshisLimits(limits.list);
fetchLimits();
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
if (focusedCoordinator) {
const newLimits = federation.getCoordinator(focusedCoordinator).limits;
if (Object.keys(newLimits).length !== 0) {
updateAmountLimits(newLimits, fav.currency, maker.premium);
updateCurrentPrice(newLimits, fav.currency, maker.premium);
updateSatoshisLimits(newLimits);
setLimits(newLimits);
}
}, []);
}
}, [coordinatorUpdatedAt]);
const updateAmountLimits = function (limitList: LimitList, currency: number, premium: number) {
const index = currency == 0 ? 1 : currency;
const updateAmountLimits = function (
limitList: LimitList,
currency: number,
premium: number,
): void {
const index = currency === 0 ? 1 : currency;
let minAmountLimit: number = limitList[index].min_amount * (1 + premium / 100);
let maxAmountLimit: number = limitList[index].max_amount * (1 + premium / 100);
@ -113,24 +119,28 @@ const MakerForm = ({
setAmountLimits([minAmountLimit, maxAmountLimit]);
};
const updateSatoshisLimits = function (limitList: LimitList) {
const updateSatoshisLimits = function (limitList: LimitList): void {
const minAmount: number = limitList[1000].min_amount * 100000000;
const maxAmount: number = limitList[1000].max_amount * 100000000;
setSatoshisLimits([minAmount, maxAmount]);
};
const updateCurrentPrice = function (limitsList: LimitList, currency: number, premium: number) {
const index = currency == 0 ? 1 : currency;
const updateCurrentPrice = function (
limitsList: LimitList,
currency: number,
premium: number,
): void {
const index = currency === 0 ? 1 : currency;
let price = '...';
if (maker.isExplicit && maker.amount > 0 && maker.satoshis > 0) {
price = maker.amount / (maker.satoshis / 100000000);
} else if (!maker.is_explicit) {
} else if (!maker.isExplicit) {
price = limitsList[index].price * (1 + premium / 100);
}
setCurrentPrice(parseFloat(Number(price).toPrecision(5)));
};
const handleCurrencyChange = function (newCurrency: number) {
const handleCurrencyChange = function (newCurrency: number): void {
const currencyCode: string = currencyDict[newCurrency];
setCurrencyCode(currencyCode);
setFav({
@ -138,12 +148,12 @@ const MakerForm = ({
currency: newCurrency,
mode: newCurrency === 1000 ? 'swap' : 'fiat',
});
updateAmountLimits(limits.list, newCurrency, maker.premium);
updateCurrentPrice(limits.list, newCurrency, maker.premium);
updateAmountLimits(limits, newCurrency, maker.premium);
updateCurrentPrice(limits, newCurrency, maker.premium);
if (makerHasAmountRange) {
const minAmount = parseFloat(Number(limits.list[newCurrency].min_amount).toPrecision(2));
const maxAmount = parseFloat(Number(limits.list[newCurrency].max_amount).toPrecision(2));
const minAmount = parseFloat(Number(limits[newCurrency].min_amount).toPrecision(2));
const maxAmount = parseFloat(Number(limits[newCurrency].max_amount).toPrecision(2));
if (
parseFloat(maker.minAmount) < minAmount ||
parseFloat(maker.minAmount) > maxAmount ||
@ -163,10 +173,12 @@ const MakerForm = ({
return maker.advancedOptions && amountRangeEnabled;
}, [maker.advancedOptions, amountRangeEnabled]);
const handlePaymentMethodChange = function (paymentArray: { name: string; icon: string }[]) {
let includeCoordinates = false;
const handlePaymentMethodChange = function (
paymentArray: Array<{ name: string; icon: string }>,
): void {
let str = '';
const arrayLength = paymentArray.length;
let includeCoordinates = false;
for (let i = 0; i < arrayLength; i++) {
str += paymentArray[i].name + ' ';
@ -190,14 +202,18 @@ const MakerForm = ({
});
};
const handleMinAmountChange = function (e) {
const handleMinAmountChange = function (
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
): void {
setMaker({
...maker,
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({
...maker,
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> =
function ({ target: { value } }) {
function ({ target: { value } }): void {
const max = fav.mode === 'fiat' ? 999 : 99;
const min = -100;
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 });
premium = -99.99;
}
updateCurrentPrice(limits.list, fav.currency, premium);
updateAmountLimits(limits.list, fav.currency, premium);
updateCurrentPrice(limits, fav.currency, premium);
updateAmountLimits(limits, fav.currency, premium);
setMaker({
...maker,
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;
let badSatoshisText: string = '';
let satoshis: string = newSatoshis;
@ -247,14 +263,14 @@ const MakerForm = ({
});
};
const handleClickRelative = function () {
const handleClickRelative = function (): void {
setMaker({
...maker,
isExplicit: false,
});
};
const handleClickExplicit = function () {
const handleClickExplicit = function (): void {
if (!maker.advancedOptions) {
setMaker({
...maker,
@ -263,12 +279,24 @@ const MakerForm = ({
}
};
const handleCreateOrder = function () {
if (!disableRequest) {
const handleCreateOrder = function (): void {
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);
const body = {
type: fav.type == 0 ? 1 : 0,
currency: fav.currency == 0 ? 1 : fav.currency,
type: fav.type === 0 ? 1 : 0,
currency: fav.currency === 0 ? 1 : fav.currency,
amount: makerHasAmountRange ? null : maker.amount,
has_range: makerHasAmountRange,
min_amount: makerHasAmountRange ? maker.minAmount : null,
@ -276,7 +304,7 @@ const MakerForm = ({
payment_method:
maker.paymentMethodsText === '' ? 'not specified' : maker.paymentMethodsText,
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,
public_duration: maker.publicDuration,
escrow_duration: maker.escrowDuration,
@ -284,48 +312,54 @@ const MakerForm = ({
latitude: maker.latitude,
longitude: maker.longitude,
};
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient
.post(baseUrl, '/api/make/', body, { tokenSHA256: robot.tokenSHA256 })
.then((data: object) => {
.post(url, `${basePath}/api/make/`, body, auth)
.then((data: any) => {
setBadRequest(data.bad_request);
if (data.id) {
onOrderCreated(data.id);
if (data.id !== undefined) {
onOrderCreated(maker.coordinator, data.id);
}
setSubmittingRequest(false);
})
.catch(() => {
setBadRequest('Request error');
});
}
setOpenDialogs(false);
};
const handleChangePublicDuration = function (date: Date) {
const handleChangePublicDuration = function (date: Date): void {
const d = new Date(date);
const hours: number = d.getHours();
const minutes: number = d.getMinutes();
const total_secs: number = hours * 60 * 60 + minutes * 60;
const totalSecs: number = hours * 60 * 60 + minutes * 60;
setMaker({
...maker,
publicExpiryTime: date,
publicDuration: total_secs,
publicDuration: totalSecs,
});
};
const handleChangeEscrowDuration = function (date: Date) {
const handleChangeEscrowDuration = function (date: Date): void {
const d = new Date(date);
const hours: number = d.getHours();
const minutes: number = d.getMinutes();
const total_secs: number = hours * 60 * 60 + minutes * 60;
const totalSecs: number = hours * 60 * 60 + minutes * 60;
setMaker({
...maker,
escrowExpiryTime: date,
escrowDuration: total_secs,
escrowDuration: totalSecs,
});
};
const handleClickAdvanced = function () {
const handleClickAdvanced = function (): void {
if (maker.advancedOptions) {
handleClickRelative();
setMaker({ ...maker, advancedOptions: false });
@ -352,14 +386,16 @@ const MakerForm = ({
);
}, [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 minAmount = maker.amount
const minAmount =
maker.amount !== ''
? parseFloat((maker.amount / 2).toPrecision(2))
: parseFloat(Number(limits.list[index].max_amount * 0.25).toPrecision(2));
const maxAmount = maker.amount
: parseFloat(Number(limits[index].max_amount * 0.25).toPrecision(2));
const maxAmount =
maker.amount !== ''
? parseFloat(maker.amount)
: parseFloat(Number(limits.list[index].max_amount * 0.75).toPrecision(2));
: parseFloat(Number(limits[index].max_amount * 0.75).toPrecision(2));
setMaker({
...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 maxAmount = e.target.value[1];
@ -406,11 +442,14 @@ const MakerForm = ({
const handleClickAmountRangeEnabled = function (
_e: React.ChangeEvent<HTMLInputElement>,
checked: boolean,
) {
): void {
setAmountRangeEnabled(checked);
};
const amountLabel = useMemo(() => {
if (!focusedCoordinator) return;
const info = federation.getCoordinator(focusedCoordinator)?.info;
const defaultRoutingBudget = 0.001;
let label = t('Amount');
let helper = '';
@ -420,7 +459,7 @@ const MakerForm = ({
swapSats = computeSats({
amount: Number(maker.amount),
premium: Number(maker.premium),
fee: -info.maker_fee,
fee: -(info?.maker_fee ?? 0),
routingBudget: defaultRoutingBudget,
});
label = t('Onchain amount to send (BTC)');
@ -431,7 +470,7 @@ const MakerForm = ({
swapSats = computeSats({
amount: Number(maker.amount),
premium: Number(maker.premium),
fee: info.maker_fee,
fee: info?.maker_fee ?? 0,
});
label = t('Onchain amount to receive (BTC)');
helper = t('You send approx {{swapSats}} LN Sats (fees might vary)', {
@ -440,30 +479,30 @@ const MakerForm = ({
}
}
return { label, helper, swapSats };
}, [fav, maker.amount, maker.premium, info]);
}, [fav, maker.amount, maker.premium, federationUpdatedAt]);
const disableSubmit = useMemo(() => {
return (
fav.type == null ||
(!makerHasAmountRange &&
maker.amount != '' &&
maker.amount !== '' &&
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1])) ||
maker.badPaymentMethod ||
(maker.amount == null && (!makerHasAmountRange || limits.loading)) ||
(maker.amount == null && (!makerHasAmountRange || Object.keys(limits).lenght < 1)) ||
(makerHasAmountRange && (minAmountError || maxAmountError)) ||
(!makerHasAmountRange && maker.amount <= 0) ||
(maker.isExplicit && (maker.badSatoshisText != '' || maker.satoshis == '')) ||
(!maker.isExplicit && maker.badPremiumText != '')
(maker.isExplicit && (maker.badSatoshisText !== '' || maker.satoshis === '')) ||
(!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 });
setMaker(defaultMaker);
};
const handleAddLocation = (pos: [number, number]) => {
if (pos && pos.length === 2) {
const handleAddLocation = (pos: [number, number]): void => {
if (pos?.length === 2) {
setMaker((maker) => {
return {
...maker,
@ -471,10 +510,11 @@ const MakerForm = ({
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 cash = fiatMethods.find((method) => method.icon === 'cash');
if (cash) {
if (cash !== null) {
newMethods.unshift(cash);
handlePaymentMethodChange(newMethods);
}
@ -482,7 +522,7 @@ const MakerForm = ({
}
};
const SummaryText = function () {
const SummaryText = (): JSX.Element => {
return (
<Typography
component='h2'
@ -494,7 +534,7 @@ const MakerForm = ({
? fav.mode === 'fiat'
? t('Order for ')
: t('Swap of ')
: fav.type == 1
: fav.type === 1
? fav.mode === 'fiat'
? t('Buy BTC for ')
: t('Swap into LN ')
@ -512,7 +552,7 @@ const MakerForm = ({
{' ' + (fav.mode === 'fiat' ? currencyCode : 'Sats')}
{maker.isExplicit
? t(' of {{satoshis}} Satoshis', { satoshis: pn(maker.satoshis) })
: maker.premium == 0
: maker.premium === 0
? fav.mode === 'fiat'
? t(' at market price')
: ''
@ -531,7 +571,7 @@ const MakerForm = ({
setOpenDialogs(false);
}}
onClickDone={handleCreateOrder}
hasRobot={robot.avatarLoaded}
hasRobot={garage.getRobot().avatarLoaded}
onClickGenerateRobot={onClickGenerateRobot}
/>
<F2fMapDialog
@ -542,19 +582,19 @@ const MakerForm = ({
message={t(
'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]) => {
if (pos) handleAddLocation(pos);
if (pos != null) handleAddLocation(pos);
setOpenWorldmap(false);
}}
zoom={maker.latitude && maker.longitude ? 6 : undefined}
/>
<Collapse in={limits.list.length == 0}>
<div style={{ display: limits.list.length == 0 ? '' : 'none' }}>
<Collapse in={Object.keys(limits).lenght === 0}>
<div style={{ display: Object.keys(limits) === 0 ? '' : 'none' }}>
<LinearProgress />
</div>
</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 item>
<IconButton
@ -590,7 +630,7 @@ const MakerForm = ({
>
<Switch
size='small'
disabled={limits.list.length == 0}
disabled={Object.keys(limits).length === 0}
checked={maker.advancedOptions}
onChange={handleClickAdvanced}
/>
@ -611,9 +651,9 @@ const MakerForm = ({
<FormHelperText sx={{ textAlign: 'center' }}>{t('Swap?')}</FormHelperText>
<Checkbox
sx={{ position: 'relative', bottom: '0.3em' }}
checked={fav.mode == 'swap'}
checked={fav.mode === 'swap'}
onClick={() => {
handleCurrencyChange(fav.mode == 'swap' ? 1 : 1000);
handleCurrencyChange(fav.mode === 'swap' ? 1 : 1000);
}}
/>
</FormControl>
@ -636,10 +676,10 @@ const MakerForm = ({
type: 1,
});
}}
disableElevation={fav.type == 1}
disableElevation={fav.type === 1}
sx={{
backgroundColor: fav.type == 1 ? 'primary.main' : 'background.paper',
color: fav.type == 1 ? 'background.paper' : 'text.secondary',
backgroundColor: fav.type === 1 ? 'primary.main' : 'background.paper',
color: fav.type === 1 ? 'background.paper' : 'text.secondary',
':hover': {
color: 'background.paper',
},
@ -656,11 +696,11 @@ const MakerForm = ({
type: 0,
});
}}
disableElevation={fav.type == 0}
disableElevation={fav.type === 0}
color='secondary'
sx={{
backgroundColor: fav.type == 0 ? 'secondary.main' : 'background.paper',
color: fav.type == 0 ? 'background.secondary' : 'text.secondary',
backgroundColor: fav.type === 0 ? 'secondary.main' : 'background.paper',
color: fav.type === 0 ? 'background.secondary' : 'text.secondary',
':hover': {
color: 'background.paper',
},
@ -730,15 +770,15 @@ const MakerForm = ({
disabled={makerHasAmountRange}
variant={makerHasAmountRange ? 'filled' : 'outlined'}
error={
maker.amount != '' &&
maker.amount !== '' &&
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1])
}
helperText={
maker.amount < amountLimits[0] && maker.amount != ''
maker.amount < amountLimits[0] && maker.amount !== ''
? t('Must be more than {{minAmount}}', {
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}}', {
maxAmount: pn(parseFloat(amountLimits[1].toPrecision(2))),
})
@ -761,7 +801,7 @@ const MakerForm = ({
}}
/>
</Tooltip>
{fav.mode === 'swap' && maker.amount != '' ? (
{fav.mode === 'swap' && maker.amount !== '' ? (
<FormHelperText sx={{ textAlign: 'center' }}>
{amountLabel.helper}
</FormHelperText>
@ -780,7 +820,7 @@ const MakerForm = ({
inputProps={{
style: { textAlign: 'center' },
}}
value={fav.currency == 0 ? 1 : fav.currency}
value={fav.currency === 0 ? 1 : fav.currency}
onChange={(e) => {
handleCurrencyChange(e.target.value);
}}
@ -806,13 +846,15 @@ const MakerForm = ({
<Grid item xs={12}>
<AutocompletePayments
onAutocompleteChange={handlePaymentMethodChange}
onClick={() => setOpenWorldmap(true)}
onClick={() => {
setOpenWorldmap(true);
}}
optionsType={fav.mode}
error={maker.badPaymentMethod}
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(
fav.mode == 'swap'
fav.mode === 'swap'
? t('Enter the destination of the Lightning swap')
: 'Enter your preferred fiat payment methods. Fast methods are highly recommended.',
)}
@ -844,7 +886,9 @@ const MakerForm = ({
color: theme.palette.text.secondary,
borderColor: theme.palette.text.disabled,
}}
onClick={() => setOpenWorldmap(true)}
onClick={() => {
setOpenWorldmap(true);
}}
>
{t('Face to Face Location')}
<Map style={{ paddingLeft: 5 }} />
@ -913,7 +957,7 @@ const MakerForm = ({
<TextField
fullWidth
label={t('Satoshis')}
error={maker.badSatoshisText != ''}
error={maker.badSatoshisText !== ''}
helperText={maker.badSatoshisText === '' ? null : maker.badSatoshisText}
type='number'
required={true}
@ -933,7 +977,7 @@ const MakerForm = ({
<div style={{ display: maker.isExplicit ? 'none' : '' }}>
<TextField
fullWidth
error={maker.badPremiumText != ''}
error={maker.badPremiumText !== ''}
helperText={maker.badPremiumText === '' ? null : maker.badPremiumText}
label={t('Premium over Market (%)')}
type='number'
@ -1096,6 +1140,15 @@ const MakerForm = ({
</Grid>
</Collapse>
<SelectCoordinator
coordinator={maker.coordinator}
setCoordinator={(coordinator) => {
setMaker((maker) => {
return { ...maker, coordinator };
});
}}
/>
<Grid container direction='column' alignItems='center'>
<Grid item>
<SummaryText />
@ -1152,7 +1205,7 @@ const MakerForm = ({
</Typography>
</Grid>
<Collapse in={!(limits.list.length == 0)}>
<Collapse in={!(Object.keys(limits).length === 0)}>
<Tooltip
placement='top'
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 { MapContainer, GeoJSON, useMapEvents, TileLayer, Tooltip, Marker } from 'react-leaflet';
import { useTheme, LinearProgress } from '@mui/material';
import { UseAppStoreType, AppContext } from '../../contexts/AppContext';
import { GeoJsonObject } from 'geojson';
import { DivIcon, LeafletMouseEvent } from 'leaflet';
import { PublicOrder } from '../../models';
import { type GeoJsonObject } from 'geojson';
import { DivIcon, type LeafletMouseEvent } from 'leaflet';
import { type PublicOrder } from '../../models';
import OrderTooltip from '../Charts/helpers/OrderTooltip';
import getWorldmapGeojson from '../../geo/Web';
import MarkerClusterGroup from '@christopherpickering/react-leaflet-markercluster';
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
interface MapPinProps {
fillColor: string;
@ -17,7 +17,6 @@ interface MapPinProps {
}
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>`;
};
@ -45,13 +44,17 @@ const Map = ({
interactive = false,
}: Props): JSX.Element => {
const theme = useTheme();
const { baseUrl } = useContext<UseAppStoreType>(AppContext);
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
const [worldmap, setWorldmap] = useState<GeoJsonObject | undefined>();
useEffect(() => {
if (!worldmap) {
getWorldmapGeojson(apiClient, baseUrl).then(setWorldmap);
}
getWorldmapGeojson(apiClient, hostUrl)
.then((data) => {
setWorldmap(data);
})
.catch((error) => {
console.error('Error:', error);
});
}, []);
const RobotMarker = (
@ -59,8 +62,8 @@ const Map = ({
position: [number, number],
orderType: number,
order?: PublicOrder,
) => {
const fillColor = orderType == 1 ? theme.palette.primary.main : theme.palette.secondary.main;
): JSX.Element => {
const fillColor = orderType === 1 ? theme.palette.primary.main : theme.palette.secondary.main;
const outlineColor = 'black';
const eyesColor = 'white';
@ -77,10 +80,12 @@ const Map = ({
})
}
eventHandlers={{
click: (_event: LeafletMouseEvent) => order?.id && onOrderClicked(order.id),
click: (_event: LeafletMouseEvent) => {
order?.id && onOrderClicked(order.id);
},
}}
>
{order && (
{order != null && (
<Tooltip direction='top'>
<OrderTooltip order={order} />
</Tooltip>
@ -89,7 +94,7 @@ const Map = ({
);
};
const LocationMarker = () => {
const LocationMarker = (): JSX.Element => {
useMapEvents({
click(event: LeafletMouseEvent) {
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 <></>;
return (
<MarkerClusterGroup showCoverageOnHover={true} disableClusteringAtZoom={14}>
{orders.map((order) => {
if (!order.latitude || !order.longitude) return <></>;
return RobotMarker(order.id, [order.latitude, order.longitude], order.type || 0, order);
if (!order?.latitude || !order?.longitude) return <></>;
return RobotMarker(order.id, [order.latitude, order.longitude], order.type ?? 0, order);
})}
</MarkerClusterGroup>
);
@ -117,12 +122,12 @@ const Map = ({
<MapContainer
maxZoom={15}
center={center ?? [0, 0]}
zoom={zoom ? zoom : 2}
zoom={zoom ?? 2}
attributionControl={false}
style={{ height: '100%', width: '100%', backgroundColor: theme.palette.background.paper }}
>
{!useTiles && !worldmap && <LinearProgress />}
{!useTiles && worldmap && (
{!useTiles && worldmap == null && <LinearProgress />}
{!useTiles && worldmap != null && (
<GeoJSON
data={worldmap}
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 {
Tooltip,
Alert,
useTheme,
IconButton,
type TooltipProps,
styled,
tooltipClasses,
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { type Order } from '../../models';
import Close from '@mui/icons-material/Close';
import { type Page } from '../../basic/NavBar';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
interface NotificationsProps {
order: Order | undefined;
rewards: number | undefined;
page: Page;
openProfile: () => void;
@ -64,7 +62,6 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
}));
const Notifications = ({
order,
rewards,
page,
windowWidth,
@ -72,6 +69,7 @@ const Notifications = ({
}: NotificationsProps): JSX.Element => {
const { t } = useTranslation();
const navigate = useNavigate();
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const [message, setMessage] = useState<NotificationMessage>(emptyNotificationMessage);
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 basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange');
const moveToOrderPage = function () {
navigate(`/order/${order?.id}`);
const moveToOrderPage = function (): void {
navigate(`/order/${String(garage.getOrder()?.id)}`);
setShow(false);
};
@ -108,7 +106,7 @@ const Notifications = ({
const Messages: MessagesProps = {
bondLocked: {
title: t(`${order?.is_maker ? 'Maker' : 'Taker'} bond locked`),
title: t(`${garage.getOrder()?.is_maker === true ? 'Maker' : 'Taker'} bond locked`),
severity: 'info',
onClick: moveToOrderPage,
sound: audio.ding,
@ -208,28 +206,32 @@ const Notifications = ({
},
};
const notify = function (message: NotificationMessage) {
if (message.title != '') {
const notify = function (message: NotificationMessage): void {
if (message.title !== '') {
setMessage(message);
setShow(true);
setTimeout(() => {
setShow(false);
}, message.timeout);
if (message.sound != null) {
message.sound.play();
void message.sound.play();
}
if (!inFocus) {
setTitleAnimation(
setInterval(function () {
const title = document.title;
document.title = title == basePageTitle ? message.pageTitle : basePageTitle;
document.title = title === basePageTitle ? message.pageTitle : basePageTitle;
}, 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;
// Order status descriptions:
@ -252,37 +254,35 @@ const Notifications = ({
// 17: 'Maker lost dispute'
// 18: 'Taker lost dispute'
if (status == 5 && oldStatus != 5) {
if (status === 5 && oldStatus !== 5) {
message = Messages.expired;
} else if (oldStatus == undefined) {
} else if (oldStatus === undefined) {
message = emptyNotificationMessage;
} else if (order?.is_maker && status > 0 && oldStatus == 0) {
} else if (order.is_maker && status > 0 && oldStatus === 0) {
message = Messages.bondLocked;
} else if (order?.is_taker && status > 5 && oldStatus <= 5) {
} else if (order.is_taker && status > 5 && oldStatus <= 5) {
message = Messages.bondLocked;
} else if (order?.is_maker && status > 5 && oldStatus <= 5) {
} else if (order.is_maker && status > 5 && oldStatus <= 5) {
message = Messages.taken;
} else if (order?.is_seller && status > 7 && oldStatus < 7) {
} else if (order.is_seller && status > 7 && oldStatus < 7) {
message = Messages.escrowLocked;
} else if ([9, 10].includes(status) && oldStatus < 9) {
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;
} else if (order?.is_buyer && status == 14 && oldStatus != 14) {
} else if (order.is_buyer && status === 14 && oldStatus !== 14) {
message = Messages.successful;
} else if (order?.is_buyer && status == 15 && oldStatus < 14) {
} else if (order.is_buyer && status === 15 && oldStatus < 14) {
message = Messages.routingFailed;
} else if (status == 11 && oldStatus < 11) {
message = Messages.dispute;
} else if (status == 11 && oldStatus < 11) {
} else if (status === 11 && oldStatus < 11) {
message = Messages.dispute;
} else if (
((order?.is_maker && status == 18) || (order?.is_taker && status == 17)) &&
((order.is_maker && status === 18) || (order.is_taker && status === 17)) &&
oldStatus < 17
) {
message = Messages.disputeWinner;
} else if (
((order?.is_maker && status == 17) || (order?.is_taker && status == 18)) &&
((order.is_maker && status === 17) || (order.is_taker && status === 18)) &&
oldStatus < 17
) {
message = Messages.disputeLoser;
@ -293,20 +293,23 @@ const Notifications = ({
// Notify on order status change
useEffect(() => {
if (order != undefined && order.status != oldOrderStatus) {
const order = garage.getOrder();
if (order !== null) {
if (order.status !== oldOrderStatus) {
handleStatusChange(oldOrderStatus, order.status);
setOldOrderStatus(order.status);
} else if (order != undefined && order.chat_last_index > oldChatIndex) {
if (page != 'order') {
} else if (order.chat_last_index > oldChatIndex) {
if (page !== 'order') {
notify(Messages.chatMessage);
}
setOldChatIndex(order.chat_last_index);
}
}, [order]);
}
}, [orderUpdatedAt]);
// Notify on rewards change
useEffect(() => {
if (rewards != undefined) {
if (rewards !== undefined) {
if (rewards > oldRewards) {
notify(Messages.rewards);
}
@ -316,7 +319,7 @@ const Notifications = ({
// Set blinking page title and clear on visibility change > infocus
useEffect(() => {
if (titleAnimation != undefined && inFocus) {
if (titleAnimation !== undefined && inFocus) {
clearInterval(titleAnimation);
}
}, [inFocus]);

View File

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

View File

@ -25,13 +25,13 @@ import { type Order, type Info } from '../../models';
import { ConfirmationDialog } from '../Dialogs';
import { LoadingButton } from '@mui/lab';
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 {
order: Order;
setOrder: (state: Order) => void;
baseUrl: string;
info: Info;
info?: Info;
onClickGenerateRobot?: () => void;
}
@ -42,15 +42,15 @@ interface OpenDialogsProps {
const closeAll = { inactiveMaker: false, confirmation: false };
const TakeButton = ({
order,
setOrder,
baseUrl,
info,
onClickGenerateRobot = () => null,
}: TakeButtonProps): JSX.Element => {
const { t } = useTranslation();
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 [badRequest, setBadRequest] = useState<string>('');
@ -58,11 +58,15 @@ const TakeButton = ({
const [open, setOpen] = useState<OpenDialogsProps>(closeAll);
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 defaultRoutingBudget = 0.001;
const btc_now = order.satoshis_now / 100000000;
const rate = order.amount ? order.amount / btc_now : order.max_amount / btc_now;
const btcNow = order.satoshis_now / 100000000;
const rate = order.amount != null ? order.amount / btcNow : order.max_amount / btcNow;
const amount = order.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
const satoshis = computeSats({
amount,
@ -74,12 +78,13 @@ const TakeButton = ({
};
useEffect(() => {
setSatoshis(satoshisNow());
}, [order.satoshis_now, takeAmount, info]);
setSatoshis(satoshisNow() ?? '');
}, [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 (
<Dialog
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) {
return takeOrderButton();
} else {
@ -132,8 +145,8 @@ const TakeButton = ({
}
};
const handleTakeAmountChange = function (e) {
if (e.target.value != '' && e.target.value != null) {
const handleTakeAmountChange = function (e: React.ChangeEvent<HTMLInputElement>): void {
if (e.target.value !== '' && e.target.value != null) {
setTakeAmount(`${parseFloat(e.target.value)}`);
} else {
setTakeAmount(e.target.value);
@ -141,18 +154,22 @@ const TakeButton = ({
};
const amountHelperText = useMemo(() => {
const amount = order.currency == 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
if (amount < Number(order.min_amount) && takeAmount != '') {
const order = garage.getOrder();
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');
} else if (amount > Number(order.max_amount) && takeAmount != '') {
} else if (amount > Number(order.max_amount) && takeAmount !== '') {
return t('Too high');
} else {
return null;
}
}, [order, takeAmount]);
}, [orderUpdatedAt, takeAmount]);
const onTakeOrderClicked = function () {
if (order.maker_status == 'Inactive') {
const onTakeOrderClicked = function (): void {
if (garage.getOrder()?.maker_status === 'Inactive') {
setOpen({ inactiveMaker: true, confirmation: false });
} else {
setOpen({ inactiveMaker: false, confirmation: true });
@ -160,17 +177,18 @@ const TakeButton = ({
};
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 (
amount < Number(order.min_amount) ||
amount > Number(order.max_amount) ||
takeAmount == '' ||
amount < Number(order?.min_amount) ||
amount > Number(order?.max_amount) ||
takeAmount === '' ||
takeAmount == null
);
}, [takeAmount, order]);
}, [takeAmount, orderUpdatedAt]);
const takeOrderButton = function () {
if (order.has_range) {
const takeOrderButton = function (): JSX.Element {
if (garage.getOrder()?.has_range) {
return (
<Box
sx={{
@ -209,8 +227,8 @@ const TakeButton = ({
required={true}
value={takeAmount}
inputProps={{
min: order.min_amount,
max: order.max_amount,
min: garage.getOrder()?.min_amount,
max: garage.getOrder()?.max_amount,
style: { textAlign: 'center' },
}}
onChange={handleTakeAmountChange}
@ -260,10 +278,10 @@ const TakeButton = ({
</div>
</Grid>
</Grid>
{satoshis != '0' && satoshis != '' && !invalidTakeAmount ? (
{satoshis !== '0' && satoshis !== '' && !invalidTakeAmount ? (
<Grid item>
<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 send {{satoshis}} Sats (Approx)', { satoshis })}
</FormHelperText>
@ -296,33 +314,44 @@ const TakeButton = ({
}
};
const takeOrder = function () {
const takeOrder = function (): void {
if (!focusedCoordinator) return;
setLoadingTake(true);
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient
.post(
baseUrl,
'/api/order/?order_id=' + order.id,
url,
`/api/order/?order_id=${String(garage.getOrder()?.id)}`,
{
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) => {
setLoadingTake(false);
if (data.bad_request) {
if (data?.bad_request !== undefined) {
setBadRequest(data.bad_request);
} else {
setOrder(data);
garage.updateOrder(data as Order);
setBadRequest('');
}
})
.catch(() => {
setBadRequest('Request error');
});
};
return (
<Box>
<Countdown date={new Date(order.penalty)} renderer={countdownTakeOrderRenderer} />
{badRequest != '' ? (
<Countdown
date={new Date(garage.getOrder()?.penalty ?? '')}
renderer={countdownTakeOrderRenderer}
/>
{badRequest !== '' ? (
<Box style={{ padding: '0.5em' }}>
<Typography align='center' color='secondary'>
{t(badRequest)}
@ -342,7 +371,7 @@ const TakeButton = ({
setLoadingTake(true);
setOpen(closeAll);
}}
hasRobot={robot.avatarLoaded}
hasRobot={garage.getRobot().avatarLoaded}
onClickGenerateRobot={onClickGenerateRobot}
/>
<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 {
List,
@ -15,7 +15,7 @@ import {
Typography,
IconButton,
Tooltip,
Button,
ListItemButton,
} from '@mui/material';
import Countdown, { type CountdownRenderProps, zeroPad } from 'react-countdown';
@ -36,42 +36,48 @@ import { PaymentStringAsIcons } from '../../components/PaymentMethods';
import { FlagWithProps, SendReceiveIcon } from '../Icons';
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 TakeButton from './TakeButton';
import { F2fMapDialog } from '../Dialogs';
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
interface OrderDetailsProps {
order: Order;
setOrder: (state: Order) => void;
info: Info;
coordinator: Coordinator;
onClickCoordinator?: () => void;
baseUrl: string;
hasRobot: boolean;
onClickGenerateRobot?: () => void;
}
const OrderDetails = ({
order,
info,
setOrder,
coordinator,
onClickCoordinator = () => null,
baseUrl,
hasRobot,
onClickGenerateRobot = () => null,
}: OrderDetailsProps): JSX.Element => {
const { t } = useTranslation();
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
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 [openWorldmap, setOpenWorldmap] = useState<boolean>(false);
const amountString = useMemo(() => {
// 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 (
amountToString(
order.amount * 100000000,
order.amount ? false : order.has_range,
(order.amount * 100000000).toString(),
order.amount > 0 ? false : order.has_range,
order.min_amount * 100000000,
order.max_amount * 100000000,
) + ' Sats'
@ -79,14 +85,14 @@ const OrderDetails = ({
} else {
return (
amountToString(
order.amount,
order.amount ? false : order.has_range,
order.amount.toString(),
order.amount > 0 ? false : order.has_range,
order.min_amount,
order.max_amount,
) + ` ${currencyCode}`
);
}
}, [order.currency, order.amount, order.min_amount, order.max_amount, order.has_range]);
}, [orderUpdatedAt]);
// Countdown Renderer callback with condition
const countdownRenderer = function ({
@ -95,13 +101,13 @@ const OrderDetails = ({
minutes,
seconds,
completed,
}: CountdownRenderProps) {
}: CountdownRenderProps): JSX.Element {
if (completed) {
// Render a completed state
return <span> {t('The order has expired')}</span>;
} else {
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
if (fraction_left < 0.25) {
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 minutes = Math.floor((seconds - hours * 3600) / 60);
return (
<span>
{hours > 0 ? hours + 'h' : ''} {minutes > 0 ? zeroPad(minutes) + 'm' : ''}{' '}
{hours > 0 ? `${hours}h` : ''} {minutes > 0 ? `${zeroPad(minutes)}m` : ''}{' '}
</span>
);
};
// 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) {
// Render a completed state
return <span> {t('Penalty lifted, good to go!')}</span>;
@ -153,15 +167,20 @@ const OrderDetails = ({
let send: string = '';
let receive: string = '';
let sats: string = '';
const order = currentOrder.order;
const isBuyer = (order.type == 0 && order.is_maker) || (order.type == 1 && !order.is_maker);
const tradeFee = order.is_maker ? info?.maker_fee ?? 0 : info?.taker_fee ?? 0;
if (order === null) return {};
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 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 (order.amount) {
if (order.amount > 0) {
sats = computeSats({
amount: order.amount,
fee: -tradeFee,
@ -181,7 +200,7 @@ const OrderDetails = ({
routingBudget: defaultRoutingBudget,
rate,
});
sats = `${min}-${max}`;
sats = `${String(min)}-${String(max)}`;
}
send = t('You send via {{method}} {{amount}}', {
amount: amountString,
@ -192,7 +211,7 @@ const OrderDetails = ({
amount: sats,
});
} else {
if (order.amount) {
if (order.amount > 0) {
sats = computeSats({
amount: order.amount,
fee: tradeFee,
@ -209,7 +228,7 @@ const OrderDetails = ({
fee: tradeFee,
rate,
});
sats = `${min}-${max}`;
sats = `${String(min)}-${String(max)}`;
}
send = t('You send via Lightning {{amount}} Sats (Approx)', { amount: sats });
receive = t('You receive via {{method}} {{amount}}', {
@ -218,62 +237,93 @@ const OrderDetails = ({
});
}
return { send, receive };
}, [order.currency, order.satoshis_now, order.amount, order.has_range]);
}, [orderUpdatedAt]);
return (
<Grid container spacing={0}>
<F2fMapDialog
latitude={order.latitude}
longitude={order.longitude}
latitude={currentOrder.order?.latitude}
longitude={currentOrder.order?.longitude}
open={openWorldmap}
orderType={order.type || 0}
orderType={currentOrder.order?.type ?? 0}
zoom={6}
message={t(
'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}>
<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>
<ListItemAvatar sx={{ width: '4em', height: '4em' }}>
<RobotAvatar
statusColor={statusBadgeColor(order.maker_status)}
nickname={order.maker_nick}
tooltip={t(order.maker_status)}
orderType={order.type}
statusColor={statusBadgeColor(currentOrder.order?.maker_status ?? '')}
nickname={currentOrder.order?.maker_nick}
tooltip={t(currentOrder.order?.maker_status ?? '')}
orderType={currentOrder.order?.type}
baseUrl={baseUrl}
small={true}
/>
</ListItemAvatar>
<ListItemText
primary={`${order.maker_nick} (${
order.type
? t(order.currency == 1000 ? 'Swapping Out' : 'Seller')
: t(order.currency == 1000 ? 'Swapping In' : 'Buyer')
primary={`${currentOrder.order?.maker_nick} (${
currentOrder.order?.type === 1
? t(currentOrder.order?.currency === 1000 ? 'Swapping Out' : 'Seller')
: t(currentOrder.order?.currency === 1000 ? 'Swapping In' : 'Buyer')
})`}
secondary={t('Order maker')}
/>
</ListItem>
<Collapse in={order.is_participant && order.taker_nick !== 'None'}>
<Collapse
in={currentOrder.order?.is_participant && currentOrder.order?.taker_nick !== 'None'}
>
<Divider />
<ListItem>
<ListItemText
primary={`${order.taker_nick} (${
order.type
? t(order.currency == 1000 ? 'Swapping In' : 'Buyer')
: t(order.currency == 1000 ? 'Swapping Out' : 'Seller')
primary={`${currentOrder.order?.taker_nick} (${
currentOrder.order?.type === 1
? t(currentOrder.order?.currency === 1000 ? 'Swapping In' : 'Buyer')
: t(currentOrder.order?.currency === 1000 ? 'Swapping Out' : 'Seller')
})`}
secondary={t('Order taker')}
/>
<ListItemAvatar>
<RobotAvatar
avatarClass='smallAvatar'
statusColor={statusBadgeColor(order.taker_status)}
nickname={order.taker_nick == 'None' ? undefined : order.taker_nick}
tooltip={t(order.taker_status)}
orderType={order.type === 0 ? 1 : 0}
statusColor={statusBadgeColor(currentOrder.order?.taker_status ?? '')}
nickname={
currentOrder.order?.taker_nick === 'None'
? undefined
: currentOrder.order?.taker_nick
}
tooltip={t(currentOrder.order?.taker_status ?? '')}
orderType={currentOrder.order?.type === 0 ? 1 : 0}
baseUrl={baseUrl}
small={true}
/>
@ -284,12 +334,15 @@ const OrderDetails = ({
<Chip label={t('Order Details')} />
</Divider>
<Collapse in={order.is_participant}>
<Collapse in={currentOrder.order?.is_participant}>
<ListItem>
<ListItemIcon>
<Article />
</ListItemIcon>
<ListItemText primary={t(order.status_message)} secondary={t('Order status')} />
<ListItemText
primary={t(currentOrder.order?.status_message ?? '')}
secondary={t('Order status')}
/>
</ListItem>
<Divider />
</Collapse>
@ -311,7 +364,7 @@ const OrderDetails = ({
</ListItemIcon>
<ListItemText
primary={amountString}
secondary={order.amount ? 'Amount' : 'Amount Range'}
secondary={(currentOrder.order?.amount ?? 0) > 0 ? 'Amount' : 'Amount Range'}
/>
<ListItemIcon>
<IconButton
@ -360,18 +413,24 @@ const OrderDetails = ({
size={1.42 * theme.typography.fontSize}
othersText={t('Others')}
verbose={true}
text={order.payment_method}
text={currentOrder.order?.payment_method}
/>
}
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>
<Tooltip enterTouchDelay={0} title={t('F2F location')}>
<div>
<IconButton onClick={() => setOpenWorldmap(true)}>
<IconButton
onClick={() => {
setOpenWorldmap(true);
}}
>
<Map />
</IconButton>
</div>
@ -387,24 +446,27 @@ const OrderDetails = ({
<PriceChange />
</ListItemIcon>
{order.price_now !== undefined ? (
{currentOrder.order?.price_now !== undefined ? (
<ListItemText
primary={t('{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%', {
price: pn(order.price_now),
price: pn(currentOrder.order?.price_now),
currencyCode,
premium: order.premium_now,
premium: currentOrder.order?.premium_now,
})}
secondary={t('Price and Premium')}
/>
) : null}
{!order.price_now && order.is_explicit ? (
<ListItemText primary={pn(order.satoshis)} secondary={t('Amount of Satoshis')} />
{currentOrder.order?.price_now === undefined && currentOrder.order?.is_explicit ? (
<ListItemText
primary={pn(currentOrder.order?.satoshis)}
secondary={t('Amount of Satoshis')}
/>
) : null}
{!order.price_now && !order.is_explicit ? (
{currentOrder.order?.price_now === undefined && !currentOrder.order?.is_explicit ? (
<ListItemText
primary={parseFloat(Number(order.premium).toFixed(2)) + '%'}
primary={`${parseFloat(Number(currentOrder.order?.premium).toFixed(2))}%`}
secondary={t('Premium over market price')}
/>
) : null}
@ -418,7 +480,7 @@ const OrderDetails = ({
</ListItemIcon>
<Grid container>
<Grid item xs={4.5}>
<ListItemText primary={order.id} secondary={t('Order ID')} />
<ListItemText primary={currentOrder.order?.id} secondary={t('Order ID')} />
</Grid>
<Grid item xs={7.5}>
<Grid container>
@ -429,7 +491,7 @@ const OrderDetails = ({
</Grid>
<Grid item xs={10}>
<ListItemText
primary={timerRenderer(order.escrow_duration)}
primary={timerRenderer(currentOrder.order?.escrow_duration)}
secondary={t('Deposit timer')}
></ListItemText>
</Grid>
@ -439,39 +501,47 @@ const OrderDetails = ({
</ListItem>
{/* if order is in a status that does not expire, do not show countdown */}
<Collapse in={![4, 5, 12, 13, 14, 15, 16, 17, 18].includes(order.status)}>
<Collapse
in={![4, 5, 12, 13, 14, 15, 16, 17, 18].includes(currentOrder.order?.status ?? 0)}
>
<Divider />
<ListItem>
<ListItemIcon>
<AccessTime />
</ListItemIcon>
<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>
</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>
</List>
{/* If the user has a penalty/limit */}
{order.penalty !== undefined ? (
{currentOrder.order?.penalty !== undefined ? (
<Grid item xs={12}>
<Alert severity='warning' sx={{ borderRadius: '0' }}>
<Countdown date={new Date(order.penalty)} renderer={countdownPenaltyRenderer} />
<Countdown
date={new Date(currentOrder.order?.penalty ?? '')}
renderer={countdownPenaltyRenderer}
/>
</Alert>
</Grid>
) : (
<></>
)}
{!order.is_participant ? (
{!currentOrder.order?.is_participant ? (
<Grid item xs={12}>
<TakeButton
order={order}
setOrder={setOrder}
baseUrl={baseUrl}
hasRobot={hasRobot}
info={info}
info={coordinator.info}
onClickGenerateRobot={onClickGenerateRobot}
/>
</Grid>

View File

@ -19,11 +19,11 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
const parsedText = useMemo(() => {
const rows = [];
let custom_methods = text;
let customMethods = text;
// Adds icons for each PaymentMethod that matches
methods.forEach((method, i) => {
if (text.includes(method.name)) {
custom_methods = custom_methods.replace(method.name, '');
customMethods = customMethods.replace(method.name, '');
rows.push(
<Tooltip
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
const chars_left = custom_methods
const charsLeft = customMethods
.replace(' ', '')
.replace(' ', '')
.replace(' ', '')
.replace(' ', '')
.replace(' ', '');
if (chars_left.length > 0) {
if (charsLeft.length > 0) {
rows.push(
<Tooltip
key={'pushed'}
placement='top'
enterTouchDelay={0}
title={verbose ? othersText : othersText + ': ' + custom_methods}
title={verbose ? othersText : othersText + ': ' + customMethods}
>
<div
style={{
@ -82,7 +82,7 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
{rows}{' '}
<div style={{ display: 'inline-block' }}>
{' '}
<span>{custom_methods}</span>
<span>{customMethods}</span>
</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 { Avatar, Badge, Tooltip, useTheme } from '@mui/material';
import { Avatar, Badge, Tooltip } from '@mui/material';
import { SendReceiveIcon } from '../Icons';
import { apiClient } from '../../services/api';
import placeholder from './placeholder.json';
import { UseAppStoreType, AppContext } from '../../contexts/AppContext';
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
interface Props {
nickname: string | undefined;
smooth?: boolean;
coordinator?: boolean;
small?: boolean;
flipHorizontally?: boolean;
style?: object;
@ -41,27 +44,35 @@ const RobotAvatar: React.FC<Props> = ({
avatarClass = 'flippedSmallAvatar',
imageStyle = {},
onLoad = () => {},
coordinator = false,
baseUrl,
}) => {
const { settings, origin, hostUrl } = useContext<UseAppStoreType>(AppContext);
const { federation, focusedCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const [avatarSrc, setAvatarSrc] = useState<string>();
const [nicknameReady, setNicknameReady] = useState<boolean>(false);
const [activeBackground, setActiveBackground] = useState<boolean>(true);
const path = coordinator ? '/static/federation/' : '/static/assets/avatars/';
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 className = placeholderType == 'loading' ? 'loadingAvatar' : 'generatingAvatar';
const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar';
useEffect(() => {
if (nickname != undefined) {
if (nickname !== undefined) {
if (window.NativeRobosats === undefined) {
setAvatarSrc(`${baseUrl}/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`);
setAvatarSrc(`${baseUrl}${path}${nickname}${small ? '.small' : ''}.webp`);
setNicknameReady(true);
} else {
} else if (focusedCoordinator) {
setNicknameReady(true);
apiClient
.fileImageUrl(baseUrl, `/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`)
const { url } = federation
.getCoordinator(focusedCoordinator)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
void apiClient
.fileImageUrl(url, `${path}${nickname}${small ? '.small' : ''}.webp`)
.then(setAvatarSrc);
}
} else {
@ -133,7 +144,7 @@ const RobotAvatar: React.FC<Props> = ({
const getAvatarWithBadges = useCallback(() => {
let component = avatar;
if (statusColor) {
if (statusColor !== undefined) {
component = (
<Badge variant='dot' overlap='circular' badgeContent='' color={statusColor}>
{component}
@ -153,7 +164,7 @@ const RobotAvatar: React.FC<Props> = ({
);
}
if (tooltip) {
if (tooltip !== undefined) {
component = (
<Tooltip placement={tooltipPosition} enterTouchDelay={0} title={tooltip}>
{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 { useTranslation } from 'react-i18next';
import { Select, MenuItem, useTheme, Grid, Typography } from '@mui/material';
import type Language from '../../models/Language.model';
import {
Select,
MenuItem,
useTheme,
Grid,
Typography,
type SelectChangeEvent,
} from '@mui/material';
import Flags from 'country-flag-icons/react/3x2';
import { CataloniaFlag, BasqueCountryFlag } from '../Icons';
import type { Language } from '../../models';
const menuLanuguages = [
{ name: 'English', i18nCode: 'en', flag: Flags.US },
@ -31,18 +38,18 @@ interface SelectLanguageProps {
setLanguage: (lang: Language) => void;
}
const SelectLanguage = ({ language, setLanguage }: SelectLanguageProps): JSX.Element => {
const SelectLanguage: React.FC<SelectLanguageProps> = ({ language, setLanguage }) => {
const theme = useTheme();
const { t, i18n } = useTranslation();
const { i18n } = useTranslation();
const flagProps = {
width: 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);
i18n.changeLanguage(e.target.value);
void i18n.changeLanguage(e.target.value);
};
return (

View File

@ -32,10 +32,9 @@ import SwapCalls from '@mui/icons-material/SwapCalls';
interface SettingsFormProps {
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 theme = useTheme();
const { t } = useTranslation();
@ -176,8 +175,8 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
</ListItemIcon>
<Slider
value={settings.fontSize}
min={settings.frontend == 'basic' ? 12 : 10}
max={settings.frontend == 'basic' ? 16 : 14}
min={settings.frontend === 'basic' ? 12 : 10}
max={settings.frontend === 'basic' ? 16 : 14}
step={1}
onChange={(e) => {
const fontSize = e.target.value;
@ -215,7 +214,6 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
</ToggleButtonGroup>
</ListItem>
{showNetwork ? (
<ListItem>
<ListItemIcon>
<Link />
@ -236,9 +234,6 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
</ToggleButton>
</ToggleButtonGroup>
</ListItem>
) : (
<></>
)}
</List>
</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';
if (status === 'unlocked') {
Icon = LockOpen;
color = theme.palette.mode == 'dark' ? 'lightgreen' : 'green';
color = theme.palette.mode === 'dark' ? 'lightgreen' : 'green';
} else if (status === 'settled') {
Icon = Balance;
color = theme.palette.mode == 'dark' ? 'lightred' : 'red';
color = theme.palette.mode === 'dark' ? 'lightred' : 'red';
}
if (status === 'hide') {

View File

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

View File

@ -4,21 +4,21 @@ import { Alert } from '@mui/material';
import { type Order } from '../../models';
interface CollabCancelAlertProps {
order: Order;
order: Order | null;
}
const CollabCancelAlert = ({ order }: CollabCancelAlertProps): JSX.Element => {
const { t } = useTranslation();
let text = '';
if (order.pending_cancel) {
if (order?.pending_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');
}
return text != '' ? (
return text !== '' ? (
<Alert severity='warning' style={{ width: '100%' }}>
{text}
</Alert>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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