feat(wallet): buy page bootstrap
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DeclinedModal } from './DeclinedModal';
|
||||
|
||||
export default {
|
||||
title: 'Buy/DeclinedModal',
|
||||
component: DeclinedModal,
|
||||
};
|
||||
|
||||
export const Terms = () => (
|
||||
<DeclinedModal onOk={async () => console.log('user has accepted')} onClose={async () => console.log('closed')} />
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { ConfirmationModal } from '../Modals/ConfirmationModal';
|
||||
|
||||
export const DeclinedModal = ({ onOk, onClose }: { onOk: () => Promise<void>; onClose: () => void }) => (
|
||||
<ConfirmationModal
|
||||
open
|
||||
title="Buy Nym Terms and Conditions"
|
||||
confirmButton="Go back to Terms and Conditions"
|
||||
onConfirm={onOk}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Typography>Can’t procced to buy tokens without acceptance</Typography>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { MockBuyContextProvider } from 'src/context/mocks/buy';
|
||||
|
||||
import { SignMessageModal } from './SignMessageModal';
|
||||
|
||||
export default {
|
||||
title: 'Buy/SignMessage',
|
||||
component: SignMessageModal,
|
||||
};
|
||||
|
||||
export const SignMessage = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<MockBuyContextProvider>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Open Sign Modal
|
||||
</Button>
|
||||
{open && (
|
||||
<SignMessageModal
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MockBuyContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Stack, TextField, Typography } from '@mui/material';
|
||||
import { useBuyContext } from 'src/context';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
import { ErrorModal } from '../Modals/ErrorModal';
|
||||
import { CopyToClipboard } from '../CopyToClipboard';
|
||||
|
||||
export const SignMessageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [signature, setSignature] = useState<string>();
|
||||
|
||||
const { signMessage, loading, refresh, error } = useBuyContext();
|
||||
|
||||
const handleSign = async () => {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
signMessage(message).then((sig) => {
|
||||
setSignature(sig);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setMessage(event.target.value);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
<ErrorModal
|
||||
open
|
||||
message={`An error occured: ${error}`}
|
||||
onClose={() => {
|
||||
refresh();
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
open
|
||||
header="Sign message"
|
||||
okLabel="Sign Message"
|
||||
onOk={handleSign}
|
||||
onClose={onClose}
|
||||
okDisabled={loading}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<TextField
|
||||
id="outlined-multiline-static"
|
||||
label="Message"
|
||||
multiline
|
||||
rows={8}
|
||||
placeholder="Paste your message here"
|
||||
fullWidth
|
||||
value={message}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
id="outlined-multiline-static"
|
||||
multiline
|
||||
rows={3}
|
||||
value={signature}
|
||||
placeholder="Signature"
|
||||
fullWidth
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Stack direction="row" alignItems="center" alignSelf="flex-end">
|
||||
<Typography variant="body2" component="span" fontWeight={400} sx={{ mr: 1, color: 'text.primary' }}>
|
||||
Copy signature
|
||||
</Typography>
|
||||
<CopyToClipboard text={signature} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Tutorial } from './Tutorial';
|
||||
|
||||
export default {
|
||||
title: 'Buy/Page',
|
||||
component: Tutorial,
|
||||
};
|
||||
|
||||
export const BuyPage = () => <Tutorial />;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||
import { NymCard } from '../NymCard';
|
||||
|
||||
export const Tutorial = () => (
|
||||
<Box>
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ mb: 3, mt: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
How to buy NYM with Bity?
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">Follow these 3 steps below to quickly and easily buy NYM tokens</Typography>
|
||||
</Box>
|
||||
<Button variant="contained" color="primary" onClick={() => {}}>
|
||||
Buy Nym
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1}>
|
||||
<NymCard title="1. Define the purchase amount">Card 1</NymCard>
|
||||
<NymCard title="2. Sign a message with your Nym wallet">Card 2</NymCard>
|
||||
<NymCard title="3. Transfer funds and receive NYM">Card 3</NymCard>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
@@ -35,6 +35,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
}}
|
||||
disabled={!text}
|
||||
>
|
||||
{!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
|
||||
</IconButton>
|
||||
@@ -51,6 +52,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
|
||||
}}
|
||||
onClick={() => handleCopy(text)}
|
||||
endIcon={copied && <Check sx={{ color: (theme) => theme.palette.success.light }} />}
|
||||
disabled={!text}
|
||||
>
|
||||
{!copied ? 'Copy' : 'Copied'}
|
||||
</Button>
|
||||
|
||||
@@ -225,3 +225,36 @@ export const withBackButton = () => {
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export const withBackButtonAndCustomLabel = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BasePage handleClick={handleClick}>
|
||||
<SimpleModal
|
||||
open={open}
|
||||
hideCloseIcon
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="This is a modal"
|
||||
okLabel="Primary action"
|
||||
onBack={() => setOpen(false)}
|
||||
backLabel="Cancel"
|
||||
backButtonFullWidth
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</Typography>
|
||||
<ModalDivider />
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
|
||||
</Typography>
|
||||
</SimpleModal>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ export const SimpleModal: React.FC<{
|
||||
header: string | React.ReactNode;
|
||||
subHeader?: string;
|
||||
okLabel: string;
|
||||
backLabel?: string;
|
||||
backButtonFullWidth?: boolean;
|
||||
okDisabled?: boolean;
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
@@ -38,6 +40,8 @@ export const SimpleModal: React.FC<{
|
||||
header,
|
||||
subHeader,
|
||||
okLabel,
|
||||
backLabel,
|
||||
backButtonFullWidth,
|
||||
sx,
|
||||
children,
|
||||
backdropProps,
|
||||
@@ -71,7 +75,7 @@ export const SimpleModal: React.FC<{
|
||||
|
||||
{(onOk || onBack) && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}>
|
||||
{onBack && <StyledBackButton onBack={onBack} />}
|
||||
{onBack && <StyledBackButton onBack={onBack} label={backLabel} fullWidth={backButtonFullWidth} />}
|
||||
{onOk && (
|
||||
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}>
|
||||
{okLabel}
|
||||
|
||||
@@ -54,6 +54,12 @@ export const Nav = () => {
|
||||
mode: 'admin',
|
||||
onClick: () => navigate('/admin'),
|
||||
},
|
||||
{
|
||||
label: 'Buy',
|
||||
route: '/buy',
|
||||
Icon: Bonding,
|
||||
onClick: () => navigate('/buy'),
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,8 +2,18 @@ import React from 'react';
|
||||
import { Button, SxProps } from '@mui/material';
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
|
||||
export const StyledBackButton = ({ onBack, sx }: { onBack: () => void; sx?: SxProps }) => (
|
||||
<Button disableFocusRipple size="large" variant="outlined" onClick={onBack} sx={sx}>
|
||||
<ArrowBackIosNewIcon fontSize="small" />
|
||||
export const StyledBackButton = ({
|
||||
onBack,
|
||||
label,
|
||||
fullWidth,
|
||||
sx,
|
||||
}: {
|
||||
onBack: () => void;
|
||||
label?: string;
|
||||
fullWidth?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => (
|
||||
<Button disableFocusRipple size="large" fullWidth={fullWidth} variant="outlined" onClick={onBack} sx={sx}>
|
||||
{label || <ArrowBackIosNewIcon fontSize="small" />}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { sign } from 'src/requests';
|
||||
import { Console } from 'src/utils/console';
|
||||
// import { AppContext } from './main';
|
||||
|
||||
export type TBuyContext = {
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
signMessage: (message: string) => Promise<string | undefined>;
|
||||
refresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const BuyContext = createContext<TBuyContext>({
|
||||
loading: false,
|
||||
signMessage: async () => '',
|
||||
refresh: async () => undefined,
|
||||
});
|
||||
|
||||
export const BuyContextProvider = ({ children }: { children?: React.ReactNode }): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setError(undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const signMessage = async (message: string) => {
|
||||
let signature;
|
||||
setLoading(true);
|
||||
try {
|
||||
signature = await sign(message);
|
||||
} catch (e: any) {
|
||||
Console.log(`Sign message operation failed: ${e}`);
|
||||
setError(`Sign message operation failed: ${e}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return signature;
|
||||
};
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
signMessage,
|
||||
}),
|
||||
[loading, error],
|
||||
);
|
||||
|
||||
return <BuyContext.Provider value={memoizedValue}>{children}</BuyContext.Provider>;
|
||||
};
|
||||
|
||||
export const useBuyContext = () => useContext<TBuyContext>(BuyContext);
|
||||
@@ -2,3 +2,4 @@ export * from './main';
|
||||
export * from './auth';
|
||||
export * from './accounts';
|
||||
export * from './bonding';
|
||||
export * from './buy';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { BuyContext } from '../buy';
|
||||
import { mockSleep } from './utils';
|
||||
|
||||
export const MockBuyContextProvider = ({ children }: { children?: React.ReactNode }): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setError(undefined);
|
||||
}, []);
|
||||
|
||||
const signMessage = async (message: string) => {
|
||||
setLoading(true);
|
||||
await mockSleep(1042);
|
||||
setLoading(false);
|
||||
return `imagineareallyrealisticsignaturehash${message}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
signMessage,
|
||||
}),
|
||||
[loading, error],
|
||||
);
|
||||
|
||||
return <BuyContext.Provider value={memoizedValue}>{children}</BuyContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { BuyContextProvider } from 'src/context';
|
||||
import { Tutorial } from 'src/components/Buy/Tutorial';
|
||||
|
||||
export const BuyPage = () => (
|
||||
<BuyContextProvider>
|
||||
<Tutorial />
|
||||
</BuyContextProvider>
|
||||
);
|
||||
@@ -4,3 +4,4 @@ export * from './balance';
|
||||
export * from './bonding';
|
||||
export * from './delegation';
|
||||
export * from './internal-docs';
|
||||
export * from './buy';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ApplicationLayout } from 'src/layouts';
|
||||
import { Terminal } from 'src/pages/terminal';
|
||||
import { Send } from 'src/components/Send';
|
||||
import { Receive } from '../components/Receive';
|
||||
import { Balance, InternalDocs, DelegationPage, Admin, BondingPage, NodeSettingsPage } from '../pages';
|
||||
import { Balance, InternalDocs, DelegationPage, Admin, BondingPage, NodeSettingsPage, BuyPage } from '../pages';
|
||||
|
||||
export const AppRoutes = () => (
|
||||
<ApplicationLayout>
|
||||
@@ -18,6 +18,7 @@ export const AppRoutes = () => (
|
||||
<Route path="/delegation" element={<DelegationPage />} />
|
||||
<Route path="/docs" element={<InternalDocs />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/buy" element={<BuyPage />} />
|
||||
</Routes>
|
||||
</ApplicationLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user