Merge pull request #925 from nymtech/feature/ui-enhancements

Feature/UI enhancements for Desktop Wallet
This commit is contained in:
Tommy Verrall
2021-12-01 14:47:21 +00:00
committed by GitHub
56 changed files with 3281 additions and 2977 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"semi": false
}
+17
View File
@@ -2786,6 +2786,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b46b233de7d83bc167fe43ae2dda3b5b84e80e09cceba581e4decb958a4896bf"
dependencies = [
"pathdiff",
"winapi",
]
[[package]]
name = "openssl"
version = "0.10.38"
@@ -2899,6 +2909,12 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pbkdf2"
version = "0.8.0"
@@ -4524,6 +4540,7 @@ dependencies = [
"http",
"ignore",
"once_cell",
"open",
"percent-encoding",
"rand 0.8.4",
"raw-window-handle",
+7 -6
View File
@@ -13,11 +13,12 @@
},
"dependencies": {
"@babel/preset-typescript": "^7.15.0",
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.6.0",
"@hookform/resolvers": "^2.8.0",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@material-ui/styles": "^4.11.4",
"@mui/icons-material": "^5.2.0",
"@mui/material": "^5.2.2",
"@mui/styles": "^5.2.2",
"@types/react-dom": "^17.0.9",
"bs58": "^4.0.1",
"clsx": "^1.1.1",
@@ -48,8 +49,8 @@
"html-webpack-plugin": "^5.3.2",
"style-loader": "^3.2.1",
"url-loader": "^4.1.1",
"webpack": "^5.50.0",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.1.0"
"webpack-dev-server": "^4.5.0"
}
}
+8
View File
@@ -1,6 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap"
rel="stylesheet"
/>
<title>Nym Wallet</title>
</head>
<body>
+1 -1
View File
@@ -18,7 +18,7 @@ tauri-build = { version = "1.0.0-beta.4" }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-beta.8", features = [] }
tauri = { version = "1.0.0-beta.8", features = ["shell-open"] }
tokio = { version = "1.10", features = ["sync"] }
dirs = "4.0"
bip39 = "1.0"
+6 -3
View File
@@ -48,14 +48,17 @@
"updater": {
"active": false
},
"allowlist": {},
"allowlist": {
"shell": {
"open": true
}
},
"windows": [
{
"title": "nym-wallet",
"width": 1268,
"height": 768,
"resizable": true,
"fullscreen": false
"resizable": true
}
],
"security": {
+8 -13
View File
@@ -10,9 +10,8 @@ import {
Paper,
Slide,
TextField,
Theme,
} from '@material-ui/core'
import { useTheme } from '@material-ui/styles'
} from '@mui/material'
import { ClientContext } from '../context/main'
import { NymCard } from '.'
import { getContractParams, setContractParams } from '../requests'
@@ -77,13 +76,9 @@ const AdminForm: React.FC<{
onCancel()
}
const theme: Theme = useTheme()
return (
<FormControl fullWidth>
<div
style={{ padding: theme.spacing(3, 5), maxWidth: 700, minWidth: 400 }}
>
<Box sx={{ padding: [3, 5], maxWidth: 700, minWidth: 400 }}>
<Grid container spacing={3}>
<Grid item xs={12}>
<TextField
@@ -190,15 +185,15 @@ const AdminForm: React.FC<{
/>
</Grid>
</Grid>
</div>
</Box>
<Grid
container
spacing={1}
justifyContent="flex-end"
style={{
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
sx={{
borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`,
background: (theme) => theme.palette.grey[100],
padding: 2,
}}
>
<Grid item>
+95
View File
@@ -0,0 +1,95 @@
import React, { useContext, useEffect } from 'react'
import {
AppBar as MuiAppBar,
Divider,
Grid,
IconButton,
Toolbar,
Typography,
useMediaQuery,
} from '@mui/material'
import { Box } from '@mui/system'
import { Logout } from '@mui/icons-material'
import { ClientContext } from '../context/main'
import { CopyToClipboard } from '.'
export const AppBar = () => {
const { userBalance, logOut, clientDetails } = useContext(ClientContext)
const matches = useMediaQuery('(min-width: 769px)')
return (
<MuiAppBar
position="sticky"
sx={{ boxShadow: 'none', bgcolor: 'nym.background.light' }}
>
<Toolbar>
<Grid
container
justifyContent="space-between"
alignItems="center"
flexWrap="nowrap"
>
<Grid container item alignItems="center">
<Grid item>
<AppBarItem
primaryText="Balance"
secondaryText={userBalance.balance?.printable_balance}
/>
</Grid>
{matches && (
<>
<Divider
orientation="vertical"
variant="middle"
flexItem
sx={{ mr: 1 }}
/>
<Grid item>
<AppBarItem
primaryText="Address"
secondaryText={clientDetails?.client_address}
Action={
<CopyToClipboard
text={clientDetails?.client_address}
iconButton
/>
}
/>
</Grid>
</>
)}
</Grid>
<Grid item>
<IconButton onClick={logOut} sx={{ color: 'nym.background.dark' }}>
<Logout />
</IconButton>
</Grid>
</Grid>
</Toolbar>
</MuiAppBar>
)
}
const AppBarItem: React.FC<{
primaryText: string
secondaryText?: string
Action?: React.ReactNode
}> = ({ primaryText, secondaryText = '', Action }) => {
return (
<Box sx={{ p: 1, mr: 1 }}>
<Typography variant="body2" component="span" sx={{ color: 'grey.600' }}>
{primaryText}:
</Typography>{' '}
<Typography
variant="body2"
component="span"
color="nym.background.dark"
sx={{ mr: 1 }}
>
{secondaryText}
</Typography>
{Action}
</Box>
)
}
+7 -4
View File
@@ -1,8 +1,11 @@
import React from 'react'
import Typography from '@material-ui/core/Typography'
import Grid from '@material-ui/core/Grid'
import { CircularProgress } from '@material-ui/core'
import { Alert, AlertTitle } from '@material-ui/lab'
import {
Alert,
AlertTitle,
CircularProgress,
Grid,
Typography,
} from '@mui/material'
type ConfirmationProps = {
isLoading: boolean
+69 -38
View File
@@ -1,51 +1,82 @@
import React, { useState } from 'react'
import { Button } from '@material-ui/core'
import { Check } from '@material-ui/icons'
import { green } from '@material-ui/core/colors'
import React, { useEffect, useState } from 'react'
import { Button, IconButton, Tooltip } from '@mui/material'
import { Check, ContentCopy } from '@mui/icons-material'
import { clipboard } from '@tauri-apps/api'
const copy = (text: string): Promise<{ success: boolean; value: string }> => {
return new Promise((resolve, reject) => {
clipboard
.writeText(text)
.then(() => resolve({ success: true, value: text }))
.catch((e) => reject({ success: false, value: 'Failed to copy: ' + e }))
})
}
export const handleCopy = async ({
text,
cb,
export const CopyToClipboard = ({
text = '',
light,
iconButton,
}: {
text: string
cb: (success: boolean) => void
text?: string
light?: boolean
iconButton?: boolean
}) => {
const res = await copy(text)
if (res.success) {
setTimeout(() => {
cb(true)
}, 750)
} else {
console.log(res.value)
}
}
export const CopyToClipboard = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false)
const updateCopyStatus = (isCopied: boolean) => setCopied(isCopied)
const handleCopy = async (text: string) => {
try {
await clipboard.writeText(text)
setCopied(true)
} catch (e) {
console.log('failed to copy: ' + e)
}
}
useEffect(() => {
let timer: NodeJS.Timeout
if (copied) {
timer = setTimeout(() => {
setCopied(false)
}, 2000)
}
return () => clearTimeout(timer)
}, [copied])
if (iconButton)
return (
<Tooltip title={!copied ? 'Copy' : 'Copied!'} leaveDelay={500}>
<IconButton
onClick={() => handleCopy(text)}
size="small"
sx={{
color: (theme) =>
light
? theme.palette.common.white
: theme.palette.nym.background.dark,
}}
>
{!copied ? (
<ContentCopy fontSize="small" />
) : (
<Check color="success" />
)}
</IconButton>
</Tooltip>
)
return (
<Button
size="small"
variant={copied ? 'text' : 'outlined'}
aria-label="save"
data-testid="copy-button"
onClick={() => handleCopy({ text, cb: updateCopyStatus })}
endIcon={copied && <Check />}
style={copied ? { background: green[500], color: 'white' } : {}}
variant="outlined"
color="inherit"
sx={{
color: (theme) =>
light
? theme.palette.common.white
: theme.palette.nym.background.dark,
borderColor: (theme) =>
light
? theme.palette.common.white
: theme.palette.nym.background.dark,
}}
onClick={() => handleCopy(text)}
endIcon={
copied && (
<Check sx={{ color: (theme) => theme.palette.success.light }} />
)
}
>
{copied ? 'Copied' : 'Copy'}
{!copied ? 'Copy' : 'Copied'}
</Button>
)
}
+1 -2
View File
@@ -1,7 +1,6 @@
import React from 'react'
import { FallbackProps } from 'react-error-boundary'
import { Alert, AlertTitle } from '@material-ui/lab'
import { Button } from '@material-ui/core'
import { Alert, AlertTitle, Button } from '@mui/material'
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
return (
+36 -70
View File
@@ -1,79 +1,59 @@
import React, { useContext, useEffect } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'
import {
List,
ListItem,
ListItemIcon,
ListItemText,
Theme,
} from '@material-ui/core'
import {
AccountBalanceWalletRounded,
AccountBalanceWalletOutlined,
ArrowBack,
ArrowForward,
AttachMoney,
Cancel,
ExitToApp,
HowToVote,
CancelOutlined,
HowToVoteOutlined,
MoneyOff,
Description,
Settings,
} from '@material-ui/icons'
import { makeStyles } from '@material-ui/styles'
import clsx from 'clsx'
} from '@mui/icons-material'
import { ADMIN_ADDRESS, ClientContext } from '../context/main'
let routesSchema = [
{
label: 'Balance',
route: '/balance',
Icon: <AccountBalanceWalletRounded />,
Icon: AccountBalanceWalletOutlined,
},
{
label: 'Send',
route: '/send',
Icon: <ArrowForward />,
Icon: ArrowForward,
},
{
label: 'Receive',
route: '/receive',
Icon: <ArrowBack />,
Icon: ArrowBack,
},
{
label: 'Bond',
route: '/bond',
Icon: <AttachMoney />,
Icon: AttachMoney,
},
{
label: 'Unbond',
route: '/unbond',
Icon: <MoneyOff />,
Icon: MoneyOff,
},
{
label: 'Delegate',
route: '/delegate',
Icon: <HowToVote />,
Icon: HowToVoteOutlined,
},
{
label: 'Undelegate',
route: '/undelegate',
Icon: <Cancel />,
Icon: CancelOutlined,
},
]
const useStyles = makeStyles((theme: Theme) => ({
navItem: {
color: '#fff',
fontSize: 20,
},
selected: {
color: theme.palette.primary.light,
},
}))
export const Nav = () => {
const classes = useStyles()
const { clientDetails, handleShowAdmin, logOut } = useContext(ClientContext)
const { clientDetails, handleShowAdmin } = useContext(ClientContext)
const location = useLocation()
useEffect(() => {
@@ -81,7 +61,7 @@ export const Nav = () => {
routesSchema.push({
label: 'Docs',
route: '/docs',
Icon: <Description />,
Icon: Description,
})
}
}, [])
@@ -91,56 +71,42 @@ export const Nav = () => {
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'flex-start',
}}
>
<List>
{routesSchema.map((r, i) => (
<ListItem button component={Link} to={r.route} key={i}>
<List disablePadding>
{routesSchema.map(({ Icon, route, label }, i) => (
<ListItem disableGutters component={Link} to={route} key={i}>
<ListItemIcon
className={clsx([
classes.navItem,
location.pathname === r.route ? classes.selected : undefined,
])}
sx={{
minWidth: 30,
color:
location.pathname === route ? 'primary.main' : 'common.white',
}}
>
{r.Icon}
<Icon sx={{ fontSize: 20 }} />
</ListItemIcon>
<ListItemText
primary={r.label}
primaryTypographyProps={{
className: clsx([
classes.navItem,
location.pathname === r.route ? classes.selected : undefined,
]),
sx={{
color:
location.pathname === route ? 'primary.main' : 'common.white',
}}
primary={label}
/>
</ListItem>
))}
{clientDetails?.client_address === ADMIN_ADDRESS && (
<ListItem button onClick={handleShowAdmin}>
<ListItemIcon className={classes.navItem}>
<Settings />
</ListItemIcon>
<ListItemText
primary="Admin"
primaryTypographyProps={{
className: classes.navItem,
<ListItem disableGutters onClick={handleShowAdmin}>
<ListItemIcon
sx={{
minWidth: 30,
}}
/>
>
<Settings sx={{ fontSize: 20, color: 'white' }} />
</ListItemIcon>
<ListItemText primary="Admin" sx={{ color: 'common.white' }} />
</ListItem>
)}
<ListItem button onClick={logOut}>
<ListItemIcon data-testid="log-out" className={classes.navItem}>
<ExitToApp />
</ListItemIcon>
<ListItemText
primary="Log out"
primaryTypographyProps={{
className: classes.navItem,
}}
/>
</ListItem>
</List>
</div>
)
@@ -1,119 +0,0 @@
import React, { useContext, useEffect, useState } from 'react'
import {
CardContent,
CircularProgress,
IconButton,
Tooltip,
Typography,
useTheme,
} from '@material-ui/core'
import { ClientContext } from '../context/main'
import { CheckCircleOutline, FileCopy, Refresh } from '@material-ui/icons'
import { NymCard } from './NymCard'
import { Alert } from '@material-ui/lab'
import { handleCopy } from './CopyToClipboard'
import { truncate } from '../utils'
export const BalanceCard = () => {
const { getBalance } = useContext(ClientContext)
const theme = useTheme()
useEffect(getBalance.fetchBalance, [])
return (
<div style={{ margin: theme.spacing(3) }}>
<NymCard
title="Balance"
subheader="Current wallet balance"
noPadding
Action={
<Tooltip title="Refresh balance">
<IconButton data-testid="refresh-balance" onClick={getBalance.fetchBalance} size="small">
<Refresh />
</IconButton>
</Tooltip>
}
>
<CardContent>
<div style={{ display: 'flex', justifyContent: 'center' }}>
{getBalance.isLoading ? (
<CircularProgress size={24} />
) : getBalance.error ? (
<Alert severity="error" style={{ width: '100%' }}>
{getBalance.error}
</Alert>
) : (
<Typography variant="h6" data-testid="account-balance">
{getBalance.balance?.printable_balance}
</Typography>
)}
</div>
</CardContent>
</NymCard>
</div>
)
}
enum EnumCopyState {
copying,
copySuccess,
}
export const AddressCard = () => {
const { clientDetails } = useContext(ClientContext)
const [copyState, setCopyState] = useState<EnumCopyState>()
const theme = useTheme()
return (
<div style={{ margin: theme.spacing(3) }}>
<NymCard
title="Address"
subheader="Wallet payments address"
noPadding
data-testid="wallet-address-header"
Action={
<Tooltip title={!copyState ? 'Copy address' : 'Copied'}>
<span>
<IconButton
disabled={!!copyState}
onClick={async () => {
setCopyState(EnumCopyState.copying)
await handleCopy({
text: clientDetails?.client_address || '',
cb: (isCopied) => {
if (isCopied) {
setCopyState(EnumCopyState.copySuccess)
setTimeout(() => {
setCopyState(undefined)
}, 2500)
}
},
})
}}
>
{copyState === EnumCopyState.copying ? (
<CircularProgress size={24} />
) : copyState === EnumCopyState.copySuccess ? (
<CheckCircleOutline
style={{ color: theme.palette.success.main }}
/>
) : (
<FileCopy />
)}
</IconButton>
</span>
</Tooltip>
}
>
<CardContent>
<Typography data-testid="wallet-address"
style={{ fontWeight: theme.typography.fontWeightRegular }}
>
{truncate(clientDetails?.client_address!, 35)}
</Typography>
</CardContent>
</NymCard>
</div>
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { Alert, AlertTitle } from '@material-ui/lab'
import { Alert, AlertTitle } from '@mui/material'
export const NoClientError = () => {
return (
@@ -1,11 +1,11 @@
import React from 'react'
import {
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
} from '@material-ui/core'
import React from 'react'
} from '@mui/material'
import { EnumNodeType } from '../types/global'
export const NodeTypeSelector = ({
+18 -13
View File
@@ -1,5 +1,6 @@
import React from 'react'
import { Card, CardContent, CardHeader, useTheme } from '@material-ui/core'
import { Card, CardContent, CardHeader } from '@mui/material'
import { styled } from '@mui/styles'
export const NymCard: React.FC<{
title: string
@@ -7,7 +8,6 @@ export const NymCard: React.FC<{
Action?: React.ReactNode
noPadding?: boolean
}> = ({ title, subheader, Action, noPadding, children }) => {
const theme = useTheme()
return (
<Card variant="outlined">
<CardHeader
@@ -17,19 +17,24 @@ export const NymCard: React.FC<{
titleTypographyProps={{ variant: 'h5' }}
subheaderTypographyProps={{ variant: 'subtitle1' }}
action={Action}
style={{
padding: theme.spacing(2.5),
borderBottom: `1px solid ${theme.palette.grey[200]}`,
sx={{
color: 'nym.background.dark',
padding: 2.5,
borderBottom: (theme) => `1px solid ${theme.palette.grey[100]}`,
}}
/>
<CardContent
style={{
background: theme.palette.grey[50],
padding: noPadding ? 0 : theme.spacing(2, 5),
}}
>
{children}
</CardContent>
{noPadding ? (
<CardContentNoPadding>{children}</CardContentNoPadding>
) : (
<CardContent>{children}</CardContent>
)}
</Card>
)
}
const CardContentNoPadding = styled(CardContent)({
padding: 0,
'&:last-child': {
paddingBottom: 0,
},
})
+5 -7
View File
@@ -1,6 +1,5 @@
import React from 'react'
import { CircularProgress, Theme } from '@material-ui/core'
import { useTheme } from '@material-ui/styles'
import { CircularProgress, Box } from '@mui/material'
export enum EnumRequestStatus {
initial = 'initial',
@@ -18,16 +17,15 @@ export const RequestStatus = ({
Success: React.ReactNode
Error: React.ReactNode
}) => {
const theme: Theme = useTheme()
return (
<div style={{ padding: theme.spacing(3, 5) }}>
<Box sx={{ padding: [3, 5] }}>
{status === EnumRequestStatus.loading && (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress size={48} />
</div>
</Box>
)}
{status === EnumRequestStatus.success && Success}
{status === EnumRequestStatus.error && Error}
</div>
</Box>
)
}
-1
View File
@@ -4,7 +4,6 @@ export * from './Confirmation'
export * from './CopyToClipboard'
export * from './NymCard'
export * from './Nav'
export * from './NavigationCards'
export * from './NodeTypeSelector'
export * from './RequestStatus'
export * from './NoClientError'
+21 -7
View File
@@ -1,14 +1,18 @@
import React, { createContext, useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import { TClientDetails, TSignInWithMnemonic } from '../types'
import { TUseGetBalance, useGetBalance } from '../hooks/useGetBalance'
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance'
export const ADMIN_ADDRESS = 'punk1h3w4nj7kny5dfyjw2le4vm74z03v9vd4dstpu0'
export const urls = {
blockExplorer: 'https://testnet-milhon-blocks.nymtech.net',
}
type TClientContext = {
clientDetails?: TClientDetails
getBalance: TUseGetBalance
userBalance: TUseuserBalance
showAdmin: boolean
mode: 'light' | 'dark'
handleShowAdmin: () => void
logIn: (clientDetails: TSignInWithMnemonic) => void
logOut: () => void
@@ -23,18 +27,27 @@ export const ClientContextProvider = ({
}) => {
const [clientDetails, setClientDetails] = useState<TClientDetails>()
const [showAdmin, setShowAdmin] = useState(false)
const [mode, setMode] = useState<'light' | 'dark'>('light')
const history = useHistory()
const getBalance = useGetBalance()
const userBalance = useGetBalance()
useEffect(() => {
!clientDetails ? history.push('/signin') : history.push('/balance')
}, [clientDetails])
if (!clientDetails) {
history.push('/signin')
} else {
userBalance.fetchBalance()
history.push('/balance')
}
}, [clientDetails, userBalance.fetchBalance])
const logIn = async (clientDetails: TSignInWithMnemonic) =>
setClientDetails(clientDetails)
const logOut = () => setClientDetails(undefined)
const logOut = () => {
setClientDetails(undefined)
userBalance.clearBalance()
}
const handleShowAdmin = () => setShowAdmin((show) => !show)
@@ -42,8 +55,9 @@ export const ClientContextProvider = ({
<ClientContext.Provider
value={{
clientDetails,
getBalance,
userBalance,
showAdmin,
mode,
handleShowAdmin,
logIn,
logOut,
+9 -5
View File
@@ -1,20 +1,21 @@
import { useState } from 'react'
import { useCallback, useState } from 'react'
import { invoke } from '@tauri-apps/api'
import { Balance } from '../types'
export type TUseGetBalance = {
export type TUseuserBalance = {
error?: string
balance?: Balance
isLoading: boolean
fetchBalance: () => void
clearBalance: () => void
}
export const useGetBalance = (): TUseGetBalance => {
export const useGetBalance = (): TUseuserBalance => {
const [balance, setBalance] = useState<Balance>()
const [error, setError] = useState<string>()
const [isLoading, setIsLoading] = useState(false)
const fetchBalance = () => {
const fetchBalance = useCallback(() => {
setIsLoading(true)
setError(undefined)
invoke('get_balance')
@@ -25,12 +26,15 @@ export const useGetBalance = (): TUseGetBalance => {
setTimeout(() => {
setIsLoading(false)
}, 1000)
}
}, [])
const clearBalance = () => setBalance(undefined)
return {
error,
isLoading,
balance,
fetchBalance,
clearBalance,
}
}
+10 -14
View File
@@ -1,40 +1,36 @@
import React, { useContext } from 'react'
import ReactDOM from 'react-dom'
import { ErrorBoundary } from 'react-error-boundary'
import { CssBaseline, ThemeProvider } from '@material-ui/core'
import { BrowserRouter as Router } from 'react-router-dom'
import { Routes } from './routes'
import { theme } from './theme'
import { ClientContext, ClientContextProvider } from './context/main'
import { ApplicationLayout } from './layouts'
import { SignIn } from './routes/sign-in'
import { SignIn } from './routes/sign-in/'
import { Admin, ErrorFallback } from './components'
import { NymWalletTheme } from './theme'
const AppWrapper = () => {
const App = () => {
const { clientDetails } = useContext(ClientContext)
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<NymWalletTheme>
{!clientDetails ? (
<SignIn />
) : (
<ApplicationLayout>
<>
<Admin />
<Routes />
</>
<Admin />
<Routes />
</ApplicationLayout>
)}
</ThemeProvider>
</NymWalletTheme>
)
}
const App = () => {
const AppWrapper = () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router>
<ClientContextProvider>
<AppWrapper />
<App />
</ClientContextProvider>
</Router>
</ErrorBoundary>
@@ -43,4 +39,4 @@ const App = () => {
const root = document.getElementById('root')
ReactDOM.render(<App />, root)
ReactDOM.render(<AppWrapper />, root)
+23 -49
View File
@@ -1,71 +1,45 @@
import React from 'react'
import { Divider } from '@material-ui/core'
import { AddressCard, BalanceCard, Nav } from '../components'
import { Box } from '@mui/material'
import { Nav } from '../components'
import Logo from '../images/logo-background.svg'
import { theme } from '../theme'
import { AppBar } from '../components/AppBar'
export const ApplicationLayout = ({
children,
}: {
children: React.ReactElement
}) => {
export const ApplicationLayout: React.FC = ({ children }) => {
return (
<div
style={{
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'grid',
gridTemplateColumns: '400px auto',
gridTemplateColumns: '240px auto',
gridTemplateRows: '100%',
gridColumnGap: '8px',
gridRowGap: '0px',
overflow: 'hidden',
}}
>
<div
style={{
gridArea: '1 / 1 / 2 / 2',
<Box
sx={{
background: '#121726',
overflow: 'auto',
p: [4, 5],
}}
>
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: theme.spacing(6),
}}
>
<img src={Logo} style={{ width: 75 }} />
</div>
<Divider
light
variant="middle"
style={{
background: theme.palette.grey[100],
marginTop: theme.spacing(6),
}}
/>
<div style={{ marginTop: theme.spacing(10) }}>
<BalanceCard />
<AddressCard />
</div>
<Box sx={{ mb: 3 }}>
<img src={Logo} style={{ width: 45 }} />
</Box>
<div style={{ marginTop: theme.spacing(7) }}>
<Nav />
</div>
<div />
</div>
<div
style={{
gridArea: '1 / 2 / 2 / 3',
<Nav />
</Box>
<Box
sx={{
bgcolor: 'nym.background.light',
overflow: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<AppBar />
{children}
</div>
</div>
</Box>
</Box>
)
}
+5 -8
View File
@@ -1,19 +1,16 @@
import React from 'react'
import { Grid, Theme, useTheme } from '@material-ui/core'
import { Box, Grid } from '@mui/material'
export const Layout = ({ children }: { children: React.ReactElement }) => {
const theme: Theme = useTheme()
return (
<div
style={{
padding: theme.spacing(5),
<Box
sx={{
padding: 5,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'auto',
}}
>
<Grid container justifyContent="center" style={{ margin: 'auto' }}>
@@ -21,6 +18,6 @@ export const Layout = ({ children }: { children: React.ReactElement }) => {
{children}
</Grid>
</Grid>
</div>
</Box>
)
}
+8 -6
View File
@@ -18,7 +18,7 @@ export const createAccount = async (): Promise<TCreateAccount> =>
await invoke('create_new_account')
export const signInWithMnemonic = async (
mnemonic: string
mnemonic: string,
): Promise<TSignInWithMnemonic> =>
await invoke('connect_with_mnemonic', { mnemonic })
@@ -75,15 +75,17 @@ export const bond = async ({
export const unbond = async (type: EnumNodeType) =>
await invoke(`unbond_${type}`)
export const getBalance = async (): Promise<Balance> =>
export const userBalance = async (): Promise<Balance> =>
await invoke('get_balance')
export const getContractParams = async (): Promise<TauriContractSettingsParams> =>
await invoke('get_contract_settings')
export const getContractParams =
async (): Promise<TauriContractSettingsParams> =>
await invoke('get_contract_settings')
export const setContractParams = async (
params: TauriContractSettingsParams
): Promise<TauriContractSettingsParams> => await invoke('update_contract_settings', { params })
params: TauriContractSettingsParams,
): Promise<TauriContractSettingsParams> =>
await invoke('update_contract_settings', { params })
export const getReverseMixDelegations = async (): Promise<TDelegation> =>
await invoke('get_reverse_mix_delegations_paged')
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React from 'react'
import { Layout } from '../layouts'
export const NotFound = () => {
+25 -45
View File
@@ -1,61 +1,41 @@
import React, { useEffect } from 'react'
import { Button, CircularProgress, Grid } from '@material-ui/core'
import { Alert } from '@material-ui/lab'
import { Refresh } from '@material-ui/icons'
import React, { useContext } from 'react'
import { Alert, Button, Grid, Link } from '@mui/material'
import { Box } from '@mui/system'
import { OpenInNew } from '@mui/icons-material'
import { NymCard } from '../components'
import { Layout } from '../layouts'
import { theme } from '../theme'
import { useGetBalance } from '../hooks/useGetBalance'
import { ClientContext, urls } from '../context/main'
export const Balance = () => {
const { balance, isLoading, error, fetchBalance } = useGetBalance()
useEffect(fetchBalance, [])
const RefreshAction = () => (
<Button
variant="contained"
size="small"
color="primary"
type="submit"
data-testid="refresh-button"
onClick={fetchBalance}
disabled={isLoading}
disableElevation
startIcon={<Refresh />}
endIcon={isLoading && <CircularProgress size={20} />}
style={{ marginRight: theme.spacing(2) }}
>
Refresh
</Button>
)
const { userBalance, clientDetails } = useContext(ClientContext)
return (
<Layout>
<NymCard title="Check Balance" data-testid="check-balance">
<NymCard title="Balance" data-testid="check-balance">
<Grid container direction="column" spacing={2}>
<Grid item>
{error && (
<Alert
severity="error"
data-testid="error-refresh"
action={<RefreshAction />}
style={{ padding: theme.spacing(2) }}
>
{error}
{userBalance.error && (
<Alert severity="error" data-testid="error-refresh" sx={{ p: 2 }}>
{userBalance.error}
</Alert>
)}
{!error && (
<Alert
severity="success"
data-testid="refresh-success"
style={{ padding: theme.spacing(2, 3) }}
action={<RefreshAction />}
>
{'The current balance is ' + balance?.printable_balance}
</Alert>
{!userBalance.error && (
<Box data-testid="refresh-success" sx={{ p: 2 }}>
{'The current balance is ' +
userBalance.balance?.printable_balance}
</Box>
)}
</Grid>
<Grid item>
<Link
sx={{ pl: 1 }}
href={`${urls.blockExplorer}/account/${clientDetails?.client_address}`}
target="_blank"
>
<Button endIcon={<OpenInNew />}>Last transactions</Button>
</Link>
</Grid>
</Grid>
</NymCard>
</Layout>
+16 -20
View File
@@ -1,5 +1,7 @@
import React, { useContext } from 'react'
import {
Alert,
Box,
Button,
Checkbox,
CircularProgress,
@@ -8,10 +10,7 @@ import {
Grid,
InputAdornment,
TextField,
Theme,
} from '@material-ui/core'
import { useTheme } from '@material-ui/styles'
import { Alert } from '@material-ui/lab'
} from '@mui/material'
import { yupResolver } from '@hookform/resolvers/yup'
import { useForm } from 'react-hook-form'
import { EnumNodeType } from '../../types/global'
@@ -79,7 +78,7 @@ export const BondForm = ({
onSuccess,
}: {
disabled: boolean
fees?: { [key in EnumNodeType]: Coin }
fees?: { [EnumNodeType.mixnode]: Coin; [EnumNodeType.gateway]?: Coin }
onError: (message?: string) => void
onSuccess: (message?: string) => void
}) => {
@@ -95,12 +94,12 @@ export const BondForm = ({
defaultValues,
})
const { getBalance } = useContext(ClientContext)
const { userBalance } = useContext(ClientContext)
const watchNodeType = watch('nodeType', defaultValues.nodeType)
const watchAdvancedOptions = watch(
'withAdvancedOptions',
defaultValues.withAdvancedOptions
defaultValues.withAdvancedOptions,
)
const onSubmit = async (data: TBondFormFields) => {
@@ -114,7 +113,7 @@ export const BondForm = ({
await bond({ type: data.nodeType, data: formattedData, amount })
.then(() => {
getBalance.fetchBalance()
userBalance.fetchBalance()
onSuccess(`Successfully bonded to ${data.identityKey}`)
})
.catch((e) => {
@@ -122,11 +121,9 @@ export const BondForm = ({
})
}
const theme: Theme = useTheme()
return (
<FormControl fullWidth>
<div style={{ padding: theme.spacing(3, 5) }}>
<Box sx={{ padding: [3, 5] }}>
<Grid container spacing={3}>
<Grid container item justifyContent="space-between">
<Grid item>
@@ -146,7 +143,7 @@ export const BondForm = ({
{`A fee of ${
watchNodeType === EnumNodeType.mixnode
? fees.mixnode.amount
: fees.gateway.amount
: fees?.gateway?.amount
} PUNK will apply to this transaction`}
</Alert>
</Grid>
@@ -352,15 +349,15 @@ export const BondForm = ({
</>
)}
</Grid>
</div>
<div
style={{
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`,
bgcolor: (theme) => theme.palette.grey[50],
padding: 2,
}}
>
<Button
@@ -368,7 +365,6 @@ export const BondForm = ({
variant="contained"
color="primary"
type="submit"
size="large"
data-testid="submit-button"
disableElevation
onClick={handleSubmit(onSubmit)}
@@ -376,7 +372,7 @@ export const BondForm = ({
>
Bond
</Button>
</div>
</Box>
</FormControl>
)
}
+16 -18
View File
@@ -1,7 +1,5 @@
import React, { useContext, useEffect, useState } from 'react'
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
import { Alert } from '@material-ui/lab'
import { useTheme } from '@material-ui/styles'
import { Alert, Box, Button, CircularProgress } from '@mui/material'
import { BondForm } from './BondForm'
import { NymCard } from '../../components'
import {
@@ -20,9 +18,7 @@ export const Bond = () => {
const [fees, setFees] = useState<TFee>()
const { checkOwnership, ownership } = useCheckOwnership()
const { getBalance } = useContext(ClientContext)
const theme: Theme = useTheme()
const { userBalance } = useContext(ClientContext)
useEffect(() => {
if (status === EnumRequestStatus.initial) {
@@ -43,14 +39,14 @@ export const Bond = () => {
<NymCard title="Bond" subheader="Bond a node or gateway" noPadding>
{ownership?.hasOwnership && (
<Alert
severity="warning"
severity="warning"
action={
<Button
disabled={status === EnumRequestStatus.loading}
onClick={async () => {
setStatus(EnumRequestStatus.loading)
await unbond(ownership.nodeType!)
getBalance.fetchBalance()
userBalance.fetchBalance()
setStatus(EnumRequestStatus.initial)
}}
data-testid="unBond"
@@ -58,17 +54,17 @@ export const Bond = () => {
Unbond
</Button>
}
style={{ margin: theme.spacing(2) }}
style={{ margin: 2 }}
>
{`Looks like you already have a ${ownership.nodeType} bonded.`}
</Alert>
)}
{status === EnumRequestStatus.loading && (
<Box
style={{
sx={{
display: 'flex',
justifyContent: 'center',
padding: theme.spacing(3),
padding: 3,
}}
>
<CircularProgress size={48} />
@@ -94,7 +90,9 @@ export const Bond = () => {
<RequestStatus
status={status}
Success={
<Alert severity="success" data-testid="bond-success">Successfully bonded node</Alert>
<Alert severity="success" data-testid="bond-success">
Successfully bonded node
</Alert>
}
Error={
<Alert severity="error" data-testid="bond-error">
@@ -102,14 +100,14 @@ export const Bond = () => {
</Alert>
}
/>
<div
style={{
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`,
bgcolor: (theme) => theme.palette.grey[50],
padding: 2,
}}
>
<Button
@@ -120,7 +118,7 @@ export const Bond = () => {
>
Again?
</Button>
</div>
</Box>
</>
)}
</NymCard>
+14 -14
View File
@@ -1,5 +1,7 @@
import React, { useContext } from 'react'
import {
Alert,
Box,
Button,
CircularProgress,
FormControl,
@@ -8,12 +10,11 @@ import {
TextField,
Theme,
useTheme,
} from '@material-ui/core'
} from '@mui/material'
import { useForm } from 'react-hook-form'
import { EnumNodeType, TFee } from '../../types'
import { yupResolver } from '@hookform/resolvers/yup'
import { validationSchema } from './validationSchema'
import { Alert } from '@material-ui/lab'
import { ClientContext } from '../../context/main'
import { delegate, majorToMinor } from '../../requests'
import { checkHasEnoughFunds } from '../../utils'
@@ -39,7 +40,6 @@ export const DelegateForm = ({
onError: (message?: string) => void
onSuccess: (message?: string) => void
}) => {
const theme = useTheme<Theme>()
const {
register,
watch,
@@ -53,7 +53,7 @@ export const DelegateForm = ({
const watchNodeType = watch('nodeType', defaultValues.nodeType)
const { getBalance } = useContext(ClientContext)
const { userBalance } = useContext(ClientContext)
const onSubmit = async (data: TDelegateForm) => {
const hasEnoughFunds = await checkHasEnoughFunds(data.amount)
@@ -72,9 +72,9 @@ export const DelegateForm = ({
})
.then((res) => {
onSuccess(
`Successfully delegated ${data.amount} punk to ${res.target_address}`
`Successfully delegated ${data.amount} punk to ${res.target_address}`,
)
getBalance.fetchBalance()
userBalance.fetchBalance()
})
.catch((e) => {
console.log(e)
@@ -84,7 +84,7 @@ export const DelegateForm = ({
return (
<FormControl fullWidth>
<div style={{ padding: theme.spacing(3, 5) }}>
<Box sx={{ padding: [3, 5] }}>
<Grid container spacing={3}>
<Grid container item xs={12} justifyContent="space-between">
<Grid item>
@@ -126,15 +126,15 @@ export const DelegateForm = ({
/>
</Grid>
</Grid>
</div>
<div
style={{
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`,
background: (theme) => theme.palette.grey[50],
padding: 2,
}}
>
<Button
@@ -149,7 +149,7 @@ export const DelegateForm = ({
>
Delegate stake
</Button>
</div>
</Box>
</FormControl>
)
}
+11 -14
View File
@@ -1,7 +1,5 @@
import React, { useEffect, useState } from 'react'
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
import { useTheme } from '@material-ui/styles'
import { Alert, AlertTitle } from '@material-ui/lab'
import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material'
import { DelegateForm } from './DelegateForm'
import { Layout } from '../../layouts'
import { NymCard } from '../../components'
@@ -14,7 +12,7 @@ import { getGasFee } from '../../requests'
export const Delegate = () => {
const [status, setStatus] = useState<EnumRequestStatus>(
EnumRequestStatus.initial
EnumRequestStatus.initial,
)
const [message, setMessage] = useState<string>()
const [isLoading, setIsLoading] = useState(true)
@@ -34,7 +32,6 @@ export const Delegate = () => {
getFees()
}, [])
const theme: Theme = useTheme()
return (
<Layout>
<NymCard
@@ -45,10 +42,10 @@ export const Delegate = () => {
>
{isLoading && (
<Box
style={{
sx={{
display: 'flex',
justifyContent: 'center',
padding: theme.spacing(3),
p: 3,
}}
>
<CircularProgress size={48} />
@@ -76,7 +73,7 @@ export const Delegate = () => {
<Alert severity="error" data-testid="delegate-error">
<AlertTitle>Delegation failed</AlertTitle>
An error occurred with the request:
<Box style={{ wordBreak: 'break-word' }}>{message}</Box>
<Box sx={{ wordBreak: 'break-word' }}>{message}</Box>
</Alert>
}
Success={
@@ -86,14 +83,14 @@ export const Delegate = () => {
</Alert>
}
/>
<div
style={{
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`,
background: (theme) => theme.palette.grey[50],
p: 2,
}}
>
<Button
@@ -104,7 +101,7 @@ export const Delegate = () => {
>
Finish
</Button>
</div>
</Box>
</>
)}
</>
-4
View File
@@ -6,16 +6,12 @@ import { Bond } from './bond'
import { Delegate } from './delegate'
import { Receive } from './receive'
import { Send } from './send'
import { SignIn } from './sign-in'
import { Unbond } from './unbond'
import { Undelegate } from './undelegate'
import { InternalDocs } from './internal-docs'
export const Routes = () => (
<Switch>
<Route path="/signin">
<SignIn />
</Route>
<Route path="/balance">
<Balance />
</Route>
@@ -1,5 +1,5 @@
import React from 'react'
import { List, ListItem } from '@material-ui/core'
import { List, ListItem } from '@mui/material'
import { DocEntry } from './DocEntry'
export const ApiList = () => {
@@ -1,7 +1,6 @@
import React from 'react'
import { Button, Card, TextField } from '@material-ui/core'
import { Button, Card, CardContent, TextField } from '@mui/material'
import { invoke } from '@tauri-apps/api'
import CardContent from '@material-ui/core/CardContent'
interface DocEntryProps {
function: FunctionDef
@@ -24,7 +23,7 @@ function collectArgs(functionName: string, args: ArgDef[]) {
args.forEach((arg) => {
let elem: HTMLElement | null = document.getElementById(
argKey(functionName, arg.name)
argKey(functionName, arg.name),
)
if (arg.type === 'object') {
@@ -44,21 +43,21 @@ export const DocEntry = (props: DocEntryProps) => {
const onClick = () => {
invoke(
props.function.name,
collectArgs(props.function.name, props.function.args)
collectArgs(props.function.name, props.function.args),
)
.then((result) => {
setCard(
<Card>
<CardContent>{JSON.stringify(result, null, 4)}</CardContent>
</Card>
</Card>,
)
})
.catch((e) =>
setCard(
<Card>
<CardContent>{e}</CardContent>
</Card>
)
</Card>,
),
)
}
@@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react'
import React, { useContext } from 'react'
import { NymCard } from '../../components'
import { ApiList } from './ApiList'
import { Layout } from '../../layouts'
+19 -12
View File
@@ -1,11 +1,15 @@
import React, { useContext } from 'react'
import QRCode from 'qrcode.react'
import { Box, Card, Grid, Typography } from '@material-ui/core'
import { Alert } from '@material-ui/lab'
import { useMediaQuery } from '@material-ui/core'
import {
Alert,
Box,
Card,
Grid,
Typography,
useMediaQuery,
} from '@mui/material'
import { CopyToClipboard, NymCard } from '../components'
import { Layout } from '../layouts'
import { theme } from '../theme'
import { ClientContext } from '../context/main'
export const Receive = () => {
@@ -23,12 +27,12 @@ export const Receive = () => {
</Grid>
<Grid item>
<Card
style={{
margin: theme.spacing(1, 0),
sx={{
m: [1, 0],
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
padding: theme.spacing(3),
p: 3,
}}
variant="outlined"
>
@@ -42,9 +46,9 @@ export const Receive = () => {
<Typography
data-testid="client-address"
variant={matches ? 'h5' : 'subtitle1'}
style={{
sx={{
wordBreak: 'break-word',
marginRight: theme.spacing(1),
mr: 1,
}}
component="span"
>
@@ -54,15 +58,18 @@ export const Receive = () => {
</Grid>
<Grid item>
<Box
style={{
sx={{
display: 'flex',
justifyContent: 'center',
marginBottom: theme.spacing(2),
mb: 2,
}}
component="div"
>
{clientDetails && (
<QRCode data-testid="qr-code" value={clientDetails.client_address} />
<QRCode
data-testid="qr-code"
value={clientDetails.client_address}
/>
)}
</Box>
</Grid>
+36 -36
View File
@@ -1,7 +1,6 @@
import React from 'react'
import { Card, CircularProgress, Theme, Typography } from '@material-ui/core'
import { CheckCircleOutline } from '@material-ui/icons'
import { useTheme } from '@material-ui/styles'
import { Box, Card, CircularProgress, Typography } from '@mui/material'
import { CheckCircleOutline } from '@mui/icons-material'
import { SendError } from './SendError'
import { TauriTxResult } from '../../types/rust/tauritxresult'
@@ -14,11 +13,9 @@ export const SendConfirmation = ({
error?: string
isLoading: boolean
}) => {
const theme: Theme = useTheme()
return (
<div
style={{
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
@@ -30,52 +27,55 @@ export const SendConfirmation = ({
{!isLoading && !!error && <SendError message={error} />}
{!isLoading && data && (
<>
<div
style={{
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
marginBottom: theme.spacing(4),
mb: 4,
}}
>
<CheckCircleOutline
style={{
sx={{
fontSize: 50,
color: theme.palette.success.main,
marginBottom: theme.spacing(1),
color: 'success.main',
mb: 1,
}}
/>
<Typography data-testid="transaction-complete">Transaction complete</Typography>
</div>
<Typography data-testid="transaction-complete">
Transaction complete
</Typography>
</Box>
<Card
variant="outlined"
style={{ width: '100%', padding: theme.spacing(2) }}
>
<div style={{ display: 'flex', marginBottom: theme.spacing(2) }}>
<div style={{ width: '33%' }}>
<Typography style={{ color: theme.palette.grey[600] }}>
<Card variant="outlined" sx={{ width: '100%', p: 2 }}>
<Box sx={{ display: 'flex', mb: 2 }}>
<Box sx={{ width: '1/3' }}>
<Typography sx={{ color: (theme) => theme.palette.grey[600] }}>
Recipient
</Typography>
</div>
<div style={{ wordBreak: 'break-all' }}>
<Typography data-testid="to-address">{data.to_address}</Typography>
</div>
</div>
<div style={{ display: 'flex' }}>
<div style={{ width: '33%' }}>
<Typography style={{ color: theme.palette.grey[600] }}>
</Box>
<Box sx={{ wordBreak: 'break-all' }}>
<Typography data-testid="to-address">
{data.to_address}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex' }}>
<Box sx={{ width: '33%' }}>
<Typography sx={{ color: (theme) => theme.palette.grey[600] }}>
Amount
</Typography>
</div>
<div>
<Typography data-testid="send-amount">{data.amount.amount + ' punks'}</Typography>
</div>
</div>
</Box>
<Box>
<Typography data-testid="send-amount">
{data.amount.amount + ' punks'}
</Typography>
</Box>
</Box>
</Card>
</>
)}
</div>
</Box>
)
}
+11 -20
View File
@@ -1,15 +1,11 @@
import React from 'react'
import { Card, Theme, Typography } from '@material-ui/core'
import { ErrorOutline } from '@material-ui/icons'
import { Alert } from '@material-ui/lab'
import { useTheme } from '@material-ui/styles'
import { Alert, Box, Card, Typography } from '@mui/material'
import { ErrorOutline } from '@mui/icons-material'
export const SendError = ({ message }: { message?: string }) => {
const theme: Theme = useTheme()
return (
<div
style={{
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
@@ -18,30 +14,25 @@ export const SendError = ({ message }: { message?: string }) => {
}}
>
<>
<div
style={{
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
marginBottom: theme.spacing(4),
mb: 4,
}}
>
<ErrorOutline
style={{ fontSize: 50, color: theme.palette.error.main }}
/>
<ErrorOutline sx={{ fontSize: 50, color: 'error.main' }} />
<Typography>Transaction failed</Typography>
</div>
</Box>
<Card
variant="outlined"
style={{ width: '100%', padding: theme.spacing(2) }}
>
<Card variant="outlined" sx={{ width: '100%', p: 2 }}>
<Alert severity="error" data-testid="transaction-error">
An error occured during the request {message}
</Alert>
</Card>
</>
</div>
</Box>
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import React from 'react'
import { Grid, InputAdornment, TextField } from '@material-ui/core'
import { Grid, InputAdornment, TextField } from '@mui/material'
import { useFormContext } from 'react-hook-form'
export const SendForm = () => {
+9 -14
View File
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import {
Card,
CircularProgress,
@@ -6,10 +6,8 @@ import {
Grid,
Theme,
Typography,
} from '@material-ui/core'
import { useTheme } from '@material-ui/styles'
} from '@mui/material'
import { useFormContext } from 'react-hook-form'
import { ClientContext } from '../../context/main'
import { getGasFee } from '../../requests'
export const SendReview = () => {
@@ -19,8 +17,6 @@ export const SendReview = () => {
const values = getValues()
const theme: Theme = useTheme()
useEffect(() => {
const getFee = async () => {
const fee = await getGasFee('Send')
@@ -33,10 +29,10 @@ export const SendReview = () => {
return (
<Card
variant="outlined"
style={{
sx={{
width: '100%',
padding: theme.spacing(2),
margin: theme.spacing(3, 0),
p: 2,
m: [3, 0],
}}
>
{isLoadingFee ? (
@@ -44,7 +40,7 @@ export const SendReview = () => {
) : (
<Grid container spacing={2}>
<Grid item xs={12}>
<SendReviewField title="From" subtitle={values.from}/>
<SendReviewField title="From" subtitle={values.from} />
</Grid>
<Grid item xs={12}>
<Divider light />
@@ -80,13 +76,12 @@ export const SendReviewField = ({
title: string
subtitle?: string
}) => {
const theme: Theme = useTheme()
return (
<>
<Typography style={{ color: theme.palette.grey[600] }}>
{title}
<Typography style={{ color: 'grey[600]' }}>{title}</Typography>
<Typography data-testid={title} style={{ wordBreak: 'break-all' }}>
{subtitle}
</Typography>
<Typography data-testid={title} style={{ wordBreak: 'break-all' }}>{subtitle}</Typography>
</>
)
}
+22 -25
View File
@@ -1,8 +1,7 @@
import React, { useContext, useEffect, useState } from 'react'
import React, { useContext, useState } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { Button, Step, StepLabel, Stepper, Theme } from '@material-ui/core'
import { useTheme } from '@material-ui/styles'
import { Box, Button, Step, StepLabel, Stepper } from '@mui/material'
import { SendForm } from './SendForm'
import { SendReview } from './SendReview'
import { SendConfirmation } from './SendConfirmation'
@@ -31,7 +30,7 @@ export const SendWizard = () => {
const [requestError, setRequestError] = useState<string>()
const [confirmedData, setConfirmedData] = useState<TauriTxResult['details']>()
const { getBalance } = useContext(ClientContext)
const { userBalance } = useContext(ClientContext)
const steps = ['Enter address', 'Review and send', 'Await confirmation']
@@ -44,8 +43,6 @@ export const SendWizard = () => {
resolver: yupResolver(validationSchema),
})
const theme: Theme = useTheme()
const handleNextStep = methods.handleSubmit(() => setActiveStep((s) => s + 1))
const handlePreviousStep = () => setActiveStep((s) => s - 1)
@@ -85,7 +82,7 @@ export const SendWizard = () => {
amount: { denom: 'Major', amount: formState.amount },
})
setIsLoading(false)
getBalance.fetchBalance()
userBalance.fetchBalance()
})
.catch((e) => {
setRequestError(e)
@@ -97,13 +94,13 @@ export const SendWizard = () => {
return (
<FormProvider {...methods}>
<div style={{ paddingTop: theme.spacing(3) }}>
<Box sx={{ pt: 3 }}>
<Stepper
activeStep={activeStep}
style={{
background: theme.palette.grey[50],
paddingBottom: 0,
paddingTop: 0,
sx={{
bgcolor: 'grey[50]',
pb: 0,
pt: 0,
}}
>
{steps.map((s, i) => (
@@ -112,13 +109,13 @@ export const SendWizard = () => {
</Step>
))}
</Stepper>
<div
style={{
<Box
sx={{
minHeight: 300,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: theme.spacing(0, 3),
p: [0, 3],
}}
>
{activeStep === 0 ? (
@@ -132,21 +129,21 @@ export const SendWizard = () => {
error={requestError}
/>
)}
</div>
<div
style={{
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`,
background: (theme) => theme.palette.grey[50],
p: 2,
}}
>
{activeStep === 1 && (
<Button
disableElevation
style={{ marginRight: theme.spacing(1) }}
sx={{ mr: 1 }}
onClick={handlePreviousStep}
data-testid="back-button"
>
@@ -155,7 +152,7 @@ export const SendWizard = () => {
)}
<Button
variant={activeStep > 0 ? 'contained' : 'text'}
color={activeStep > 0 ? 'primary' : 'default'}
color={activeStep > 0 ? 'primary' : 'inherit'}
disableElevation
data-testid="button"
onClick={
@@ -175,8 +172,8 @@ export const SendWizard = () => {
>
{activeStep === 0 ? 'Next' : activeStep === 1 ? 'Send' : 'Finish'}
</Button>
</div>
</div>
</Box>
</Box>
</FormProvider>
)
}
-317
View File
@@ -1,317 +0,0 @@
import React, { useContext, useState } from 'react'
import {
TextField,
CircularProgress,
Button,
Typography,
Grid,
Link,
Theme,
Card,
Divider,
} from '@material-ui/core'
import { Alert } from '@material-ui/lab'
import { useTheme } from '@material-ui/styles'
import { ArrowBack, CheckCircleOutline } from '@material-ui/icons'
import logo from '../images/logo-background.svg'
import logo_alt from '../images/logo.png'
import { ClientContext } from '../context/main'
import { theme } from '../theme'
import { createAccount, signInWithMnemonic } from '../requests'
import { TCreateAccount } from '../types'
import { CopyToClipboard } from '../components'
export const SignIn = () => {
const theme: Theme = useTheme()
const [showCreateAccount, setShowCreateAccount] = useState(false)
return (
<div
style={{
height: '100vh',
width: '100vw',
display: 'grid',
gridTemplateColumns: '400px auto',
gridTemplateRows: '100%',
gridColumnGap: '0px',
gridRowGap: '0px',
}}
>
<div
style={{
gridArea: '1 / 1 / 2 / 2',
background: '#121726',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img src={logo} style={{ width: 100 }} />
</div>
<div
style={{
gridArea: '1 / 2 / 2 / 3',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: theme.palette.grey[100],
}}
>
{showCreateAccount ? (
<CreateAccountContent
showSignIn={() => setShowCreateAccount(false)}
/>
) : (
<SignInContent showCreateAccount={() => setShowCreateAccount(true)} />
)}
</div>
</div>
)
}
const SignInContent = ({
showCreateAccount,
}: {
showCreateAccount: () => void
}) => {
const [mnemonic, setMnemonic] = useState<string>('')
const [inputError, setInputError] = useState<string>()
const [isLoading, setIsLoading] = useState(false)
const { logIn } = useContext(ClientContext)
const theme: Theme = useTheme()
const handleSignIn = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setIsLoading(true)
setInputError(undefined)
try {
const res = await signInWithMnemonic(mnemonic || '')
setIsLoading(false)
logIn(res)
} catch (e: any) {
setIsLoading(false)
setInputError(e)
}
}
return (
<SignInCard>
<>
<Typography variant="h4" data-testid="sign-in">Sign in</Typography>
<form noValidate onSubmit={handleSignIn}>
<Grid container direction="column" spacing={1}>
<Grid item>
<TextField
style={{ background: 'white' }}
value={mnemonic}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setMnemonic(e.target.value)
}
size="medium"
variant="outlined"
margin="normal"
required
fullWidth
id="mnemonic"
label="BIP-39 Mnemonic"
name="mnemonic"
autoComplete="mnemonic"
autoFocus
disabled={isLoading}
/>
</Grid>
<Grid item>
<Button
fullWidth
variant="contained"
color="primary"
type="submit"
disabled={isLoading}
endIcon={isLoading && <CircularProgress size={20} />}
disableElevation
>
{!isLoading ? 'Sign In' : 'Signing in'}
</Button>
</Grid>
{inputError && (
<Grid item style={{ marginTop: theme.spacing(1) }}>
<Alert severity="error">{inputError}</Alert>
</Grid>
)}
<Grid item style={{ marginTop: theme.spacing(1) }}>
<Typography variant="body2" component="span">
Don't have an account?
</Typography>{' '}
<Link href="#" onClick={showCreateAccount}>
Create one
</Link>
</Grid>
</Grid>
</form>
</>
</SignInCard>
)
}
const SignInCard: React.FC = ({ children }) => {
const theme: Theme = useTheme()
return (
<>
<Card
style={{
width: 600,
padding: theme.spacing(6, 10),
borderRadius: theme.shape.borderRadius,
position: 'relative',
minHeight: 350,
}}
>
<img
src={logo_alt}
style={{
position: 'absolute',
width: 425,
filter: 'grayscale(100%)',
opacity: 0.1,
top: '50%',
left: '50%',
transform: 'translate(0%, -50%)',
}}
/>
{children}
</Card>
</>
)
}
const CreateAccountContent = ({ showSignIn }: { showSignIn: () => void }) => {
const [accountDetails, setAccountDetails] = useState<TCreateAccount>()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error>()
const handleCreateAccount = async () => {
setIsLoading(true)
setError(undefined)
try {
const res = await createAccount()
setTimeout(() => {
setAccountDetails(res)
setIsLoading(false)
}, 2500)
} catch (e: any) {
setError(e)
}
}
return (
<SignInCard>
<Typography variant="h4">Create wallet</Typography>
<Typography color="textSecondary">
Create a new wallet to start using the Nym network
</Typography>
<Grid
container
direction="column"
spacing={3}
style={{ marginTop: theme.spacing(3) }}
>
<Grid item container justifyContent="center">
{isLoading && <CircularProgress size={48} />}
{!isLoading && accountDetails && (
<>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
marginBottom: theme.spacing(4),
}}
>
<CheckCircleOutline
style={{
fontSize: 50,
color: theme.palette.success.main,
marginBottom: theme.spacing(1),
}}
/>
<Typography>Wallet setup complete</Typography>
</div>
<Alert severity="info" style={{ marginBottom: theme.spacing(2) }} data-testid="mnemonic-warning">
Please store your <strong>mnemonic</strong> in a safe place.
You'll need it to access your wallet
</Alert>
<Card
variant="outlined"
style={{
width: '100%',
padding: theme.spacing(2),
}}
>
<Grid container direction="column" spacing={1}>
<Grid item>
<Typography style={{ color: theme.palette.grey[600] }}>
Mnemonic
</Typography>
</Grid>
<Grid item>
<Typography data-testid="mnemonic-phrase">{accountDetails.mnemonic}</Typography>
<div
style={{ display: 'flex', justifyContent: 'flex-end' }}
>
<CopyToClipboard text={accountDetails.mnemonic} />
</div>
</Grid>
<Grid item>
<Divider light />
</Grid>
<Grid item>
<Typography style={{ color: theme.palette.grey[600] }}>
Address
</Typography>
</Grid>
<Grid item>
<Typography data-testid="wallet-address">{accountDetails.client_address}</Typography>
</Grid>
</Grid>
</Card>
</>
)}
</Grid>
{error && (
<Grid item style={{ marginTop: theme.spacing(1) }}>
<Alert severity="error" data-testid="error">{error}</Alert>
</Grid>
)}
<Grid item>
{!accountDetails && (
<Button
onClick={handleCreateAccount}
fullWidth
variant="contained"
color="primary"
type="submit"
data-testid="create-button"
disableElevation
style={{ marginBottom: theme.spacing(1) }}
disabled={isLoading}
>
Create
</Button>
)}
<Button
fullWidth
variant="text"
onClick={showSignIn}
data-testid="sign-in-button"
startIcon={<ArrowBack />}
>
Sign in
</Button>
</Grid>
</Grid>
</SignInCard>
)
}
@@ -0,0 +1,105 @@
import React, { useEffect, useState } from 'react'
import {
Alert,
Button,
Card,
CardActions,
CardContent,
CardHeader,
CircularProgress,
Stack,
Typography,
} from '@mui/material'
import { Box } from '@mui/system'
import { ArrowBack } from '@mui/icons-material'
import { createAccount } from '../../requests'
import { TCreateAccount } from '../../types'
import logo from '../../images/logo-background.svg'
import { CopyToClipboard } from '../../components'
export const CreateAccountContent: React.FC<{ showSignIn: () => void }> = ({
showSignIn,
}) => {
const [accountDetails, setAccountDetails] = useState<TCreateAccount>()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error>()
const handleCreateAccount = async () => {
setIsLoading(true)
setError(undefined)
try {
const res = await createAccount()
setTimeout(() => {
setAccountDetails(res)
setIsLoading(false)
}, 2500)
} catch (e: any) {
setError(e)
}
}
useEffect(() => {
handleCreateAccount()
}, [])
if (isLoading) return <CircularProgress size={70} />
return (
<Stack spacing={4} alignItems="center">
<img src={logo} width={80} />
<Typography sx={{ color: 'common.white' }} variant="h4">
Congratulations
</Typography>
<Typography sx={{ color: 'common.white' }} variant="h6">
Account setup complete!
</Typography>
<Alert
severity="info"
variant="outlined"
sx={{ color: 'info.light' }}
data-testid="mnemonic-warning"
>
<Typography>
Please store your mnemonic in a safe place. You'll need it to access
your account!
</Typography>
</Alert>
<Card
variant="outlined"
sx={{ bgcolor: 'transparent', p: 2, borderColor: 'common.white' }}
>
<CardHeader sx={{ color: 'common.white' }} title="Mnemonic" />
<CardContent
sx={{ color: 'common.white' }}
data-testid="mnemonic-phrase"
>
{accountDetails?.mnemonic}
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<CopyToClipboard text={accountDetails?.mnemonic || ''} light />
</CardActions>
</Card>
<Box sx={{ textAlign: 'center' }}>
<Typography sx={{ color: 'common.white' }}>Address:</Typography>
<Typography sx={{ color: 'common.white' }} data-testid="wallet-address">
{accountDetails?.client_address}
</Typography>
</Box>
{error && (
<Alert severity="error" variant="outlined">
{error}
</Alert>
)}
<Button
variant="contained"
onClick={showSignIn}
data-testid="sign-in-button"
startIcon={<ArrowBack />}
size="large"
sx={{ width: 360 }}
>
Back to Sign in
</Button>
</Stack>
)
}
+38
View File
@@ -0,0 +1,38 @@
import React, { useContext, useState } from 'react'
import { Box } from '@mui/system'
import { SignInContent } from './sign-in'
import { CreateAccountContent } from './create-account'
export const SignIn = () => {
const [showCreateAccount, setShowCreateAccount] = useState(false)
return (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'auto',
bgcolor: (theme) => theme.palette.nym.background.dark,
}}
>
<Box
sx={{
width: 500,
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
{showCreateAccount ? (
<CreateAccountContent
showSignIn={() => setShowCreateAccount(false)}
/>
) : (
<SignInContent showCreateAccount={() => setShowCreateAccount(true)} />
)}
</Box>
</Box>
)
}
+128
View File
@@ -0,0 +1,128 @@
import React, { useContext, useState } from 'react'
import {
Button,
CircularProgress,
Grid,
Stack,
Link,
TextField,
Typography,
Alert,
} from '@mui/material'
import { styled } from '@mui/styles'
import logo from '../../images/logo-background.svg'
import { signInWithMnemonic } from '../../requests'
import { ClientContext } from '../../context/main'
export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({
showCreateAccount,
}) => {
const [mnemonic, setMnemonic] = useState<string>('')
const [inputError, setInputError] = useState<string>()
const [isLoading, setIsLoading] = useState(false)
const { logIn } = useContext(ClientContext)
const handleSignIn = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault()
setIsLoading(true)
setInputError(undefined)
try {
const res = await signInWithMnemonic(mnemonic || '')
setIsLoading(false)
logIn(res)
} catch (e: any) {
setIsLoading(false)
setInputError(e)
}
}
return (
<Stack spacing={3} alignItems="center" sx={{ width: '80%' }}>
<img src={logo} style={{ width: 80 }} />
<Typography sx={{ color: 'common.white' }}>
Enter Mnemonic and sign in
</Typography>
<Grid container direction="column" spacing={3}>
<Grid item style={{ paddingTop: 0 }}>
<StyledInput
value={mnemonic}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setMnemonic(e.target.value)
}
size="medium"
variant="outlined"
margin="normal"
required
fullWidth
id="mnemonic"
label="BIP-39 Mnemonic"
name="mnemonic"
autoComplete="mnemonic"
autoFocus
disabled={isLoading}
sx={{ m: 0 }}
/>
</Grid>
<Grid item>
<Button
fullWidth
variant="contained"
color="primary"
disabled={isLoading}
endIcon={isLoading && <CircularProgress size={20} />}
disableElevation
size="large"
onClick={handleSignIn}
>
{!isLoading ? 'Sign In' : 'Signing in'}
</Button>
</Grid>
{inputError && (
<Grid item sx={{ mt: 1 }}>
<Alert
severity="error"
variant="outlined"
data-testid="error"
sx={{ color: 'error.light' }}
>
{inputError}
</Alert>
</Grid>
)}
</Grid>
<div>
<Typography sx={{ color: 'common.white' }} component="span">
Don't have an account?
</Typography>{' '}
<Link href="#" onClick={showCreateAccount}>
Create one now
</Link>
</div>
</Stack>
)
}
const StyledInput = styled((props) => <TextField {...props} />)(
({ theme }) => ({
'& input': {
color: theme.palette.nym.text.light,
},
'& label': {
color: theme.palette.nym.text.light,
},
'& label.Mui-focused': {
color: theme.palette.primary.main,
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: theme.palette.common.white,
},
'&:hover fieldset': {
borderColor: theme.palette.primary.main,
},
},
}),
)
+7 -12
View File
@@ -1,20 +1,15 @@
import React, { useContext, useEffect, useState } from 'react'
import { Alert, Box, Button, CircularProgress, Theme } from '@mui/material'
import { NymCard } from '../../components'
import { UnbondForm } from './UnbondForm'
import { Layout } from '../../layouts'
import { useCheckOwnership } from '../../hooks/useCheckOwnership'
import { Alert } from '@material-ui/lab'
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
import { ClientContext } from '../../context/main'
import { unbond } from '../../requests'
import { useTheme } from '@material-ui/styles'
export const Unbond = () => {
const [isLoading, setIsLoading] = useState(false)
const { checkOwnership, ownership } = useCheckOwnership()
const { getBalance } = useContext(ClientContext)
const theme: Theme = useTheme()
const { userBalance } = useContext(ClientContext)
useEffect(() => {
const initialiseForm = async () => {
@@ -37,29 +32,29 @@ export const Unbond = () => {
onClick={async () => {
setIsLoading(true)
await unbond(ownership.nodeType!)
getBalance.fetchBalance()
userBalance.fetchBalance()
setIsLoading(false)
}}
>
Unbond
</Button>
}
style={{ margin: theme.spacing(2) }}
sx={{ m: 2 }}
>
{`Looks like you already have a ${ownership.nodeType} bonded.`}
</Alert>
)}
{!ownership.hasOwnership && (
<Alert severity="info" style={{ margin: theme.spacing(3) }} data-testid="no-bond">
<Alert severity="info" sx={{ m: 3 }} data-testid="no-bond">
You don't currently have a bonded node
</Alert>
)}
{isLoading && (
<Box
style={{
sx={{
display: 'flex',
justifyContent: 'center',
padding: theme.spacing(3),
p: 3,
}}
>
<CircularProgress size={48} />
@@ -1,18 +1,17 @@
import React, { useContext, useEffect } from 'react'
import { useForm, Controller } from 'react-hook-form'
import {
Box,
Alert,
Autocomplete,
Button,
CircularProgress,
FormControl,
Grid,
TextField,
Theme,
} from '@material-ui/core'
import { Alert, Autocomplete } from '@material-ui/lab'
import { useTheme } from '@material-ui/styles'
} from '@mui/material'
import { yupResolver } from '@hookform/resolvers/yup'
import { validationSchema } from './validationSchema'
import { NodeTypeSelector } from '../../components/NodeTypeSelector'
import { EnumNodeType, TFee } from '../../types'
import { ClientContext } from '../../context/main'
import { undelegate } from '../../requests'
@@ -55,9 +54,7 @@ export const UndelegateForm = ({
setValue('identity', '')
}, [watchNodeType])
const { getBalance } = useContext(ClientContext)
const theme: Theme = useTheme()
const { userBalance } = useContext(ClientContext)
const onSubmit = async (data: TFormData) => {
await undelegate({
@@ -66,14 +63,14 @@ export const UndelegateForm = ({
})
.then(async (res) => {
onSuccess(`Successfully undelegated from ${res.target_address}`)
getBalance.fetchBalance()
userBalance.fetchBalance()
})
.catch((e) => onError(e))
}
return (
<FormControl fullWidth>
<div style={{ padding: theme.spacing(3, 5) }}>
<Box sx={{ p: [3, 5] }}>
<Grid container spacing={3} direction="column">
<Grid container item xs={12} justifyContent="space-between">
<Grid item>
@@ -113,15 +110,15 @@ export const UndelegateForm = ({
/>
</Grid>
</Grid>
</div>
<div
style={{
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`,
background: (theme) => theme.palette.grey[50],
p: 2,
}}
>
<Button
@@ -136,7 +133,7 @@ export const UndelegateForm = ({
>
Undelegate stake
</Button>
</div>
</Box>
</FormControl>
)
}
+11 -19
View File
@@ -1,6 +1,4 @@
import React, { useEffect, useState } from 'react'
import { Alert, AlertTitle } from '@material-ui/lab'
import { useTheme } from '@material-ui/styles'
import { NymCard } from '../../components'
import { UndelegateForm } from './UndelegateForm'
import { Layout } from '../../layouts'
@@ -8,12 +6,8 @@ import {
EnumRequestStatus,
RequestStatus,
} from '../../components/RequestStatus'
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
import {
getGasFee,
getReverseGatewayDelegations,
getReverseMixDelegations,
} from '../../requests'
import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material'
import { getGasFee, getReverseMixDelegations } from '../../requests'
import { TFee, TDelegation } from '../../types'
export type TDelegations = {
@@ -23,7 +17,7 @@ export type TDelegations = {
export const Undelegate = () => {
const [message, setMessage] = useState<string>()
const [status, setStatus] = useState<EnumRequestStatus>(
EnumRequestStatus.initial
EnumRequestStatus.initial,
)
const [isLoading, setIsLoading] = useState(true)
const [fees, setFees] = useState<TFee>()
@@ -57,8 +51,6 @@ export const Undelegate = () => {
setIsLoading(false)
}
const theme: Theme = useTheme()
return (
<Layout>
<NymCard
@@ -68,10 +60,10 @@ export const Undelegate = () => {
>
{isLoading && (
<Box
style={{
sx={{
display: 'flex',
justifyContent: 'center',
padding: theme.spacing(3),
p: 3,
}}
>
<CircularProgress size={48} />
@@ -111,14 +103,14 @@ export const Undelegate = () => {
</Alert>
}
/>
<div
style={{
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
borderTop: `1px solid ${theme.palette.grey[200]}`,
background: theme.palette.grey[100],
padding: theme.spacing(2),
borderTop: '1px solid grey[200]',
bgcolor: 'grey[50]',
p: 2,
}}
>
<Button
@@ -130,7 +122,7 @@ export const Undelegate = () => {
>
Finish
</Button>
</div>
</Box>
</>
)}
</>
-66
View File
@@ -1,66 +0,0 @@
import { createTheme } from '@material-ui/core'
const nymPalette = {
primary: {
main: '#F4731B',
},
secondary: {
main: '#009FA8',
},
background: {
main: '#121726',
},
}
export const theme = createTheme({
palette: {
primary: {
main: nymPalette.primary.main,
},
secondary: {
main: nymPalette.secondary.main,
},
},
shape: {
borderRadius: 24,
},
overrides: {
MuiButton: {
root: {
padding: '12px 24px',
},
containedPrimary: {
color: 'white',
},
text: {
padding: '12px 24px',
},
outlined: {
padding: '12px 24px',
},
textSizeSmall: {
padding: '6px 12px',
},
outlinedSizeSmall: {
padding: '8px 12px',
},
containedSizeSmall: {
padding: '8px 12px',
},
},
MuiStepIcon: {
text: {
fill: '#fff',
},
},
MuiTooltip: {
tooltipPlacementBottom: {
background: nymPalette.background.main,
padding: '8px 12px',
fontSize: 12,
},
},
},
})
+19
View File
@@ -0,0 +1,19 @@
import React, { useContext } from 'react'
import { createTheme, ThemeProvider } from '@mui/material/styles'
import { CssBaseline } from '@mui/material'
import { getDesignTokens } from './theme'
import { ClientContext } from '../context/main'
/**
* Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context.
*/
export const NymWalletTheme: React.FC = ({ children }) => {
const { mode } = useContext(ClientContext)
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode])
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
)
}
+86
View File
@@ -0,0 +1,86 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface */
import {
Theme,
ThemeOptions,
Palette,
PaletteOptions,
} from '@mui/material/styles'
import { PaletteMode } from '@mui/material'
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
/**
* This interface defines a palette used across Nym for branding
*/
interface NymPalette {
highlight: string
background: { light: string; dark: string }
text: {
light: string
}
}
interface NymPaletteVariant {
mode: PaletteMode
background: {
main: string
paper: string
}
text: {
main: string
}
topNav: {
background: string
}
}
/**
* A palette definition only for the Nym Wallet that extends the Nym palette
*/
interface NymWalletPalette {
nymWallet: {}
}
interface NymPaletteAndNymWalletPalette {
nym: NymPalette & NymWalletPalette
}
type NymPaletteAndNymWalletPaletteOptions =
Partial<NymPaletteAndNymWalletPalette>
/**
* Add anything not palette related to the theme here
*/
interface NymTheme {}
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteAndNymWalletPalette {}
interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions {}
}
+201
View File
@@ -0,0 +1,201 @@
import { PaletteMode } from '@mui/material'
import {
PaletteOptions,
NymPalette,
NymWalletPalette,
ThemeOptions,
createTheme,
NymPaletteVariant,
} from '@mui/material/styles'
//-----------------------------------------------------------------------------------------------
// Nym palette type definitions
//
/**
* The Nym palette.
*
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode.
*/
const nymPalette: NymPalette = {
/** emphasises important elements */
highlight: '#FB6E4E',
background: { light: '#F4F6F8', dark: '#121726' },
text: {
light: '#F2F2F2',
},
}
const darkMode: NymPaletteVariant = {
mode: 'dark',
background: {
main: '#121726',
paper: '#242C3D',
},
text: {
main: '#F2F2F2',
},
topNav: {
background: '#111826',
},
}
const lightMode: NymPaletteVariant = {
mode: 'light',
background: {
main: '#F2F2F2',
paper: '#FFFFFF',
},
text: {
main: '#666666',
},
topNav: {
background: '#111826',
},
}
/**
* Nym palette specific to the Nym Wallet
*
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode.
*/
const nymWalletPalette = (variant: NymPaletteVariant): NymWalletPalette => ({
nymWallet: {},
})
//-----------------------------------------------------------------------------------------------
// Nym palettes for light and dark mode
//
/**
* Map a Nym palette variant onto the MUI palette
*/
const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
text: {
primary: variant.text.main,
},
primary: {
main: nymPalette.highlight,
contrastText: '#fff',
},
background: {
default: variant.background.main,
paper: variant.background.paper,
},
})
/**
* Returns the Network Explorer palette for light mode.
*/
const createLightModePalette = (): PaletteOptions => ({
nym: {
...nymPalette,
...nymWalletPalette(lightMode),
},
...variantToMUIPalette(lightMode),
})
/**
* Returns the Network Explorer palette for dark mode.
*/
const createDarkModePalette = (): PaletteOptions => ({
nym: {
...nymPalette,
...nymWalletPalette(darkMode),
},
...variantToMUIPalette(darkMode),
})
/**
* IMPORANT: if you need to get the default MUI theme, use the following
*
* import { createTheme as systemCreateTheme } from '@mui/system';
*
* // get the MUI system defaults for light mode
* const systemTheme = systemCreateTheme({ palette: { mode: 'light' } });
*
*
* return {
* // change `primary` to default MUI `success`
* primary: {
* main: systemTheme.palette.success.main,
* },
* nym: {
* ...nymPalette,
* ...nymWalletPalette,
* },
* };
*/
//-----------------------------------------------------------------------------------------------
// Nym theme overrides
//
/**
* Gets the theme options to be passed to `createTheme`.
*
* Based on pattern from https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette.
*
* @param mode The theme mode: 'light' or 'dark'
*/
export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
// first, create the palette from user's choice of light or dark mode
const { palette } = createTheme({
palette: {
mode,
...(mode === 'light'
? createLightModePalette()
: createDarkModePalette()),
},
})
// then customise theme and components
return {
typography: {
fontFamily: [
'Open Sans',
'sans-serif',
'BlinkMacSystemFont',
'Roboto',
'Oxygen',
'Ubuntu',
'Helvetica Neue',
].join(','),
fontSize: 14,
fontWeightRegular: 500,
button: {
textTransform: 'none',
fontWeight: '600',
},
},
shape: {
borderRadius: 8,
},
transitions: {
duration: {
shortest: 150,
shorter: 200,
short: 250,
standard: 300,
complex: 375,
enteringScreen: 225,
leavingScreen: 195,
},
easing: {
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
},
},
components: {
MuiButton: {
styleOverrides: {
sizeLarge: {
height: 55,
},
},
},
},
palette,
}
}
+4 -1
View File
@@ -23,7 +23,10 @@ export type TCreateAccount = {
mnemonic: string
} & TSignInWithMnemonic
export type TFee = { [EnumNodeType.mixnode]: Coin }
export type TFee = {
[EnumNodeType.mixnode]: Coin
[EnumNodeType.gateway]?: Coin
}
export type TDelegation = {
delegated_nodes: string[]
+3 -3
View File
@@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api'
import bs58 from 'bs58'
import { minor, valid } from 'semver'
import { getBalance, majorToMinor } from '../requests'
import { userBalance, majorToMinor } from '../requests'
import { Coin } from '../types'
export const validateKey = (key: string): boolean => {
@@ -18,7 +18,7 @@ export const validateKey = (key: string): boolean => {
export const validateAmount = async (
amount: string,
minimum: string
minimum: string,
): Promise<boolean> => {
// tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc
if (!Number(amount)) {
@@ -93,6 +93,6 @@ export const truncate = (text: string, trim: number) =>
export const checkHasEnoughFunds = async (allocationValue: string) => {
const minorValue = await majorToMinor(allocationValue)
const walletValue = await getBalance()
const walletValue = await userBalance()
return !(+walletValue.coin.amount - +minorValue.amount < 0)
}
+1
View File
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
+3
View File
@@ -10,6 +10,9 @@ module.exports = {
compress: true,
historyApiFallback: true,
hot: true,
client: {
overlay: false,
},
},
plugins: [
new HtmlWebpackPlugin({
+2108 -1940
View File
File diff suppressed because it is too large Load Diff