refactor login (clean separation robot/info. Style navbar.

This commit is contained in:
Reckless_Satoshi 2022-10-29 13:36:59 -07:00
parent b9dc7f7c95
commit c660a5b0d1
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
17 changed files with 276 additions and 214 deletions

View File

@ -534,22 +534,23 @@ class UserGenSerializer(serializers.Serializer):
required=True, required=True,
help_text="SHA256 of user secret", help_text="SHA256 of user secret",
) )
# Optional fields
# (PGP keys are mandatory for new users, but optional for logins)
public_key = serializers.CharField( public_key = serializers.CharField(
max_length=2000, max_length=2000,
allow_null=False, allow_null=False,
allow_blank=False, allow_blank=False,
required=True, required=False,
help_text="Armored ASCII PGP public key block", help_text="Armored ASCII PGP public key block",
) )
encrypted_private_key = serializers.CharField( encrypted_private_key = serializers.CharField(
max_length=2000, max_length=2000,
allow_null=False, allow_null=False,
allow_blank=False, allow_blank=False,
required=True, required=False,
help_text="Armored ASCII PGP encrypted private key block", help_text="Armored ASCII PGP encrypted private key block",
) )
# Optional fields
ref_code = serializers.CharField( ref_code = serializers.CharField(
max_length=30, max_length=30,
allow_null=True, allow_null=True,

View File

@ -653,19 +653,6 @@ class UserView(APIView):
encrypted_private_key = serializer.data.get("encrypted_private_key") encrypted_private_key = serializer.data.get("encrypted_private_key")
ref_code = serializer.data.get("ref_code") ref_code = serializer.data.get("ref_code")
if not public_key or not encrypted_private_key:
context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
return Response(context, status.HTTP_400_BAD_REQUEST)
(
valid,
bad_keys_context,
public_key,
encrypted_private_key,
) = Logics.validate_pgp_keys(public_key, encrypted_private_key)
if not valid:
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
# Now the server only receives a hash of the token. So server trusts the client # Now the server only receives a hash of the token. So server trusts the client
# with computing length, counts and unique_values to confirm the high entropy of the token # with computing length, counts and unique_values to confirm the high entropy of the token
# In any case, it is up to the client if they want to create a bad high entropy token. # In any case, it is up to the client if they want to create a bad high entropy token.
@ -712,6 +699,20 @@ class UserView(APIView):
# Create new credentials and login if nickname is new # Create new credentials and login if nickname is new
if len(User.objects.filter(username=nickname)) == 0: if len(User.objects.filter(username=nickname)) == 0:
if not public_key or not encrypted_private_key:
context[
"bad_request"
] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
return Response(context, status.HTTP_400_BAD_REQUEST)
(
valid,
bad_keys_context,
public_key,
encrypted_private_key,
) = Logics.validate_pgp_keys(public_key, encrypted_private_key)
if not valid:
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
User.objects.create_user( User.objects.create_user(
username=nickname, password=token_sha256, is_staff=False username=nickname, password=token_sha256, is_staff=False
) )
@ -931,25 +932,6 @@ class InfoView(ListAPIView):
BalanceLog.objects.latest("time") BalanceLog.objects.latest("time")
) )
if request.user.is_authenticated:
context["nickname"] = request.user.username
context["referral_code"] = str(request.user.profile.referral_code)
context["earned_rewards"] = request.user.profile.earned_rewards
context["wants_stealth"] = request.user.profile.wants_stealth
# Adds/generate telegram token and whether it is enabled
context = {**context, **Telegram.get_context(request.user)}
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user
)
if not has_no_active_order:
context["active_order_id"] = order.id
else:
last_order = Order.objects.filter(
Q(maker=request.user) | Q(taker=request.user)
).last()
if last_order:
context["last_order_id"] = last_order.id
return Response(context, status.HTTP_200_OK) return Response(context, status.HTTP_200_OK)

View File

@ -7,7 +7,7 @@ import MakerPage from './MakerPage';
import BookPage from './BookPage'; import BookPage from './BookPage';
import OrderPage from './OrderPage'; import OrderPage from './OrderPage';
import SettingsPage from './SettingsPage'; import SettingsPage from './SettingsPage';
import NavBar from './NavBar'; import NavBar, { Page } from './NavBar';
import MainDialogs, { OpenDialogs } from './MainDialogs'; import MainDialogs, { OpenDialogs } from './MainDialogs';
import { apiClient } from '../services/api'; import { apiClient } from '../services/api';
@ -25,6 +25,8 @@ import {
defaultRobot, defaultRobot,
defaultInfo, defaultInfo,
} from '../models'; } from '../models';
import { sha256 } from 'js-sha256';
import RobotAvatar from '../components/RobotAvatar';
const getWindowSize = function (fontSize: number) { const getWindowSize = function (fontSize: number) {
// returns window size in EM units // returns window size in EM units
@ -34,7 +36,6 @@ const getWindowSize = function (fontSize: number) {
}; };
}; };
type Page = 'robot' | 'order' | 'create' | 'offers' | 'settings' | 'none';
interface SlideDirection { interface SlideDirection {
in: 'left' | 'right' | undefined; in: 'left' | 'right' | undefined;
out: 'left' | 'right' | undefined; out: 'left' | 'right' | undefined;
@ -66,6 +67,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
in: undefined, in: undefined,
out: undefined, out: undefined,
}); });
const [order, setOrder] = useState<number | null>(null);
const navbarHeight = 2.5; const navbarHeight = 2.5;
const closeAll = { const closeAll = {
@ -76,6 +78,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
coordinator: false, coordinator: false,
stats: false, stats: false,
update: false, update: false,
profile: false,
}; };
const [open, setOpen] = useState<OpenDialogs>(closeAll); const [open, setOpen] = useState<OpenDialogs>(closeAll);
@ -133,27 +136,60 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
coordinatorVersion: versionInfo.coordinatorVersion, coordinatorVersion: versionInfo.coordinatorVersion,
clientVersion: versionInfo.clientVersion, clientVersion: versionInfo.clientVersion,
}); });
if (!robot.nickname && data.nickname && !robot.loading) {
setRobot({
...robot,
nickname: data.nickname,
loading: false,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
referralCode: data.referral_code,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
});
}
}); });
}; };
console.log(page, slideDirection); const fetchRobot = function () {
const requestBody = {
token_sha256: sha256(robot.token),
};
apiClient.post('/api/user/', requestBody).then((data: any) => {
console.log(data);
setOrder(
data.active_order_id
? data.active_order_id
: data.last_order_id
? data.last_order_id
: order,
);
setRobot({
...robot,
nickname: data.nickname,
token: robot.token,
loading: false,
avatarLoaded: 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,
pub_key: data.public_key,
enc_priv_key: data.encrypted_private_key,
copiedToken: data.found ? true : robot.copiedToken,
});
});
};
useEffect(() => {
if (robot.token && robot.nickname === null) {
fetchRobot();
}
}, []);
return ( return (
<Router basename={basename}> <Router basename={basename}>
{/* load robot avatar image, set avatarLoaded: true */}
<RobotAvatar
style={{ display: 'none' }}
nickname={robot.avatarLoaded ? robot.nickname : null}
onLoad={() => setRobot({ ...robot, avatarLoaded: true })}
/>
<Box <Box
style={{ style={{
position: 'absolute', position: 'absolute',
@ -167,13 +203,25 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
exact exact
path='/' path='/'
render={(props: any) => ( render={(props: any) => (
<UserGenPage match={props.match} theme={theme} robot={robot} setRobot={setRobot} /> <UserGenPage
setPage={setPage}
match={props.match}
theme={theme}
robot={robot}
setRobot={setRobot}
/>
)} )}
/> />
<Route <Route
path='/robot/:refCode?' path='/robot/:refCode?'
render={(props: any) => ( render={(props: any) => (
<UserGenPage match={props.match} theme={theme} robot={robot} setRobot={setRobot} /> <UserGenPage
setPage={setPage}
match={props.match}
theme={theme}
robot={robot}
setRobot={setRobot}
/>
)} )}
/> />
@ -195,6 +243,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
setMaker={setMaker} setMaker={setMaker}
lastDayPremium={info.last_day_nonkyc_btc_premium} lastDayPremium={info.last_day_nonkyc_btc_premium}
windowSize={windowSize} windowSize={windowSize}
hasRobot={robot.avatarLoaded}
/> />
</div> </div>
</Slide> </Slide>
@ -213,9 +262,12 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
fetchLimits={fetchLimits} fetchLimits={fetchLimits}
maker={maker} maker={maker}
setMaker={setMaker} setMaker={setMaker}
setPage={setPage}
setOrder={setOrder}
fav={fav} fav={fav}
setFav={setFav} setFav={setFav}
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }} windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
hasRobot={robot.avatarLoaded}
/> />
</div> </div>
</Slide> </Slide>
@ -244,6 +296,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
</Switch> </Switch>
</Box> </Box>
<NavBar <NavBar
nickname={robot.nickname}
width={windowSize.width} width={windowSize.width}
height={navbarHeight} height={navbarHeight}
page={page} page={page}
@ -252,13 +305,17 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
setOpen={setOpen} setOpen={setOpen}
closeAll={closeAll} closeAll={closeAll}
setSlideDirection={setSlideDirection} setSlideDirection={setSlideDirection}
robot={robot} order={order}
hasRobot={robot.avatarLoaded}
/>
<MainDialogs
open={open}
setOpen={setOpen}
setRobot={setRobot} setRobot={setRobot}
info={info} info={info}
setInfo={setInfo} robot={robot}
fetchInfo={fetchInfo} closeAll={closeAll}
/> />
<MainDialogs open={open} setOpen={setOpen} info={info} closeAll={closeAll} />
</Router> </Router>
); );
}; };

View File

@ -1,12 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { Info, Robot } from '../../models';
import { useTheme, styled, Grid, IconButton } from '@mui/material';
import { Info } from '../../models';
import { import {
CommunityDialog, CommunityDialog,
CoordinatorSummaryDialog, CoordinatorSummaryDialog,
InfoDialog, InfoDialog,
LearnDialog, LearnDialog,
ProfileDialog,
StatsDialog, StatsDialog,
UpdateClientDialog, UpdateClientDialog,
} from '../../components/Dialogs'; } from '../../components/Dialogs';
@ -19,19 +18,26 @@ export interface OpenDialogs {
coordinator: boolean; coordinator: boolean;
stats: boolean; stats: boolean;
update: boolean; update: boolean;
profile: boolean; // temporary until new Robot Page is ready
} }
interface MainDialogsProps { interface MainDialogsProps {
open: OpenDialogs; open: OpenDialogs;
setOpen: (state: OpenDialogs) => void; setOpen: (state: OpenDialogs) => void;
info: Info; info: Info;
robot: Robot;
setRobot: (state: Robot) => void;
closeAll: OpenDialogs; closeAll: OpenDialogs;
} }
const MainDialogs = ({ open, setOpen, info, closeAll }: MainDialogsProps): JSX.Element => { const MainDialogs = ({
const { t } = useTranslation(); open,
const theme = useTheme(); setOpen,
info,
closeAll,
robot,
setRobot,
}: MainDialogsProps): JSX.Element => {
useEffect(() => { useEffect(() => {
if (info.openUpdateClient) { if (info.openUpdateClient) {
setOpen({ ...closeAll, update: true }); setOpen({ ...closeAll, update: true });
@ -83,6 +89,12 @@ const MainDialogs = ({ open, setOpen, info, closeAll }: MainDialogsProps): JSX.E
lastDayVolume={info.last_day_volume} lastDayVolume={info.last_day_volume}
lifetimeVolume={info.lifetime_volume} lifetimeVolume={info.lifetime_volume}
/> />
<ProfileDialog
open={open.profile}
onClose={() => setOpen({ ...open, profile: false })}
robot={robot}
setRobot={setRobot}
/>
</> </>
); );
}; };

View File

@ -8,6 +8,7 @@ import { filterOrders } from '../../utils';
import MakerForm from '../../components/MakerForm'; import MakerForm from '../../components/MakerForm';
import BookTable from '../../components/BookTable'; import BookTable from '../../components/BookTable';
import { Page } from '../NavBar';
interface MakerPageProps { interface MakerPageProps {
limits: { list: LimitList; loading: boolean }; limits: { list: LimitList; loading: boolean };
@ -18,6 +19,9 @@ interface MakerPageProps {
setFav: (state: Favorites) => void; setFav: (state: Favorites) => void;
setMaker: (state: Maker) => void; setMaker: (state: Maker) => void;
windowSize: { width: number; height: number }; windowSize: { width: number; height: number };
hasRobot: boolean;
setOrder: (state: number) => void;
setPage: (state: Page) => void;
} }
const MakerPage = ({ const MakerPage = ({
@ -29,6 +33,9 @@ const MakerPage = ({
setFav, setFav,
setMaker, setMaker,
windowSize, windowSize,
setOrder,
setPage,
hasRobot = false,
}: MakerPageProps): JSX.Element => { }: MakerPageProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -86,6 +93,10 @@ const MakerPage = ({
setFav={setFav} setFav={setFav}
maker={maker} maker={maker}
setMaker={setMaker} setMaker={setMaker}
onOrderCreated={(id) => {
setOrder(id);
setPage('order');
}}
disableRequest={matches.length > 0 && !showMatches} disableRequest={matches.length > 0 && !showMatches}
collapseAll={showMatches} collapseAll={showMatches}
onSubmit={() => setShowMatches(matches.length > 0)} onSubmit={() => setShowMatches(matches.length > 0)}

View File

@ -19,24 +19,21 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
}, },
})); }));
export interface OpenDialogs {
more: boolean;
learn: boolean;
community: boolean;
info: boolean;
coordinator: boolean;
stats: boolean;
update: boolean;
}
interface MoreTooltipProps { interface MoreTooltipProps {
open: OpenDialogs; open: OpenDialogs;
nickname: string | null;
setOpen: (state: OpenDialogs) => void; setOpen: (state: OpenDialogs) => void;
closeAll: OpenDialogs; closeAll: OpenDialogs;
children: JSX.Element; children: JSX.Element;
} }
const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): JSX.Element => { const MoreTooltip = ({
open,
setOpen,
closeAll,
nickname,
children,
}: MoreTooltipProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
return ( return (
@ -56,7 +53,7 @@ const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): J
sx={{ sx={{
color: open.info ? theme.palette.primary.main : theme.palette.text.secondary, color: open.info ? theme.palette.primary.main : theme.palette.text.secondary,
}} }}
onClick={() => setOpen({ ...closeAll, more: true, info: !open.info })} onClick={() => setOpen({ ...closeAll, info: !open.info })}
> >
<Info /> <Info />
</IconButton> </IconButton>
@ -69,7 +66,7 @@ const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): J
sx={{ sx={{
color: open.learn ? theme.palette.primary.main : theme.palette.text.secondary, color: open.learn ? theme.palette.primary.main : theme.palette.text.secondary,
}} }}
onClick={() => setOpen({ ...closeAll, more: true, learn: !open.learn })} onClick={() => setOpen({ ...closeAll, learn: !open.learn })}
> >
<School /> <School />
</IconButton> </IconButton>
@ -86,7 +83,7 @@ const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): J
sx={{ sx={{
color: open.community ? theme.palette.primary.main : theme.palette.text.secondary, color: open.community ? theme.palette.primary.main : theme.palette.text.secondary,
}} }}
onClick={() => setOpen({ ...closeAll, more: true, community: !open.community })} onClick={() => setOpen({ ...closeAll, community: !open.community })}
> >
<People /> <People />
</IconButton> </IconButton>
@ -101,7 +98,7 @@ const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): J
? theme.palette.primary.main ? theme.palette.primary.main
: theme.palette.text.secondary, : theme.palette.text.secondary,
}} }}
onClick={() => setOpen({ ...closeAll, more: true, coordinator: !open.coordinator })} onClick={() => setOpen({ ...closeAll, coordinator: !open.coordinator })}
> >
<PriceChange /> <PriceChange />
</IconButton> </IconButton>
@ -114,7 +111,7 @@ const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): J
sx={{ sx={{
color: open.stats ? theme.palette.primary.main : theme.palette.text.secondary, color: open.stats ? theme.palette.primary.main : theme.palette.text.secondary,
}} }}
onClick={() => setOpen({ ...closeAll, more: true, stats: !open.stats })} onClick={() => setOpen({ ...closeAll, stats: !open.stats })}
> >
<BubbleChart /> <BubbleChart />
</IconButton> </IconButton>

View File

@ -2,11 +2,12 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Tabs, Tab, Paper, useTheme } from '@mui/material'; import { Tabs, Tab, Paper, useTheme } from '@mui/material';
import RobotAvatar from '../../components/RobotAvatar';
import MoreTooltip from './MoreTooltip'; import MoreTooltip from './MoreTooltip';
import { OpenDialogs } from '../MainDialogs'; import { OpenDialogs } from '../MainDialogs';
import { Page } from '.';
import { import {
SettingsApplications, SettingsApplications,
SmartToy, SmartToy,
@ -15,12 +16,13 @@ import {
Assignment, Assignment,
MoreHoriz, MoreHoriz,
} from '@mui/icons-material'; } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar';
type Page = 'robot' | 'offers' | 'create' | 'order' | 'settings' | 'none';
type Direction = 'left' | 'right' | undefined; type Direction = 'left' | 'right' | undefined;
interface NavBarProps { interface NavBarProps {
page: Page; page: Page;
nickname?: string | null;
setPage: (state: Page) => void; setPage: (state: Page) => void;
slideDirection: { in: Direction; out: Direction }; slideDirection: { in: Direction; out: Direction };
setSlideDirection: (state: { in: Direction; out: Direction }) => void; setSlideDirection: (state: { in: Direction; out: Direction }) => void;
@ -29,6 +31,8 @@ interface NavBarProps {
open: OpenDialogs; open: OpenDialogs;
setOpen: (state: OpenDialogs) => void; setOpen: (state: OpenDialogs) => void;
closeAll: OpenDialogs; closeAll: OpenDialogs;
order: number | null;
hasRobot: boolean;
} }
const NavBar = ({ const NavBar = ({
@ -37,20 +41,24 @@ const NavBar = ({
slideDirection, slideDirection,
setSlideDirection, setSlideDirection,
open, open,
nickname = null,
setOpen, setOpen,
closeAll, closeAll,
width, width,
height, height,
order,
hasRobot = false,
}: NavBarProps): JSX.Element => { }: NavBarProps): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const smallBar = width < 50; const smallBar = width < 50;
const tabWidth = smallBar ? 1 : 12;
const [newPage, setNewPage] = useState<Page>(history.location.pathname.split('/')[1]); const [newPage, setNewPage] = useState<Page>(history.location.pathname.split('/')[1]);
const tabSx = smallBar ? {} : { position: 'relative', bottom: '1em' }; const tabSx = smallBar
? { position: 'relative', bottom: nickname ? '0.8em' : '0em', minWidth: '1em' }
: { position: 'relative', bottom: '1em', minWidth: '2em' };
const pagesPosition = { const pagesPosition = {
robot: 1, robot: 1,
offers: 2, offers: 2,
@ -73,7 +81,11 @@ const NavBar = ({
} else { } else {
handleSlideDirection(page, newPage); handleSlideDirection(page, newPage);
setNewPage(newPage); setNewPage(newPage);
setTimeout(() => history.push(`/${newPage}`), theme.transitions.duration.leavingScreen * 3); const param = newPage === 'order' ? order ?? '' : '';
setTimeout(
() => history.push(`/${newPage}/${param}`),
theme.transitions.duration.leavingScreen * 3,
);
} }
}; };
@ -92,16 +104,30 @@ const NavBar = ({
> >
<Tabs <Tabs
TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }} TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }}
variant={smallBar ? 'scrollable' : 'fullWidth'} variant='fullWidth'
centered={!smallBar}
allowScrollButtonsMobile
scrollButtons={smallBar}
value={page} value={page}
onChange={changePage} onChange={changePage}
> >
<Tab
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
value='none'
onClick={() => setOpen({ ...closeAll, profile: !open.profile })}
icon={
nickname ? (
<RobotAvatar
style={{ width: '2.3em', height: '2.3em' }}
avatarClass='phoneFlippedSmallAvatar'
nickname={nickname}
/>
) : (
<></>
)
}
/>
<Tab <Tab
label={smallBar ? undefined : t('Robot')} label={smallBar ? undefined : t('Robot')}
sx={tabSx} sx={{ ...tabSx, minWidth: '1em' }}
value='robot' value='robot'
icon={<SmartToy />} icon={<SmartToy />}
iconPosition='start' iconPosition='start'
@ -124,7 +150,8 @@ const NavBar = ({
<Tab <Tab
sx={tabSx} sx={tabSx}
label={smallBar ? undefined : t('Order')} label={smallBar ? undefined : t('Order')}
value='order/1' value='order'
disabled={!hasRobot}
icon={<Assignment />} icon={<Assignment />}
iconPosition='start' iconPosition='start'
/> />
@ -136,18 +163,20 @@ const NavBar = ({
iconPosition='start' iconPosition='start'
/> />
<MoreTooltip open={open} setOpen={setOpen} closeAll={closeAll}>
<Tab <Tab
sx={tabSx} sx={tabSx}
label={smallBar ? undefined : t('More')} label={smallBar ? undefined : t('More')}
value={'none'} value='none'
onClick={() => { onClick={(e) => {
setOpen(open.more ? closeAll : { ...open, more: true }); open.more ? null : setOpen({ ...open, more: true });
}} }}
icon={<MoreHoriz />} icon={
<MoreTooltip open={open} nickname={nickname} setOpen={setOpen} closeAll={closeAll}>
<MoreHoriz />
</MoreTooltip>
}
iconPosition='start' iconPosition='start'
/> />
</MoreTooltip>
</Tabs> </Tabs>
</Paper> </Paper>
); );

View File

@ -1,4 +1,5 @@
import NavBar from './NavBar'; import NavBar from './NavBar';
export type Page = 'robot' | 'order' | 'create' | 'offers' | 'settings' | 'none';
export type { OpenDialogs } from './MoreTooltip'; export type { OpenDialogs } from './MoreTooltip';
export default NavBar; export default NavBar;

View File

@ -18,7 +18,7 @@ const SettingsPage = ({ settings, setSettings, windowSize }: SettingsPageProps):
return ( return (
<Paper <Paper
elevation={12} elevation={12}
sx={{ padding: '0.6em', width: '17.25em', maxHeight: `${maxHeight}em`, overflow: 'auto' }} sx={{ padding: '0.6em', width: '18em', maxHeight: `${maxHeight}em`, overflow: 'auto' }}
> >
<Grid container> <Grid container>
<Grid item> <Grid item>

View File

@ -10,8 +10,6 @@ import {
CircularProgress, CircularProgress,
IconButton, IconButton,
} from '@mui/material'; } from '@mui/material';
import { Link } from 'react-router-dom';
import { InfoDialog } from '../components/Dialogs';
import SmartToyIcon from '@mui/icons-material/SmartToy'; import SmartToyIcon from '@mui/icons-material/SmartToy';
import CasinoIcon from '@mui/icons-material/Casino'; import CasinoIcon from '@mui/icons-material/Casino';
@ -72,6 +70,7 @@ class UserGenPage extends Component {
ref_code: refCode, ref_code: refCode,
}; };
}); });
requestBody.then((body) => console.log(body));
requestBody.then((body) => requestBody.then((body) =>
apiClient.post('/api/user/', body).then((data) => { apiClient.post('/api/user/', body).then((data) => {
this.setState({ found: data.found, bad_request: data.bad_request }); this.setState({ found: data.found, bad_request: data.bad_request });
@ -87,6 +86,9 @@ class UserGenPage extends Component {
earnedRewards: data.earned_rewards ?? this.props.eartnedRewards, earnedRewards: data.earned_rewards ?? this.props.eartnedRewards,
lastOrderId: data.last_order_id ?? this.props.lastOrderId, lastOrderId: data.last_order_id ?? this.props.lastOrderId,
stealthInvoices: data.wants_stealth ?? this.props.stealthInvoices, 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.setRobot({
...this.props.robot, ...this.props.robot,
@ -359,36 +361,6 @@ class UserGenPage extends Component {
</Tooltip> </Tooltip>
)} )}
</Grid> </Grid>
<Grid item xs={12} align='center'>
<ButtonGroup variant='contained' aria-label='outlined primary button group'>
<Button
disabled={
this.props.robot.loading ||
!(this.props.robot.token
? systemClient.getCookie('robot_token') === this.props.robot.token
: true)
}
color='primary'
to='/make/'
component={Link}
>
{t('Make Order')}
</Button>
<Button
disabled={
this.props.robot.loading ||
!(this.props.robot.token
? systemClient.getCookie('robot_token') == this.props.robot.token
: true)
}
color='secondary'
to='/book/'
component={Link}
>
{t('View Book')}
</Button>
</ButtonGroup>
</Grid>
<Grid item xs={12} align='center' sx={{ width: '26.43em' }}> <Grid item xs={12} align='center' sx={{ width: '26.43em' }}>
<Grid item> <Grid item>

View File

@ -39,52 +39,26 @@ import { UserNinjaIcon, BitcoinIcon } from '../Icons';
import { systemClient } from '../../services/System'; import { systemClient } from '../../services/System';
import { getHost, getWebln } from '../../utils'; import { getHost, getWebln } from '../../utils';
import RobotAvatar from '../RobotAvatar'; import RobotAvatar from '../RobotAvatar';
import { apiClient } from '../../services/api';
import { Robot } from '../../models';
interface Props { interface Props {
open: boolean; open: boolean;
handleClickCloseProfile: () => void; onClose: () => void;
nickname: string; robot: Robot;
activeOrderId: string | number; setRobot: (state: Robot) => void;
lastOrderId: string | number;
referralCode: string;
tgEnabled: boolean;
tgBotName: string;
tgToken: string;
handleSubmitInvoiceClicked: (e: any, invoice: string) => void;
showRewardsSpinner: boolean;
withdrawn: boolean;
badInvoice: boolean | string;
earnedRewards: number;
stealthInvoices: boolean;
handleSetStealthInvoice: (wantsStealth: boolean) => void;
updateRobot: (state: any) => void; // TODO: move to a ContextProvider
} }
const ProfileDialog = ({ const ProfileDialog = ({ open = false, onClose, robot, setRobot }: Props): JSX.Element => {
open = false,
handleClickCloseProfile,
nickname,
activeOrderId,
lastOrderId,
referralCode,
tgEnabled,
tgBotName,
tgToken,
handleSubmitInvoiceClicked,
showRewardsSpinner,
withdrawn,
badInvoice,
earnedRewards,
updateRobot,
stealthInvoices,
handleSetStealthInvoice,
}: Props): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const host = getHost(); const host = getHost();
const [rewardInvoice, setRewardInvoice] = useState<string>(''); const [rewardInvoice, setRewardInvoice] = useState<string>('');
const [showRewards, setShowRewards] = useState<boolean>(false); const [showRewards, setShowRewards] = useState<boolean>(false);
const [showRewardsSpinner, setShowRewardsSpinner] = useState<boolean>(false);
const [withdrawn, setWithdrawn] = useState<boolean>(false);
const [badInvoice, setBadInvoice] = useState<string>('');
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false); const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false); const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false); const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
@ -100,19 +74,19 @@ const ProfileDialog = ({
if (robotToken) { if (robotToken) {
systemClient.copyToClipboard(robotToken); systemClient.copyToClipboard(robotToken);
updateRobot({ copiedToken: true }); setRobot({ ...robot, copiedToken: true });
} }
}; };
const copyReferralCodeHandler = () => { const copyReferralCodeHandler = () => {
systemClient.copyToClipboard(`http://${host}/ref/${referralCode}`); systemClient.copyToClipboard(`http://${host}/ref/${robot.referralCode}`);
}; };
const handleWeblnInvoiceClicked = async (e: any) => { const handleWeblnInvoiceClicked = async (e: any) => {
e.preventDefault(); e.preventDefault();
if (earnedRewards) { if (robot.earnedRewards) {
const webln = await getWebln(); const webln = await getWebln();
const invoice = webln.makeInvoice(earnedRewards).then(() => { const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
if (invoice) { if (invoice) {
handleSubmitInvoiceClicked(e, invoice.paymentRequest); handleSubmitInvoiceClicked(e, invoice.paymentRequest);
} }
@ -120,15 +94,39 @@ const ProfileDialog = ({
} }
}; };
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string) => {
setBadInvoice('');
setShowRewardsSpinner(true);
apiClient
.post('/api/reward/', {
invoice: rewardInvoice,
})
.then((data) => {
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 handleClickEnableTelegram = () => { const handleClickEnableTelegram = () => {
window.open('https://t.me/' + tgBotName + '?start=' + tgToken, '_blank').focus(); window.open('https://t.me/' + robot.tgBotName + '?start=' + robot.tgToken, '_blank').focus();
setOpenEnableTelegram(false); setOpenEnableTelegram(false);
}; };
const setStealthInvoice = (wantsStealth: boolean) => {
apiClient
.put('/api/stealth/', { wantsStealth })
.then((data) => setRobot({ ...robot, stealthInvoices: data?.wantsStealth }));
};
return ( return (
<Dialog <Dialog
open={open} open={open}
onClose={handleClickCloseProfile} onClose={onClose}
aria-labelledby='profile-title' aria-labelledby='profile-title'
aria-describedby='profile-description' aria-describedby='profile-description'
> >
@ -143,7 +141,7 @@ const ProfileDialog = ({
<ListItem className='profileNickname'> <ListItem className='profileNickname'>
<ListItemText secondary={t('Your robot')}> <ListItemText secondary={t('Your robot')}>
<Typography component='h6' variant='h6'> <Typography component='h6' variant='h6'>
{nickname ? ( {robot.nickname ? (
<div style={{ position: 'relative', left: '-7px' }}> <div style={{ position: 'relative', left: '-7px' }}>
<div <div
style={{ style={{
@ -156,7 +154,7 @@ const ProfileDialog = ({
> >
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
<a>{nickname}</a> <a>{robot.nickname}</a>
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
</div> </div>
@ -169,17 +167,17 @@ const ProfileDialog = ({
<RobotAvatar <RobotAvatar
avatarClass='profileAvatar' avatarClass='profileAvatar'
style={{ width: 65, height: 65 }} style={{ width: 65, height: 65 }}
nickname={nickname} nickname={robot.nickname}
/> />
</ListItemAvatar> </ListItemAvatar>
</ListItem> </ListItem>
<Divider /> <Divider />
{activeOrderId ? ( {robot.activeOrderId ? (
<ListItemButton <ListItemButton
onClick={handleClickCloseProfile} onClick={onClose}
to={`/order/${activeOrderId}`} to={`/order/${robot.activeOrderId}`}
component={LinkRouter} component={LinkRouter}
> >
<ListItemIcon> <ListItemIcon>
@ -188,21 +186,21 @@ const ProfileDialog = ({
</Badge> </Badge>
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={t('One active order #{{orderID}}', { orderID: activeOrderId })} primary={t('One active order #{{orderID}}', { orderID: robot.activeOrderId })}
secondary={t('Your current order')} secondary={t('Your current order')}
/> />
</ListItemButton> </ListItemButton>
) : lastOrderId ? ( ) : robot.lastOrderId ? (
<ListItemButton <ListItemButton
onClick={handleClickCloseProfile} onClick={onClose}
to={`/order/${lastOrderId}`} to={`/order/${robot.lastOrderId}`}
component={LinkRouter} component={LinkRouter}
> >
<ListItemIcon> <ListItemIcon>
<NumbersIcon color='primary' /> <NumbersIcon color='primary' />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={t('Your last order #{{orderID}}', { orderID: lastOrderId })} primary={t('Your last order #{{orderID}}', { orderID: robot.lastOrderId })}
secondary={t('Inactive order')} secondary={t('Inactive order')}
/> />
</ListItemButton> </ListItemButton>
@ -253,8 +251,8 @@ const ProfileDialog = ({
<EnableTelegramDialog <EnableTelegramDialog
open={openEnableTelegram} open={openEnableTelegram}
onClose={() => setOpenEnableTelegram(false)} onClose={() => setOpenEnableTelegram(false)}
tgBotName={tgBotName} tgBotName={robot.tgBotName}
tgToken={tgToken} tgToken={robot.tgToken}
onClickEnable={handleClickEnableTelegram} onClickEnable={handleClickEnableTelegram}
/> />
@ -264,7 +262,7 @@ const ProfileDialog = ({
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
{tgEnabled ? ( {robot.tgEnabled ? (
<Typography color={theme.palette.success.main}> <Typography color={theme.palette.success.main}>
<b>{t('Telegram enabled')}</b> <b>{t('Telegram enabled')}</b>
</Typography> </Typography>
@ -295,8 +293,8 @@ const ProfileDialog = ({
label={t('Use stealth invoices')} label={t('Use stealth invoices')}
control={ control={
<Switch <Switch
checked={stealthInvoices} checked={robot.stealthInvoices}
onChange={() => handleSetStealthInvoice(!stealthInvoices)} onChange={() => setStealthInvoice(!robot.stealthInvoices)}
/> />
} }
/> />
@ -335,7 +333,7 @@ const ProfileDialog = ({
<ListItemText secondary={t('Share to earn 100 Sats per trade')}> <ListItemText secondary={t('Share to earn 100 Sats per trade')}>
<TextField <TextField
label={t('Your referral link')} label={t('Your referral link')}
value={host + '/ref/' + referralCode} value={host + '/ref/' + robot.referralCode}
size='small' size='small'
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
@ -363,12 +361,12 @@ const ProfileDialog = ({
<ListItemText secondary={t('Your earned rewards')}> <ListItemText secondary={t('Your earned rewards')}>
<Grid container> <Grid container>
<Grid item xs={9}> <Grid item xs={9}>
<Typography>{`${earnedRewards} Sats`}</Typography> <Typography>{`${robot.earnedRewards} Sats`}</Typography>
</Grid> </Grid>
<Grid item xs={3}> <Grid item xs={3}>
<Button <Button
disabled={earnedRewards === 0} disabled={robot.earnedRewards === 0}
onClick={() => setOpenClaimRewards(true)} onClick={() => setOpenClaimRewards(true)}
variant='contained' variant='contained'
size='small' size='small'
@ -386,7 +384,7 @@ const ProfileDialog = ({
error={!!badInvoice} error={!!badInvoice}
helperText={badInvoice || ''} helperText={badInvoice || ''}
label={t('Invoice for {{amountSats}} Sats', { label={t('Invoice for {{amountSats}} Sats', {
amountSats: earnedRewards, amountSats: robot.earnedRewards,
})} })}
size='small' size='small'
value={rewardInvoice} value={rewardInvoice}

View File

@ -55,6 +55,7 @@ interface MakerFormProps {
onSubmit?: () => void; onSubmit?: () => void;
onReset?: () => void; onReset?: () => void;
submitButtonLabel?: string; submitButtonLabel?: string;
onOrderCreated?: (id: number) => void;
} }
const MakerForm = ({ const MakerForm = ({
@ -70,6 +71,7 @@ const MakerForm = ({
onSubmit = () => {}, onSubmit = () => {},
onReset = () => {}, onReset = () => {},
submitButtonLabel = 'Create Order', submitButtonLabel = 'Create Order',
onOrderCreated = () => null,
}: MakerFormProps): JSX.Element => { }: MakerFormProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
@ -258,7 +260,10 @@ const MakerForm = ({
}; };
apiClient.post('/api/make/', body).then((data: object) => { apiClient.post('/api/make/', body).then((data: object) => {
setBadRequest(data.bad_request); setBadRequest(data.bad_request);
data.id ? history.push('/order/' + data.id) : ''; if (data.id) {
history.push('/order/' + data.id);
onOrderCreated(data.id);
}
setSubmittingRequest(false); setSubmittingRequest(false);
}); });
} }

View File

@ -7,7 +7,7 @@ import { apiClient } from '../../services/api';
import placeholder from './placeholder.json'; import placeholder from './placeholder.json';
interface Props { interface Props {
nickname: string; nickname: string | null;
smooth?: boolean; smooth?: boolean;
flipHorizontally?: boolean; flipHorizontally?: boolean;
style?: object; style?: object;
@ -38,7 +38,7 @@ const RobotAvatar: React.FC<Props> = ({
const [avatarSrc, setAvatarSrc] = useState<string>(); const [avatarSrc, setAvatarSrc] = useState<string>();
useEffect(() => { useEffect(() => {
if (nickname) { if (nickname != null) {
apiClient.fileImageUrl('/static/assets/avatars/' + nickname + '.png').then(setAvatarSrc); apiClient.fileImageUrl('/static/assets/avatars/' + nickname + '.png').then(setAvatarSrc);
} }
}, [nickname]); }, [nickname]);
@ -92,6 +92,8 @@ const RobotAvatar: React.FC<Props> = ({
alt={nickname} alt={nickname}
src={avatarSrc} src={avatarSrc}
imgProps={{ imgProps={{
sx: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
style: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
onLoad, onLoad,
}} }}
/> />

View File

@ -14,7 +14,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { Settings } from '../../models'; import { Settings } from '../../models';
import SelectLanguage from './SelectLanguage'; import SelectLanguage from './SelectLanguage';
import { Language, Palette, LightMode, DarkMode, FormatSize } from '@mui/icons-material'; import { Language, Palette, LightMode, DarkMode, SettingsOverscan } from '@mui/icons-material';
interface SettingsFormProps { interface SettingsFormProps {
dense?: boolean; dense?: boolean;
@ -113,7 +113,7 @@ const SettingsForm = ({ dense = false, settings, setSettings }: SettingsFormProp
<ListItem> <ListItem>
<ListItemIcon> <ListItemIcon>
<FormatSize /> <SettingsOverscan />
</ListItemIcon> </ListItemIcon>
<Slider <Slider
value={settings.fontSize} value={settings.fontSize}

View File

@ -23,14 +23,8 @@ export interface Robot {
export const defaultRobot: Robot = { export const defaultRobot: Robot = {
nickname: null, nickname: null,
token: systemClient.getCookie('robot_token') ?? null, token: systemClient.getCookie('robot_token') ?? null,
pub_key: pub_key: systemClient.getCookie('pub_key').split('\\').join('\n'),
systemClient.getCookie('pub_key') === undefined enc_priv_key: systemClient.getCookie('enc_priv_key').split('\\').join('\n'),
? null
: systemClient.getCookie('pub_key').split('\\').join('\n'),
enc_priv_key:
systemClient.getCookie('enc_priv_key') === undefined
? null
: systemClient.getCookie('enc_priv_key').split('\\').join('\n'),
bitsEntropy: null, bitsEntropy: null,
shannonEntropy: null, shannonEntropy: null,
stealthInvoices: true, stealthInvoices: true,

View File

@ -6,6 +6,6 @@ export { default as matchMedian } from './match';
export { default as pn } from './prettyNumbers'; export { default as pn } from './prettyNumbers';
export { amountToString } from './prettyNumbers'; export { amountToString } from './prettyNumbers';
export { default as saveAsJson } from './saveFile'; export { default as saveAsJson } from './saveFile';
export { default as statusBadgeColor } from './saveFile'; export { default as statusBadgeColor } from './statusBadgeColor';
export { genBase62Token, tokenStrength } from './token'; export { genBase62Token, tokenStrength } from './token';
export { default as getWebln } from './webln'; export { default as getWebln } from './webln';

View File

@ -4,6 +4,7 @@
*/ */
const saveAsJson = (filename, dataObjToWrite) => { const saveAsJson = (filename, dataObjToWrite) => {
console.log(filename, dataObjToWrite);
const blob = new Blob([JSON.stringify(dataObjToWrite, null, 2)], { type: 'text/json' }); const blob = new Blob([JSON.stringify(dataObjToWrite, null, 2)], { type: 'text/json' });
const link = document.createElement('a'); const link = document.createElement('a');