mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-19 04:31:35 +00:00
New Robot page (#357)
* Init RobotPage * Add onboarding component and generate token step * Add robot generation step * Add onboarding step 3 * Add Welcome componenent * Minor fixes * Add recovery component * Add robot profile component * Small fixes * Add TOR loading component * Small fixes * Fix tor loading and add highres animations on android * Lint * Fix robot profile order buttons
This commit is contained in:
parent
abe12aebb5
commit
b39ae7de41
@ -4,7 +4,7 @@ import Main from './basic/Main';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { ThemeProvider, createTheme, Theme } from '@mui/material/styles';
|
||||
import UnsafeAlert from './components/UnsafeAlert';
|
||||
import TorConnection from './components/TorConnection';
|
||||
import TorConnectionBadge from './components/TorConnection';
|
||||
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from './i18n/Web';
|
||||
@ -29,6 +29,13 @@ const makeTheme = function (settings: Settings) {
|
||||
const App = (): JSX.Element => {
|
||||
const [theme, setTheme] = useState<Theme>(makeTheme(new Settings()));
|
||||
const [settings, setSettings] = useState<Settings>(new Settings());
|
||||
const [torStatus, setTorStatus] = useState<string>('NOTINIT');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('torStatus', (event) => {
|
||||
setTorStatus(event?.detail);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTheme(makeTheme(settings));
|
||||
@ -46,9 +53,9 @@ const App = (): JSX.Element => {
|
||||
{window.NativeRobosats === undefined ? (
|
||||
<UnsafeAlert settings={settings} setSettings={setSettings} />
|
||||
) : (
|
||||
<TorConnection />
|
||||
<TorConnectionBadge torStatus={torStatus} />
|
||||
)}
|
||||
<Main settings={settings} setSettings={setSettings} />
|
||||
<Main settings={settings} setSettings={setSettings} torStatus={torStatus} />
|
||||
</ThemeProvider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { HashRouter, BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||
import { useTheme, Box, Slide, Typography } from '@mui/material';
|
||||
|
||||
import UserGenPage from './UserGenPage';
|
||||
import RobotPage from './RobotPage';
|
||||
import MakerPage from './MakerPage';
|
||||
import BookPage from './BookPage';
|
||||
import OrderPage from './OrderPage';
|
||||
@ -71,10 +71,11 @@ interface SlideDirection {
|
||||
|
||||
interface MainProps {
|
||||
settings: Settings;
|
||||
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
||||
setSettings: (state: Settings) => void;
|
||||
}
|
||||
|
||||
const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
@ -349,11 +350,11 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
appear={slideDirection.in != undefined}
|
||||
>
|
||||
<div>
|
||||
<UserGenPage
|
||||
<RobotPage
|
||||
setPage={setPage}
|
||||
torStatus={torStatus}
|
||||
setCurrentOrder={setCurrentOrder}
|
||||
match={props.match}
|
||||
theme={theme}
|
||||
windowSize={windowSize}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
baseUrl={baseUrl}
|
||||
|
300
frontend/src/basic/RobotPage/Onboarding.tsx
Normal file
300
frontend/src/basic/RobotPage/Onboarding.tsx
Normal file
@ -0,0 +1,300 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Collapse,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
Link,
|
||||
Typography,
|
||||
useTheme,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from '@mui/material';
|
||||
import { Page } from '../NavBar';
|
||||
import { Robot } from '../../models';
|
||||
import { Casino, Bolt, Check, Storefront, AddBox, School } from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
import TokenInput from './TokenInput';
|
||||
import { genBase62Token } from '../../utils';
|
||||
import { NewTabIcon } from '../../components/Icons';
|
||||
|
||||
interface OnboardingProps {
|
||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||
robot: Robot;
|
||||
setRobot: (state: Robot) => void;
|
||||
inputToken: string;
|
||||
setInputToken: (state: string) => void;
|
||||
getGenerateRobot: (token: string) => void;
|
||||
badRequest: string | undefined;
|
||||
setPage: (state: Page) => void;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const Onboarding = ({
|
||||
setView,
|
||||
robot,
|
||||
inputToken,
|
||||
setInputToken,
|
||||
setRobot,
|
||||
badRequest,
|
||||
getGenerateRobot,
|
||||
setPage,
|
||||
baseUrl,
|
||||
}: OnboardingProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const theme = useTheme();
|
||||
|
||||
const [step, setStep] = useState<'1' | '2' | '3'>('1');
|
||||
const [generatedToken, setGeneratedToken] = useState<boolean>(false);
|
||||
const [showMimickProgress, setShowMimickProgress] = useState<boolean>(false);
|
||||
|
||||
const generateToken = () => {
|
||||
setGeneratedToken(true);
|
||||
setInputToken(genBase62Token(36));
|
||||
setShowMimickProgress(true);
|
||||
setTimeout(() => setShowMimickProgress(false), 1000);
|
||||
};
|
||||
|
||||
const changePage = function (newPage: Page) {
|
||||
setPage(newPage);
|
||||
history.push(`/${newPage}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Accordion expanded={step === '1'} disableGutters={true}>
|
||||
<AccordionSummary>
|
||||
<Typography variant='h5' color={step == '1' ? 'text.primary' : 'text.disabled'}>
|
||||
{t('1. Generate a token')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container direction='column' alignItems='center' spacing={1} padding={1}>
|
||||
<Grid item>
|
||||
<Typography>
|
||||
{t(
|
||||
'This temporary key gives you access to a unique and private robot identity for your trade.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
{!generatedToken ? (
|
||||
<Grid item>
|
||||
<Button autoFocus onClick={generateToken} variant='contained' size='large'>
|
||||
<Casino />
|
||||
{t('Generate token')}
|
||||
</Button>
|
||||
</Grid>
|
||||
) : (
|
||||
<Grid item>
|
||||
<Collapse in={generatedToken}>
|
||||
<Grid container direction='column' alignItems='center' spacing={1}>
|
||||
<Grid item>
|
||||
<Alert variant='outlined' severity='info'>
|
||||
<b>{`${t('Store it somewhere safe!')} `}</b>
|
||||
{t(
|
||||
`This token is the one and only key to your robot and trade. You will need it later to recover your order or check its status.`,
|
||||
)}
|
||||
</Alert>
|
||||
</Grid>
|
||||
<Grid item sx={{ width: '100%' }}>
|
||||
{showMimickProgress ? (
|
||||
<LinearProgress sx={{ height: '0.7em' }} />
|
||||
) : (
|
||||
<TokenInput
|
||||
autoFocusTarget='copyButton'
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography>
|
||||
{t('You can also add your own random characters into the token or')}
|
||||
<Button size='small' onClick={generateToken}>
|
||||
<Casino />
|
||||
{t('roll again')}
|
||||
</Button>
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setStep('2');
|
||||
getGenerateRobot(inputToken);
|
||||
setRobot({ ...robot, nickname: undefined });
|
||||
}}
|
||||
variant='contained'
|
||||
size='large'
|
||||
>
|
||||
<Check />
|
||||
{t('Continue')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion expanded={step === '2'} disableGutters={true}>
|
||||
<AccordionSummary>
|
||||
<Typography variant='h5' color={step == '2' ? 'text.primary' : 'text.disabled'}>
|
||||
{t('2. Meet your robot identity')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container direction='column' alignItems='center' spacing={1}>
|
||||
<Grid item>
|
||||
<Typography>
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
t('This is your trading avatar')
|
||||
) : (
|
||||
<>
|
||||
<b>{t('Building your robot!')}</b>
|
||||
<LinearProgress />
|
||||
</>
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ width: '13.5em' }}>
|
||||
<RobotAvatar
|
||||
nickname={robot.nickname}
|
||||
smooth={true}
|
||||
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
|
||||
placeholderType='generating'
|
||||
imageStyle={{
|
||||
transform: '',
|
||||
border: '2px solid #555',
|
||||
filter: 'drop-shadow(1px 1px 1px #000000)',
|
||||
height: '12.4em',
|
||||
width: '12.4em',
|
||||
}}
|
||||
tooltipPosition='top'
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
<Grid item>
|
||||
<Typography align='center'>{t('Hi! My name is')}</Typography>
|
||||
<Typography component='h5' variant='h5'>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Bolt
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
height: '1.5em',
|
||||
width: '1.5em',
|
||||
}}
|
||||
/>
|
||||
<b>{robot.nickname}</b>
|
||||
<Bolt
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
height: '1.5em',
|
||||
width: '1.5em',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : null}
|
||||
<Grid item>
|
||||
<Collapse in={!!(robot.avatarLoaded && robot.nickname)}>
|
||||
<Button onClick={() => setStep('3')} variant='contained' size='large'>
|
||||
<Check />
|
||||
{t('Continue')}
|
||||
</Button>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion expanded={step === '3'} disableGutters={true}>
|
||||
<AccordionSummary>
|
||||
<Typography variant='h5' color={step == '3' ? 'text.primary' : 'text.disabled'}>
|
||||
{t('3. Browse or create an order')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container direction='column' alignItems='center' spacing={1} padding={1.5}>
|
||||
<Grid item>
|
||||
<Typography>
|
||||
{t(
|
||||
'RoboSats is a peer-to-peer marketplace. You can browse the public offers or create a new one.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<ButtonGroup variant='contained'>
|
||||
<Button color='primary' onClick={() => changePage('offers')}>
|
||||
<Storefront /> <div style={{ width: '0.5em' }} />
|
||||
{t('Offers')}
|
||||
</Button>
|
||||
<Button color='secondary' onClick={() => changePage('create')}>
|
||||
<AddBox /> <div style={{ width: '0.5em' }} />
|
||||
{t('Create')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Typography>
|
||||
{`${t('If you need help on your RoboSats journey join our public support')} `}
|
||||
<Link target='_blank' href='https://t.me/robosats_es' rel='noreferrer'>
|
||||
{t('Telegram group')}
|
||||
</Link>
|
||||
{`, ${t('or visit the robot school for documentation.')} `}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
sx={{ color: 'black' }}
|
||||
component={Link}
|
||||
href='https://learn.robosats.com'
|
||||
target='_blank'
|
||||
color='inherit'
|
||||
variant='contained'
|
||||
>
|
||||
<School /> <div style={{ width: '0.5em' }} />
|
||||
{t('Learn RoboSats')}
|
||||
<div style={{ width: '0.5em' }} />
|
||||
<NewTabIcon sx={{ width: '0.8em' }} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item sx={{ position: 'relative', top: '0.6em' }}>
|
||||
<Button color='inherit' onClick={() => setView('profile')}>
|
||||
{t('See profile')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Onboarding;
|
84
frontend/src/basic/RobotPage/Recovery.tsx
Normal file
84
frontend/src/basic/RobotPage/Recovery.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Collapse, Grid, Typography, useTheme } from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Page } from '../NavBar';
|
||||
import { Robot } from '../../models';
|
||||
import { Casino, Download, ContentCopy, SmartToy, Bolt } from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
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;
|
||||
setInputToken: (state: string) => void;
|
||||
getGenerateRobot: (token: string) => void;
|
||||
setPage: (state: Page) => void;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const Recovery = ({
|
||||
robot,
|
||||
setRobot,
|
||||
inputToken,
|
||||
setView,
|
||||
setInputToken,
|
||||
getGenerateRobot,
|
||||
setPage,
|
||||
baseUrl,
|
||||
}: RecoveryProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const recoveryDisabled = () => {
|
||||
return !(inputToken.length > 20);
|
||||
};
|
||||
const onClickRecover = () => {
|
||||
if (recoveryDisabled()) {
|
||||
} else {
|
||||
getGenerateRobot(inputToken);
|
||||
setView('profile');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container direction='column' alignItems='center' spacing={1} padding={2}>
|
||||
<Grid item>
|
||||
<Typography align='center'>
|
||||
{t(
|
||||
'Please, introduce your robot token to re-build your robot and gain access to its trades.',
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TokenInput
|
||||
showCopy={false}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
label={t('Paste token here')}
|
||||
robot={robot}
|
||||
onPressEnter={onClickRecover}
|
||||
badRequest={''}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant='contained'
|
||||
size='large'
|
||||
disabled={recoveryDisabled()}
|
||||
onClick={onClickRecover}
|
||||
>
|
||||
<Key /> <div style={{ width: '0.5em' }} />
|
||||
{t('Recover')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Recovery;
|
203
frontend/src/basic/RobotPage/RobotProfile.tsx
Normal file
203
frontend/src/basic/RobotPage/RobotProfile.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Link, Grid, LinearProgress, Typography, Alert } from '@mui/material';
|
||||
import { Bolt, Logout, Refresh } from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
import TokenInput from './TokenInput';
|
||||
import { Page } from '../NavBar';
|
||||
import { Robot } from '../../models';
|
||||
import { genBase62Token } from '../../utils';
|
||||
|
||||
interface RobotProfileProps {
|
||||
robot: Robot;
|
||||
setRobot: (state: Robot) => void;
|
||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||
inputToken: string;
|
||||
setCurrentOrder: (state: number) => void;
|
||||
logoutRobot: () => void;
|
||||
setInputToken: (state: string) => void;
|
||||
getGenerateRobot: (token: string) => void;
|
||||
setPage: (state: Page) => void;
|
||||
baseUrl: string;
|
||||
badRequest: string;
|
||||
robotFound: boolean;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const RobotProfile = ({
|
||||
robot,
|
||||
setRobot,
|
||||
inputToken,
|
||||
setInputToken,
|
||||
setCurrentOrder,
|
||||
getGenerateRobot,
|
||||
logoutRobot,
|
||||
setPage,
|
||||
setView,
|
||||
badRequest,
|
||||
baseUrl,
|
||||
robotFound,
|
||||
width,
|
||||
}: RobotProfileProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Grid container direction='column' alignItems='center' spacing={2} padding={2}>
|
||||
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
<Typography align='center' component='h5' variant='h5'>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{width < 19 ? null : (
|
||||
<Bolt
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
height: '1.5em',
|
||||
width: '1.5em',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<b>{robot.nickname}</b>
|
||||
{width < 19 ? null : (
|
||||
<Bolt
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
height: '1.5em',
|
||||
width: '1.5em',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
<b>{t('Building your robot!')}</b>
|
||||
<LinearProgress />
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ width: `13.5em` }}>
|
||||
<RobotAvatar
|
||||
nickname={robot.nickname}
|
||||
smooth={true}
|
||||
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
|
||||
placeholderType='generating'
|
||||
imageStyle={{
|
||||
transform: '',
|
||||
border: '2px solid #555',
|
||||
filter: 'drop-shadow(1px 1px 1px #000000)',
|
||||
height: `12.4em`,
|
||||
width: `12.4em`,
|
||||
}}
|
||||
tooltip={t('This is your trading avatar')}
|
||||
tooltipPosition='top'
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* {robotFound ? (
|
||||
<Grid item>
|
||||
<Typography variant='h6'>
|
||||
{t('Welcome back!')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)} */}
|
||||
|
||||
{robot.activeOrderId ? (
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
history.push('/order/' + robot.activeOrderId);
|
||||
setPage('order');
|
||||
setCurrentOrder(robot.activeOrderId);
|
||||
}}
|
||||
>
|
||||
{t('Active order #{{orderID}}', { orderID: robot.activeOrderId })}
|
||||
</Button>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{robot.lastOrderId ? (
|
||||
<Grid item container direction='column' alignItems='center'>
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
history.push('/order/' + robot.lastOrderId);
|
||||
setPage('order');
|
||||
setCurrentOrder(robot.lastOrderId);
|
||||
}}
|
||||
>
|
||||
{t('Last order #{{orderID}}', { orderID: robot.lastOrderId })}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Alert severity='warning'>
|
||||
<Grid container direction='column' alignItems='center'>
|
||||
<Grid item>
|
||||
{t(
|
||||
'Reusing trading identity degrades your privacy against other users, coordinators and observers.',
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item sx={{ position: 'relative', right: '1em' }}>
|
||||
<Button
|
||||
color='inherit'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
logoutRobot();
|
||||
setView('welcome');
|
||||
}}
|
||||
>
|
||||
<Refresh />
|
||||
{t('Generate a new Robot')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
<Grid item sx={{ width: '100%' }}>
|
||||
<TokenInput
|
||||
inputToken={inputToken}
|
||||
editable={false}
|
||||
showDownload={true}
|
||||
label={t('Store your token safely')}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
disabled={!(robot.avatarLoaded && robot.nickname)}
|
||||
size='small'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
logoutRobot();
|
||||
setView('welcome');
|
||||
}}
|
||||
>
|
||||
<Logout /> <div style={{ width: '0.5em' }} />
|
||||
{t('Logout Robot')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default RobotProfile;
|
107
frontend/src/basic/RobotPage/TokenInput.tsx
Normal file
107
frontend/src/basic/RobotPage/TokenInput.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconButton, TextField, Tooltip, useTheme } from '@mui/material';
|
||||
import { Robot } from '../../models';
|
||||
import { Download, ContentCopy } from '@mui/icons-material';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { saveAsJson } from '../../utils';
|
||||
|
||||
interface TokenInputProps {
|
||||
robot: Robot;
|
||||
editable?: boolean;
|
||||
showDownload?: boolean;
|
||||
fullWidth?: boolean;
|
||||
setRobot: (state: Robot) => void;
|
||||
inputToken: string;
|
||||
autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
|
||||
onPressEnter: () => void;
|
||||
badRequest: string | undefined;
|
||||
setInputToken: (state: string) => void;
|
||||
showCopy?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const TokenInput = ({
|
||||
robot,
|
||||
editable = true,
|
||||
showCopy = true,
|
||||
label,
|
||||
setRobot,
|
||||
showDownload = false,
|
||||
fullWidth = true,
|
||||
onPressEnter,
|
||||
autoFocusTarget = 'textfield',
|
||||
inputToken,
|
||||
badRequest,
|
||||
setInputToken,
|
||||
}: TokenInputProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [showCopied, setShowCopied] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCopied(false);
|
||||
}, [inputToken]);
|
||||
|
||||
const createJsonFile = () => {
|
||||
return {
|
||||
token: robot.token,
|
||||
token_shannon_entropy: robot.shannonEntropy,
|
||||
token_bit_entropy: robot.bitsEntropy,
|
||||
public_key: robot.pubKey,
|
||||
encrypted_private_key: robot.encPrivKey,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
error={!!badRequest}
|
||||
disabled={!editable}
|
||||
required={true}
|
||||
label={label || undefined}
|
||||
value={inputToken}
|
||||
autoFocus={autoFocusTarget == 'texfield'}
|
||||
fullWidth={fullWidth}
|
||||
sx={{ borderColor: 'primary' }}
|
||||
variant={editable ? 'outlined' : 'filled'}
|
||||
helperText={badRequest}
|
||||
size='medium'
|
||||
onChange={(e) => setInputToken(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onPressEnter();
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: showDownload ? (
|
||||
<Tooltip enterTouchDelay={250} title={t('Download token and PGP credentials')}>
|
||||
<IconButton
|
||||
color='primary'
|
||||
sx={{ position: 'relative', top: label ? '0.4em' : '0em' }}
|
||||
onClick={() => saveAsJson(robot.nickname + '.json', createJsonFile())}
|
||||
>
|
||||
<Download sx={{ width: '1em', height: '1em' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null,
|
||||
endAdornment: showCopy ? (
|
||||
<Tooltip open={showCopied} title={t('Copied!')}>
|
||||
<IconButton
|
||||
autoFocus={autoFocusTarget == 'copyButton'}
|
||||
color={robot.copiedToken ? 'inherit' : 'primary'}
|
||||
onClick={() => {
|
||||
systemClient.copyToClipboard(inputToken);
|
||||
setShowCopied(true);
|
||||
setTimeout(() => setShowCopied(false), 1000);
|
||||
setRobot({ ...robot, copiedToken: true });
|
||||
}}
|
||||
>
|
||||
<ContentCopy sx={{ width: '1em', height: '1em' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenInput;
|
120
frontend/src/basic/RobotPage/Welcome.tsx
Normal file
120
frontend/src/basic/RobotPage/Welcome.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Button, Grid, Typography, useTheme } from '@mui/material';
|
||||
import { RoboSatsTextIcon } from '../../components/Icons';
|
||||
import { FastForward, RocketLaunch, Key } from '@mui/icons-material';
|
||||
import { genBase62Token } from '../../utils';
|
||||
|
||||
interface WelcomeProps {
|
||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||
getGenerateRobot: (token: string) => void;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const Welcome = ({ setView, width, getGenerateRobot }: WelcomeProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
alignItems='center'
|
||||
spacing={1.8}
|
||||
paddingTop={2.2}
|
||||
padding={0.5}
|
||||
>
|
||||
<Grid item>
|
||||
<svg width={0} height={0}>
|
||||
<linearGradient id='linearColors' x1={1} y1={0} x2={1} y2={1}>
|
||||
<stop offset={0} stopColor={theme.palette.primary.main} />
|
||||
<stop offset={1} stopColor={theme.palette.secondary.main} />
|
||||
</linearGradient>
|
||||
</svg>
|
||||
<RoboSatsTextIcon
|
||||
sx={{
|
||||
fill: 'url(#linearColors)',
|
||||
height: `${Math.min(width * 0.66, 17) * 0.25}em`,
|
||||
width: `${Math.min(width * 0.66, 17)}em`,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
lineHeight={0.82}
|
||||
sx={{ position: 'relative', bottom: '0.3em' }}
|
||||
color='secondary'
|
||||
align='center'
|
||||
component='h6'
|
||||
variant='h6'
|
||||
>
|
||||
{t('A Simple and Private LN P2P Exchange')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<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',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid container direction='column' alignItems='center' spacing={1} padding={1.5}>
|
||||
<Grid item>
|
||||
<Typography align='center'>
|
||||
{t('Create a new robot and learn to use RoboSats')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
size='large'
|
||||
color='primary'
|
||||
variant='contained'
|
||||
onClick={() => setView('onboarding')}
|
||||
>
|
||||
<RocketLaunch />
|
||||
<div style={{ width: '0.5em' }} />
|
||||
{t('Start')}
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Typography align='center'>
|
||||
{t('Recover an existing robot using your token')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
size='small'
|
||||
color='secondary'
|
||||
variant='contained'
|
||||
onClick={() => setView('recovery')}
|
||||
>
|
||||
<Key /> <div style={{ width: '0.5em' }} />
|
||||
{t('Recovery')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item sx={{ position: 'relative', bottom: '0.5em' }}>
|
||||
<Button
|
||||
size='small'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
setView('profile');
|
||||
getGenerateRobot(genBase62Token(36));
|
||||
}}
|
||||
>
|
||||
<FastForward /> <div style={{ width: '0.5em' }} />
|
||||
{t('Fast Generate Robot')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Welcome;
|
@ -0,0 +1,272 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Paper,
|
||||
Grid,
|
||||
CircularProgress,
|
||||
Box,
|
||||
Alert,
|
||||
Typography,
|
||||
useTheme,
|
||||
AlertTitle,
|
||||
} from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Page } from '../NavBar';
|
||||
import { Robot } from '../../models';
|
||||
import { tokenStrength } from '../../utils';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { apiClient } from '../../services/api';
|
||||
import { genKey } from '../../pgp';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import Onboarding from './Onboarding';
|
||||
import Welcome from './Welcome';
|
||||
import RobotProfile from './RobotProfile';
|
||||
import Recovery from './Recovery';
|
||||
import { TorIcon } from '../../components/Icons';
|
||||
|
||||
interface RobotPageProps {
|
||||
setPage: (state: Page) => void;
|
||||
setCurrentOrder: (state: number) => void;
|
||||
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
||||
robot: Robot;
|
||||
setRobot: (state: Robot) => void;
|
||||
windowSize: { width: number; height: number };
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const RobotPage = ({
|
||||
setPage,
|
||||
setCurrentOrder,
|
||||
torStatus,
|
||||
windowSize,
|
||||
robot,
|
||||
setRobot,
|
||||
baseUrl,
|
||||
}: RobotPageProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const refCode = params.refCode;
|
||||
const width = Math.min(windowSize.width * 0.8, 28);
|
||||
const maxHeight = windowSize.height * 0.85 - 3;
|
||||
const theme = useTheme();
|
||||
|
||||
const [robotFound, setRobotFound] = useState<boolean>(false);
|
||||
const [badRequest, setBadRequest] = useState<string | undefined>(undefined);
|
||||
const [inputToken, setInputToken] = useState<string>('');
|
||||
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
|
||||
robot.token ? 'profile' : 'welcome',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (robot.token) {
|
||||
setInputToken(robot.token);
|
||||
}
|
||||
if (robot.nickname == null && robot.token) {
|
||||
getGenerateRobot(robot.token);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getGenerateRobot = (token: string) => {
|
||||
const strength = tokenStrength(token);
|
||||
setRobot({ ...robot, loading: true, avatarLoaded: false });
|
||||
setInputToken(token);
|
||||
|
||||
const requestBody = genKey(token).then(function (key) {
|
||||
return {
|
||||
token_sha256: sha256(token),
|
||||
public_key: key.publicKeyArmored,
|
||||
encrypted_private_key: key.encryptedPrivateKeyArmored,
|
||||
unique_values: strength.uniqueValues,
|
||||
counts: strength.counts,
|
||||
length: token.length,
|
||||
ref_code: refCode,
|
||||
};
|
||||
});
|
||||
|
||||
requestBody.then(
|
||||
async (body) =>
|
||||
await apiClient.post(baseUrl, '/api/user/', body).then((data: any) => {
|
||||
setRobotFound(data?.found);
|
||||
setBadRequest(data?.bad_request);
|
||||
setCurrentOrder(
|
||||
data.active_order_id
|
||||
? data.active_order_id
|
||||
: data.last_order_id
|
||||
? data.last_order_id
|
||||
: null,
|
||||
);
|
||||
// Add nick and token to App state (token only if not a bad request)
|
||||
data.bad_request
|
||||
? setRobot({
|
||||
...robot,
|
||||
avatarLoaded: true,
|
||||
loading: false,
|
||||
nickname: data.nickname ?? robot.nickname,
|
||||
activeOrderId: data.active_order_id ?? null,
|
||||
referralCode: data.referral_code ?? robot.referralCode,
|
||||
earnedRewards: data.earned_rewards ?? robot.earnedRewards,
|
||||
lastOrderId: data.last_order_id ?? robot.lastOrderId,
|
||||
stealthInvoices: data.wants_stealth ?? robot.stealthInvoices,
|
||||
tgEnabled: data.tg_enabled,
|
||||
tgBotName: data.tg_bot_name,
|
||||
tgToken: data.tg_token,
|
||||
})
|
||||
: setRobot({
|
||||
...robot,
|
||||
nickname: data.nickname,
|
||||
token,
|
||||
loading: false,
|
||||
activeOrderId: data.active_order_id ?? null,
|
||||
lastOrderId: data.last_order_id ?? null,
|
||||
referralCode: data.referral_code,
|
||||
earnedRewards: data.earned_rewards ?? 0,
|
||||
stealthInvoices: data.wants_stealth,
|
||||
tgEnabled: data.tg_enabled,
|
||||
tgBotName: data.tg_bot_name,
|
||||
tgToken: data.tg_token,
|
||||
bitsEntropy: data.token_bits_entropy,
|
||||
shannonEntropy: data.token_shannon_entropy,
|
||||
pubKey: data.public_key,
|
||||
encPrivKey: data.encrypted_private_key,
|
||||
copiedToken: data.found ? true : robot.copiedToken,
|
||||
}) &
|
||||
systemClient.setItem('robot_token', token) &
|
||||
systemClient.setItem('pub_key', data.public_key.split('\n').join('\\')) &
|
||||
systemClient.setItem(
|
||||
'enc_priv_key',
|
||||
data.encrypted_private_key.split('\n').join('\\'),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteRobot = () => {
|
||||
apiClient.delete(baseUrl, '/api/user');
|
||||
logoutRobot();
|
||||
};
|
||||
|
||||
const logoutRobot = () => {
|
||||
setInputToken('');
|
||||
setRobotFound(false);
|
||||
systemClient.deleteCookie('sessionid');
|
||||
systemClient.deleteItem('robot_token');
|
||||
systemClient.deleteItem('pub_key');
|
||||
systemClient.deleteItem('enc_priv_key');
|
||||
setTimeout(() => setRobot(new Robot()), 10);
|
||||
};
|
||||
|
||||
if (!(window.NativeRobosats === undefined) && !(torStatus == 'DONE' || torStatus == '"Done"')) {
|
||||
return (
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{
|
||||
width: `${width}em`,
|
||||
maxHeight: `${maxHeight}em`,
|
||||
}}
|
||||
>
|
||||
<Grid container direction='column' alignItems='center' spacing={1} padding={2}>
|
||||
<Grid item>
|
||||
<Typography align='center' variant='h6'>
|
||||
{t('Connecting to TOR')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box>
|
||||
<svg width={0} height={0}>
|
||||
<linearGradient id='linearColors' x1={1} y1={0} x2={1} y2={1}>
|
||||
<stop offset={0} stopColor={theme.palette.primary.main} />
|
||||
<stop offset={1} stopColor={theme.palette.secondary.main} />
|
||||
</linearGradient>
|
||||
</svg>
|
||||
<CircularProgress thickness={3} style={{ width: '11.2em', height: '11.2em' }} />
|
||||
<Box sx={{ position: 'fixed', top: '4.6em' }}>
|
||||
<TorIcon
|
||||
sx={{
|
||||
fill: 'url(#linearColors)',
|
||||
width: '6em',
|
||||
height: '6em',
|
||||
position: 'relative',
|
||||
left: '0.7em',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Alert>
|
||||
<AlertTitle>{t('Connection encrypted and anonymized using TOR.')}</AlertTitle>
|
||||
{t(
|
||||
'This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.',
|
||||
)}
|
||||
</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{
|
||||
width: `${width}em`,
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
overflowX: 'clip',
|
||||
}}
|
||||
>
|
||||
{view === 'welcome' ? (
|
||||
<Welcome setView={setView} getGenerateRobot={getGenerateRobot} width={width} />
|
||||
) : null}
|
||||
|
||||
{view === 'onboarding' ? (
|
||||
<Onboarding
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
setPage={setPage}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{view === 'profile' ? (
|
||||
<RobotProfile
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
robotFound={robotFound}
|
||||
setRobot={setRobot}
|
||||
setCurrentOrder={setCurrentOrder}
|
||||
badRequest={badRequest}
|
||||
logoutRobot={logoutRobot}
|
||||
width={width}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
setPage={setPage}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{view === 'recovery' ? (
|
||||
<Recovery
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
setPage={setPage}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
) : null}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RobotPage;
|
@ -1,395 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
Grid,
|
||||
Typography,
|
||||
TextField,
|
||||
ButtonGroup,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
|
||||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||
import CasinoIcon from '@mui/icons-material/Casino';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import { RoboSatsNoTextIcon } from '../components/Icons';
|
||||
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { genBase62Token, tokenStrength, saveAsJson } from '../utils';
|
||||
import { genKey } from '../pgp';
|
||||
import { systemClient } from '../services/System';
|
||||
import { apiClient } from '../services/api/index';
|
||||
import RobotAvatar from '../components/RobotAvatar';
|
||||
|
||||
class UserGenPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tokenHasChanged: false,
|
||||
inputToken: '',
|
||||
found: false,
|
||||
};
|
||||
|
||||
this.refCode = this.props.match.params.refCode;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Checks in parent HomePage if there is already a nick and token
|
||||
// Displays the existing one
|
||||
if (this.props.robot.nickname != null) {
|
||||
this.setState({ inputToken: this.props.robot.token });
|
||||
} else if (this.props.robot.token) {
|
||||
this.setState({ inputToken: this.props.robot.token });
|
||||
this.getGeneratedUser(this.props.robot.token);
|
||||
} else {
|
||||
const newToken = genBase62Token(36);
|
||||
this.setState({
|
||||
inputToken: newToken,
|
||||
});
|
||||
this.getGeneratedUser(newToken);
|
||||
}
|
||||
}
|
||||
|
||||
getGeneratedUser = (token) => {
|
||||
const strength = tokenStrength(token);
|
||||
const refCode = this.refCode;
|
||||
this.props.setRobot({ ...this.props.robot, loading: true, avatarLoaded: false });
|
||||
|
||||
const requestBody = genKey(token).then(function (key) {
|
||||
return {
|
||||
token_sha256: sha256(token),
|
||||
public_key: key.publicKeyArmored,
|
||||
encrypted_private_key: key.encryptedPrivateKeyArmored,
|
||||
unique_values: strength.uniqueValues,
|
||||
counts: strength.counts,
|
||||
length: token.length,
|
||||
ref_code: refCode,
|
||||
};
|
||||
});
|
||||
requestBody.then((body) =>
|
||||
apiClient.post(this.props.baseUrl, '/api/user/', body).then((data) => {
|
||||
this.setState({ found: data.found, bad_request: data.bad_request });
|
||||
this.props.setCurrentOrder(
|
||||
data.active_order_id
|
||||
? data.active_order_id
|
||||
: data.last_order_id
|
||||
? data.last_order_id
|
||||
: null,
|
||||
);
|
||||
// Add nick and token to App state (token only if not a bad request)
|
||||
data.bad_request
|
||||
? this.props.setRobot({
|
||||
...this.props.robot,
|
||||
avatarLoaded: true,
|
||||
loading: false,
|
||||
nickname: data.nickname ?? this.props.robot.nickname,
|
||||
activeOrderId: data.active_order_id ?? null,
|
||||
referralCode: data.referral_code ?? this.props.referralCode,
|
||||
earnedRewards: data.earned_rewards ?? this.props.earnedRewards,
|
||||
lastOrderId: data.last_order_id ?? this.props.lastOrderId,
|
||||
stealthInvoices: data.wants_stealth ?? this.props.stealthInvoices,
|
||||
tgEnabled: data.tg_enabled,
|
||||
tgBotName: data.tg_bot_name,
|
||||
tgToken: data.tg_token,
|
||||
})
|
||||
: this.props.setRobot({
|
||||
...this.props.robot,
|
||||
nickname: data.nickname,
|
||||
token,
|
||||
loading: false,
|
||||
activeOrderId: data.active_order_id ? data.active_order_id : null,
|
||||
lastOrderId: data.last_order_id ? data.last_order_id : null,
|
||||
referralCode: data.referral_code,
|
||||
earnedRewards: data.earned_rewards ?? 0,
|
||||
stealthInvoices: data.wants_stealth,
|
||||
tgEnabled: data.tg_enabled,
|
||||
tgBotName: data.tg_bot_name,
|
||||
tgToken: data.tg_token,
|
||||
bitsEntropy: data.token_bits_entropy,
|
||||
shannonEntropy: data.token_shannon_entropy,
|
||||
pubKey: data.public_key,
|
||||
encPrivKey: data.encrypted_private_key,
|
||||
copiedToken: data.found ? true : this.props.robot.copiedToken,
|
||||
}) &
|
||||
systemClient.setItem('robot_token', token) &
|
||||
systemClient.setItem('pub_key', data.public_key.split('\n').join('\\')) &
|
||||
systemClient.setItem('enc_priv_key', data.encrypted_private_key.split('\n').join('\\'));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
delGeneratedUser() {
|
||||
apiClient.delete(this.props.baseUrl, '/api/user');
|
||||
|
||||
systemClient.deleteCookie('sessionid');
|
||||
systemClient.deleteItem('robot_token');
|
||||
systemClient.deleteItem('pub_key');
|
||||
systemClient.deleteItem('enc_priv_key');
|
||||
}
|
||||
|
||||
handleClickNewRandomToken = () => {
|
||||
const inputToken = genBase62Token(36);
|
||||
this.setState({
|
||||
inputToken,
|
||||
tokenHasChanged: true,
|
||||
});
|
||||
this.props.setRobot({ ...this.props.robot, copiedToken: true });
|
||||
};
|
||||
|
||||
handleChangeToken = (e) => {
|
||||
this.setState({
|
||||
inputToken: e.target.value.split(' ').join(''),
|
||||
tokenHasChanged: true,
|
||||
});
|
||||
};
|
||||
|
||||
handleClickSubmitToken = () => {
|
||||
this.delGeneratedUser();
|
||||
this.getGeneratedUser(this.state.inputToken);
|
||||
this.setState({ tokenHasChanged: false });
|
||||
this.props.setRobot({
|
||||
...this.props.robot,
|
||||
avatarLoaded: false,
|
||||
nickname: null,
|
||||
token: null,
|
||||
copiedToken: false,
|
||||
lastOrderId: null,
|
||||
activeOrderId: null,
|
||||
});
|
||||
};
|
||||
|
||||
createJsonFile = () => {
|
||||
return {
|
||||
token: this.props.robot.token,
|
||||
token_shannon_entropy: this.props.robot.shannonEntropy,
|
||||
token_bit_entropy: this.props.robot.bitsEntropy,
|
||||
public_key: this.props.robot.pub_key,
|
||||
encrypted_private_key: this.props.robot.enc_priv_key,
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, i18n } = this.props;
|
||||
const fontSize = this.props.theme.typography.fontSize;
|
||||
const fontSizeFactor = fontSize / 14; // to scale sizes, default fontSize is 14
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<div className='clickTrough' />
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
align='center'
|
||||
sx={{ width: 370 * fontSizeFactor, height: 260 * fontSizeFactor }}
|
||||
>
|
||||
{this.props.robot.avatarLoaded && this.props.robot.nickname ? (
|
||||
<div>
|
||||
<Grid item xs={12} align='center'>
|
||||
<Typography component='h5' variant='h5'>
|
||||
<b>
|
||||
{this.props.robot.nickname && systemClient.getCookie('sessionid') ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
height: 45 * fontSizeFactor,
|
||||
}}
|
||||
>
|
||||
<BoltIcon
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
height: 33 * fontSizeFactor,
|
||||
width: 33 * fontSizeFactor,
|
||||
}}
|
||||
/>
|
||||
<a>{this.props.robot.nickname}</a>
|
||||
<BoltIcon
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
height: 33 * fontSizeFactor,
|
||||
width: 33 * fontSizeFactor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align='center'>
|
||||
<RobotAvatar
|
||||
nickname={this.props.robot.nickname}
|
||||
smooth={true}
|
||||
style={{ maxWidth: 203 * fontSizeFactor, maxHeight: 203 * fontSizeFactor }}
|
||||
imageStyle={{
|
||||
transform: '',
|
||||
border: '2px solid #555',
|
||||
filter: 'drop-shadow(1px 1px 1px #000000)',
|
||||
height: `${201 * fontSizeFactor}px`,
|
||||
width: `${201 * fontSizeFactor}px`,
|
||||
}}
|
||||
tooltip={t('This is your trading avatar')}
|
||||
tooltipPosition='top'
|
||||
baseUrl={this.props.baseUrl}
|
||||
/>
|
||||
<br />
|
||||
</Grid>
|
||||
</div>
|
||||
) : (
|
||||
<CircularProgress sx={{ position: 'relative', top: 100 }} />
|
||||
)}
|
||||
</Grid>
|
||||
{this.state.found ? (
|
||||
<Grid item xs={12} align='center'>
|
||||
<Typography variant='subtitle2' color='primary'>
|
||||
{this.state.found ? t('A robot avatar was found, welcome back!') : null}
|
||||
<br />
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Grid container align='center'>
|
||||
<Grid item xs={12} align='center'>
|
||||
<TextField
|
||||
sx={{ maxWidth: 280 * fontSizeFactor }}
|
||||
error={!!this.state.bad_request}
|
||||
label={t('Store your token safely')}
|
||||
required={true}
|
||||
value={this.state.inputToken}
|
||||
variant='standard'
|
||||
helperText={this.state.bad_request}
|
||||
size='small'
|
||||
onChange={this.handleChangeToken}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleClickSubmitToken();
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<div
|
||||
style={{
|
||||
width: 50 * fontSizeFactor,
|
||||
minWidth: 50 * fontSizeFactor,
|
||||
position: 'relative',
|
||||
left: -6,
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
enterTouchDelay={250}
|
||||
title={t('Save token and PGP credentials to file')}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
color='primary'
|
||||
disabled={
|
||||
!this.props.robot.avatarLoaded ||
|
||||
!(systemClient.getItem('robot_token') == this.state.inputToken)
|
||||
}
|
||||
onClick={() =>
|
||||
saveAsJson(
|
||||
this.props.robot.nickname + '.json',
|
||||
this.createJsonFile(),
|
||||
)
|
||||
}
|
||||
>
|
||||
<DownloadIcon
|
||||
sx={{ width: 22 * fontSizeFactor, height: 22 * fontSizeFactor }}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<IconButton
|
||||
color={this.props.robot.copiedToken ? 'inherit' : 'primary'}
|
||||
disabled={
|
||||
!this.props.robot.avatarLoaded ||
|
||||
!(systemClient.getItem('robot_token') === this.state.inputToken)
|
||||
}
|
||||
onClick={() =>
|
||||
systemClient.copyToClipboard(systemClient.getItem('robot_token')) &
|
||||
this.props.setRobot({ ...this.props.robot, copiedToken: true })
|
||||
}
|
||||
>
|
||||
<ContentCopy
|
||||
sx={{ width: 18 * fontSizeFactor, height: 18 * fontSizeFactor }}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
),
|
||||
endAdornment: (
|
||||
<Tooltip enterTouchDelay={250} title={t('Generate a new token')}>
|
||||
<IconButton onClick={this.handleClickNewRandomToken}>
|
||||
<CasinoIcon
|
||||
sx={{ width: 18 * fontSizeFactor, height: 18 * fontSizeFactor }}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} align='center'>
|
||||
{this.state.tokenHasChanged ? (
|
||||
<Button type='submit' size='small' onClick={this.handleClickSubmitToken}>
|
||||
<SmartToyIcon sx={{ width: 18 * fontSizeFactor, height: 18 * fontSizeFactor }} />
|
||||
<span> {t('Generate Robot')}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t('You must enter a new token first')}
|
||||
>
|
||||
<div>
|
||||
<Button disabled={true} type='submit' size='small'>
|
||||
<SmartToyIcon sx={{ width: 18 * fontSizeFactor, height: 18 * fontSizeFactor }} />
|
||||
<span>{t('Generate Robot')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align='center' sx={{ width: '26.43em' }}>
|
||||
<Grid item>
|
||||
<div style={{ height: '2.143em' }} />
|
||||
</Grid>
|
||||
<div style={{ width: '26.43em', left: '2.143em' }}>
|
||||
<Grid container align='center'>
|
||||
<Grid item xs={0.8} />
|
||||
<Grid item xs={7.5} align='right'>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Simple and Private LN P2P Exchange')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2.5} align='left'>
|
||||
<RoboSatsNoTextIcon color='primary' sx={{ height: '3.143em', width: '3.143em' }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(UserGenPage);
|
@ -147,7 +147,7 @@ const ProfileDialog = ({
|
||||
</div>
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Your Profile')}
|
||||
{t('Your Robot')}
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
|
@ -3,7 +3,7 @@ import { SvgIcon } from '@mui/material';
|
||||
|
||||
export default function RoboSatsTextIcon(props) {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' width='2000px' height='1000px' viewBox='0 300 2000 150'>
|
||||
<SvgIcon {...props} x='0px' y='0px' width='2000px' height='500px' viewBox='0 620 2000 1'>
|
||||
<g>
|
||||
<path
|
||||
d='M455.556,849.519c10.487-10.606,18.315-22.243,23.484-35.499c11.767-30.177,10.624-59.483-6.55-87.546
|
||||
|
@ -212,13 +212,13 @@ const Notifications = ({
|
||||
setMessage(message);
|
||||
setShow(true);
|
||||
setTimeout(() => setShow(false), message.timeout);
|
||||
if (message.sound) {
|
||||
if (message.sound != null) {
|
||||
message.sound.play();
|
||||
}
|
||||
if (!inFocus) {
|
||||
setTitleAnimation(
|
||||
setInterval(function () {
|
||||
var title = document.title;
|
||||
const title = document.title;
|
||||
document.title = title == basePageTitle ? message.pageTitle : basePageTitle;
|
||||
}, 1000),
|
||||
);
|
||||
|
BIN
frontend/src/components/RobotAvatar/generatingCompressed.gif
Normal file
BIN
frontend/src/components/RobotAvatar/generatingCompressed.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 388 KiB After Width: | Height: | Size: 388 KiB |
@ -12,6 +12,7 @@ interface Props {
|
||||
flipHorizontally?: boolean;
|
||||
style?: object;
|
||||
imageStyle?: object;
|
||||
placeholderType?: 'loading' | 'generating';
|
||||
statusColor?: 'primary' | 'secondary' | 'default' | 'error' | 'info' | 'success' | 'warning';
|
||||
orderType?: number;
|
||||
tooltip?: string;
|
||||
@ -29,6 +30,7 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
tooltipPosition = 'right',
|
||||
smooth = false,
|
||||
flipHorizontally = false,
|
||||
placeholderType = 'loading',
|
||||
style = {},
|
||||
avatarClass = 'flippedSmallAvatar',
|
||||
imageStyle = {},
|
||||
@ -39,6 +41,16 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
const theme = useTheme();
|
||||
const [avatarSrc, setAvatarSrc] = useState<string>();
|
||||
|
||||
const backgroundData =
|
||||
placeholderType == 'generating' ? placeholder.generating : placeholder.loading;
|
||||
const backgroundImage = `url(data:${backgroundData.mime};base64,${backgroundData.data})`;
|
||||
const className =
|
||||
placeholderType == 'loading'
|
||||
? theme.palette.mode === 'dark'
|
||||
? 'loadingAvatarDark'
|
||||
: 'loadingAvatar'
|
||||
: 'generatingAvatar';
|
||||
|
||||
useEffect(() => {
|
||||
if (nickname != undefined) {
|
||||
if (window.NativeRobosats === undefined) {
|
||||
@ -76,10 +88,10 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
transform: flipHorizontally ? 'scaleX(-1)' : '',
|
||||
border: '0.3px solid #55555',
|
||||
filter: 'dropShadow(0.5px 0.5px 0.5px #000000)',
|
||||
backgroundImage: `url(data:${placeholder.image.mime};base64,${placeholder.image.data})`,
|
||||
backgroundImage,
|
||||
}}
|
||||
>
|
||||
<div className={theme.palette.mode === 'dark' ? 'loadingAvatarDark' : 'loadingAvatar'}>
|
||||
<div className={className}>
|
||||
<SmoothImage
|
||||
src={avatarSrc}
|
||||
imageStyles={{
|
||||
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
File diff suppressed because one or more lines are too long
10
frontend/src/components/RobotAvatar/placeholder_highres.json
Normal file
10
frontend/src/components/RobotAvatar/placeholder_highres.json
Normal file
File diff suppressed because one or more lines are too long
@ -2,16 +2,20 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Box, CircularProgress, Tooltip } from '@mui/material';
|
||||
import { TorIcon } from './Icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { gridQuickFilterValuesSelector } from '@mui/x-data-grid';
|
||||
|
||||
interface Props {
|
||||
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 }: Props): JSX.Element => {
|
||||
const TorIndicator = ({
|
||||
color,
|
||||
tooltipOpen = undefined,
|
||||
title,
|
||||
progress,
|
||||
}: TorIndicatorProps): JSX.Element => {
|
||||
return (
|
||||
<Tooltip
|
||||
open={tooltipOpen}
|
||||
@ -49,15 +53,12 @@ const TorIndicator = ({ color, tooltipOpen = undefined, title, progress }: Props
|
||||
);
|
||||
};
|
||||
|
||||
const TorConnection = (): JSX.Element => {
|
||||
const [torStatus, setTorStatus] = useState<string>('NOTINIT');
|
||||
const { t } = useTranslation();
|
||||
interface TorConnectionBadgeProps {
|
||||
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('torStatus', (event) => {
|
||||
setTorStatus(event?.detail);
|
||||
});
|
||||
}, []);
|
||||
const TorConnectionBadge = ({ torStatus }: TorConnectionBadgeProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (window?.NativeRobosats == null) {
|
||||
return <></>;
|
||||
@ -95,4 +96,4 @@ const TorConnection = (): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
export default TorConnection;
|
||||
export default TorConnectionBadge;
|
||||
|
@ -102,7 +102,7 @@ export const LightningPayoutForm = ({
|
||||
const validateInvoice = function (invoice: string, targetAmount: number) {
|
||||
try {
|
||||
const decoded = decode(invoice);
|
||||
const invoiceAmount = Math.floor(decoded['sections'][2]['value'] / 1000);
|
||||
const invoiceAmount = Math.floor(decoded.sections[2].value / 1000);
|
||||
if (targetAmount != invoiceAmount) {
|
||||
return 'Invalid invoice amount';
|
||||
} else {
|
||||
@ -571,7 +571,7 @@ export const LightningPayoutForm = ({
|
||||
style: { textAlign: 'center', maxHeight: '8em' },
|
||||
}}
|
||||
variant={lightning.useLnproxy ? 'filled' : 'standard'}
|
||||
multiline={lightning.useLnproxy ? false : true}
|
||||
multiline={!lightning.useLnproxy}
|
||||
minRows={3}
|
||||
maxRows={5}
|
||||
onChange={(e) => setLightning({ ...lightning, invoice: e.target.value ?? '' })}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import {
|
||||
Grid,
|
||||
Typography,
|
||||
@ -16,7 +16,6 @@ import currencies from '../../../../static/assets/currencies.json';
|
||||
import TradeSummary from '../TradeSummary';
|
||||
import { Favorite, RocketLaunch, ContentCopy, Refresh } from '@mui/icons-material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { Order } from '../../../models';
|
||||
import { systemClient } from '../../../services/System';
|
||||
|
@ -56,12 +56,6 @@ body {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.clickTrough {
|
||||
height: 50px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* No arrows on numeric inputs */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
@ -228,3 +222,11 @@ input[type='number'] {
|
||||
background-repeat: repeat;
|
||||
animation: animatedBackground 5s linear infinite;
|
||||
}
|
||||
|
||||
.generatingAvatar {
|
||||
background-size: 100%;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #555;
|
||||
outline-offset: -2px;
|
||||
filter: dropShadow(1px 1px 1px #000000);
|
||||
}
|
||||
|
@ -46,6 +46,18 @@ const configMobile: Configuration = {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'),
|
||||
loader: 'file-replace-loader',
|
||||
options: {
|
||||
condition: 'if-replacement-exists',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'src/components/RobotAvatar/placeholder_highres.json',
|
||||
),
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
Loading…
Reference in New Issue
Block a user