Password for wallet with routes (#1187)

* new password flow

* update global error and load state from children

* fix linting

* dont load account when creating mnemonic

* wallets: provide placeholder functions for ui password

* wallet: platform_constants

* wallet: swap println to log

* UI for existing mnemonic to be use

* wallet: inline encryption of wallet file

* wallet: tweak error enum names

* wallet: general wallet_storage tidy

* wallet: tweak some type names

* create sign-in context

* update sign in functions

* move state to context

* update pages

* connect new rust methods with frontend

* update components

* remove non-existent method

* add separate sign in pages for mnemonic and password

* add a hook for clipboard copy

* fix workmark svg sizing issue

* create step component

* use new sign in  pages

* reorder pages

* use clipboard lib directly

* ui tweaks

* use login type selector

* update password strength test + use autofocus prop for password input

* start adding routes

* restructure with routes

* wip

* more wip

* more wip

* reset state where required

* wallet: remove unused rust use statements

* fix unbond page

Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
Co-authored-by: Tommy Verrall <tommyvez@protonmail.com>
This commit is contained in:
Mark Sinclair
2022-04-01 18:44:44 +01:00
committed by GitHub
parent 9ea2ce5c70
commit 2779b5d28a
50 changed files with 935 additions and 526 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<svg width="210" height="56" viewBox="0 0 210 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 210 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.8829 0.142822H45.7169V0.28114V48.637L25.3289 0.225818L25.3012 0.142822H25.1905H13.6272H0.652966H0.514648V0.28114V55.7189V55.8572H0.652966H13.6272H13.7655V55.7189V7.28002L34.2365 55.7742L34.2642 55.8572H34.3748H45.8829H58.8294H58.9677V55.7189V0.28114V0.142822H58.8294H45.8829Z"/>
<path d="M209.347 0.142822H184.616H184.477L184.45 0.253483L171.78 48.8583L159.082 0.253483L159.054 0.142822H158.944H134.157H133.991V0.28114V55.7189V55.8572H134.157H147.104H147.242V55.7189V7.66731L159.774 55.7466L159.801 55.8572H159.94H183.564H183.675L183.703 55.7466L196.234 7.66731V55.7189V55.8572H196.373H209.347H209.485V55.7189V0.28114V0.142822H209.347Z"/>
<path d="M112.663 0.142822H112.58L112.552 0.198153L96.8116 27.5574L80.988 0.198153L80.9604 0.142822H80.8774H65.9114H65.6348L65.7731 0.364136L90.1447 42.5787V55.7189V55.8572H90.283H103.257H103.396V55.7189V42.5787L127.767 0.364136L127.905 0.142822H127.629H112.663Z"/>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1011 B

+1
View File
@@ -42,6 +42,7 @@
"react-hook-form": "^7.14.2",
"react-router-dom": "^5.2.0",
"semver": "^6.3.0",
"use-clipboard-copy": "^0.2.0",
"yup": "^0.32.9"
},
"devDependencies": {
+1 -2
View File
@@ -18,8 +18,6 @@ mod operations;
mod platform_constants;
mod state;
mod utils;
// temporarily until it is actually used
#[allow(unused)]
mod wallet_storage;
use crate::menu::AddDefaultSubmenus;
@@ -37,6 +35,7 @@ fn main() {
.manage(Arc::new(RwLock::new(State::default())))
.invoke_handler(tauri::generate_handler![
mixnet::account::connect_with_mnemonic,
mixnet::account::validate_mnemonic,
mixnet::account::create_new_account,
mixnet::account::create_new_mnemonic,
mixnet::account::create_password,
@@ -112,9 +112,8 @@ pub async fn create_new_mnemonic() -> Result<String, BackendError> {
}
#[tauri::command]
pub fn validate_mnemonic(mnemonic: &str) -> Result<(), BackendError> {
Mnemonic::from_str(mnemonic)?;
Ok(())
pub fn validate_mnemonic(mnemonic: &str) -> bool {
Mnemonic::from_str(mnemonic).is_ok()
}
#[tauri::command]
@@ -2,11 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use cosmrs::bip32::DerivationPath;
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::Formatter;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
use zeroize::Zeroizing;
use crate::error::BackendError;
@@ -23,6 +20,7 @@ pub(crate) struct StoredWallet {
}
impl StoredWallet {
#[allow(unused)]
pub fn version(&self) -> u32 {
self.version
}
@@ -31,6 +29,7 @@ impl StoredWallet {
self.accounts.is_empty()
}
#[allow(unused)]
pub fn len(&self) -> usize {
self.accounts.len()
}
@@ -45,6 +44,7 @@ impl StoredWallet {
}
}
#[allow(unused)]
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
self.accounts.get(index)
}
@@ -146,6 +146,7 @@ impl MnemonicAccount {
&self.mnemonic
}
#[allow(unused)]
pub(crate) fn hd_path(&self) -> &DerivationPath {
&self.hd_path
}
@@ -4,14 +4,13 @@
use super::password::UserPassword;
use crate::error::BackendError;
use aes_gcm::aead::generic_array::ArrayLength;
use aes_gcm::aead::{Aead, NewAead, Payload};
use aes_gcm::aead::{Aead, NewAead};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use argon2::{
password_hash::rand_core::{OsRng, RngCore},
Algorithm, Argon2, Params, Version,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::convert::TryFrom;
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use zeroize::Zeroize;
@@ -78,6 +77,7 @@ mod base64 {
}
impl<T> EncryptedData<T> {
#[allow(unused)]
pub(crate) fn encrypt_struct(data: &T, password: &UserPassword) -> Result<Self, BackendError>
where
T: Serialize,
@@ -94,10 +94,12 @@ impl<T> EncryptedData<T> {
}
impl EncryptedData<Vec<u8>> {
#[allow(unused)]
pub(crate) fn encrypt_data(data: &[u8], password: &UserPassword) -> Result<Self, BackendError> {
encrypt_data(data, password)
}
#[allow(unused)]
pub(crate) fn decrypt_data(&self, password: &UserPassword) -> Result<Vec<u8>, BackendError> {
decrypt_data(self, password)
}
@@ -159,6 +161,7 @@ fn decrypt(
.map_err(|_| BackendError::DecryptionError)
}
#[allow(unused)]
pub(crate) fn encrypt_data(
data: &[u8],
password: &UserPassword,
@@ -194,6 +197,7 @@ where
})
}
#[allow(unused)]
pub(crate) fn decrypt_data(
encrypted_data: &EncryptedData<Vec<u8>>,
password: &UserPassword,
@@ -4,14 +4,11 @@
pub(crate) use crate::wallet_storage::password::{UserPassword, WalletAccountId};
use crate::error::BackendError;
use crate::operations::mixnet::account::create_new_account;
use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME};
use crate::wallet_storage::account_data::StoredAccount;
use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData};
use crate::wallet_storage::encryption::encrypt_struct;
use cosmrs::bip32::DerivationPath;
use serde::{Deserialize, Serialize};
use std::fs::{self, create_dir_all, OpenOptions};
use std::os::unix::prelude::OpenOptionsExt;
use std::path::PathBuf;
use self::account_data::{EncryptedAccount, StoredWallet};
@@ -33,6 +30,7 @@ pub(crate) fn wallet_login_filepath() -> Result<PathBuf, BackendError> {
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
}
#[allow(unused)]
pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result<StoredWallet, BackendError> {
let store_dir = get_storage_directory()?;
let filepath = store_dir.join(WALLET_INFO_FILENAME);
@@ -154,9 +152,7 @@ pub(crate) fn remove_wallet_login_information_at_file(
#[cfg(test)]
mod tests {
use super::*;
use crate::wallet_storage::encryption::encrypt_data;
use config::defaults::COSMOS_DERIVATION_PATH;
use std::path::Path;
use tempfile::tempdir;
// I'm not 100% sure how to feel about having to touch the file system at all
@@ -3,7 +3,7 @@
use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -22,9 +22,9 @@ impl AsRef<str> for WalletAccountId {
}
impl fmt::Display for WalletAccountId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
// simple wrapper for String that will get zeroized on drop
+21 -7
View File
@@ -1,10 +1,11 @@
import React, { useMemo, createContext, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useSnackbar } from 'notistack';
import { TLoginType } from 'src/pages/sign-in/types';
import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types';
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
import { config } from '../../config';
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signOut } from '../requests';
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signInWithPassword, signOut } from '../requests';
import { currencyMap } from '../utils';
import { Console } from '../utils/console';
@@ -32,11 +33,14 @@ type TClientContext = {
currency?: TCurrency;
isLoading: boolean;
error?: string;
setIsLoading: (isLoading: boolean) => void;
setError: (value?: string) => void;
switchNetwork: (network: Network) => void;
getBondDetails: () => Promise<void>;
handleShowSettings: () => void;
handleShowAdmin: () => void;
logIn: (mnemonic: string) => void;
logIn: (opts: { type: 'mnemonic' | 'password'; value: string }) => void;
signInWithPassword: (password: string) => void;
logOut: () => void;
};
@@ -90,17 +94,24 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
refreshAccount();
}, [network]);
const logIn = async (mnemonic: string) => {
const logIn = async ({ type, value }: { type: TLoginType; value: string }) => {
if (value.length === 0) {
setError(`A ${type} must be provided`);
return;
}
try {
setIsLoading(true);
await signInWithMnemonic(mnemonic || '');
await getBondDetails();
if (type === 'mnemonic') {
await signInWithMnemonic(value);
} else {
await signInWithPassword(value);
}
setNetwork('MAINNET');
history.push('/balance');
} catch (e) {
setIsLoading(false);
setError(e as string);
} finally {
history.push('/balance');
setIsLoading(false);
}
};
@@ -131,6 +142,9 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
showSettings,
network,
currency,
setIsLoading,
setError,
signInWithPassword,
switchNetwork,
getBondDetails,
handleShowSettings,
+7 -4
View File
@@ -3,13 +3,14 @@ import ReactDOM from 'react-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { BrowserRouter as Router } from 'react-router-dom';
import { SnackbarProvider } from 'notistack';
import { Routes } from './routes';
import { AppRoutes, SignInRoutes } from './routes';
import { ClientContext, ClientContextProvider } from './context/main';
import { ApplicationLayout } from './layouts';
import { Admin, Welcome, Settings } from './pages';
import { Admin, Settings } from './pages';
import { ErrorFallback } from './components';
import { NymWalletTheme, WelcomeTheme } from './theme';
import { maximizeWindow } from './utils';
import { SignInProvider } from './pages/sign-in/context';
const App = () => {
const { clientDetails } = useContext(ClientContext);
@@ -20,14 +21,16 @@ const App = () => {
return !clientDetails ? (
<WelcomeTheme>
<Welcome />
<SignInProvider>
<SignInRoutes />
</SignInProvider>
</WelcomeTheme>
) : (
<NymWalletTheme>
<ApplicationLayout>
<Settings />
<Admin />
<Routes />
<AppRoutes />
</ApplicationLayout>
</NymWalletTheme>
);
+1 -1
View File
@@ -5,7 +5,7 @@ export * from './delegate';
export * from './internal-docs';
export * from './receive';
export * from './send';
export * from './welcome';
export * from './sign-in';
export * from './settings';
export * from './unbond';
export * from './undelegate';
@@ -16,6 +16,9 @@ export const SendConfirmation = ({
isLoading: boolean;
}) => {
const { userBalance, currency, network } = useContext(ClientContext);
if (!data && !error && !isLoading) return null;
return (
<Box
sx={{
+13 -8
View File
@@ -128,13 +128,9 @@ export const SendWizard = () => {
px: 3,
}}
>
{activeStep === 0 ? (
<SendForm />
) : activeStep === 1 ? (
<SendReview transferFee={transferFee} />
) : (
<SendConfirmation data={confirmedData} isLoading={isLoading} error={requestError} />
)}
{activeStep === 0 && <SendForm />}
{activeStep === 1 && <SendReview transferFee={transferFee} />}
<SendConfirmation data={confirmedData} isLoading={isLoading} error={requestError} />
</Box>
<Box
sx={{
@@ -154,7 +150,16 @@ export const SendWizard = () => {
color="primary"
disableElevation
data-testid="button"
onClick={activeStep === 0 ? handleNextStep : activeStep === 1 ? handleSend : handleFinish}
onClick={() => {
switch (activeStep) {
case 0:
return handleNextStep();
case 1:
return handleSend();
default:
return handleFinish();
}
}}
disabled={!!(methods.formState.errors.amount || methods.formState.errors.to || isLoading)}
size="large"
>
@@ -139,13 +139,13 @@ export const SystemVariables = ({
pt: 0,
}}
>
{nodeUpdateResponse === 'success' ? (
{nodeUpdateResponse === 'success' && (
<Typography sx={{ color: 'success.main', fontWeight: 600 }}>Node successfully updated</Typography>
) : nodeUpdateResponse === 'failed' ? (
<Typography sx={{ color: 'error.main', fontWeight: 600 }}>Node update failed</Typography>
) : (
<Fee feeType="UpdateMixnodeConfig" />
)}
{nodeUpdateResponse === 'failed' && (
<Typography sx={{ color: 'error.main', fontWeight: 600 }}>Node update failed</Typography>
)}
{!nodeUpdateResponse && <Fee feeType="UpdateMixnodeConfig" />}
<Button
variant="contained"
color="primary"
@@ -0,0 +1,8 @@
import React from 'react';
import { Alert } from '@mui/material';
export const Error = ({ message }: { message: string }) => (
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light', width: '100%' }}>
{message}
</Alert>
);
@@ -6,7 +6,7 @@ export const Title = ({ title }: { title: string }) => (
);
export const Subtitle = ({ subtitle }: { subtitle: string }) => (
<Typography sx={{ color: 'common.white', textAlign: 'center', maxWidth: 400 }}>{subtitle}</Typography>
<Typography sx={{ color: 'common.white', textAlign: 'center', maxWidth: 450 }}>{subtitle}</Typography>
);
export const SubtitleSlick = ({ subtitle }: { subtitle: string }) => (
@@ -2,3 +2,7 @@ export * from './heading';
export * from './word-tiles';
export * from './render-page';
export * from './password-strength';
export * from './error';
export * from './textfields';
export * from './step';
export * from './page-layout';
@@ -0,0 +1,33 @@
import React from 'react';
import { Stack, Box } from '@mui/material';
import { NymWordmark } from '@nymproject/react';
import { Step } from './step';
export const PageLayout: React.FC = ({ children }) => (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'auto',
bgcolor: 'nym.background.dark',
}}
>
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
<NymWordmark width={75} />
<Step />
{children}
</Stack>
</Box>
</Box>
);
@@ -5,8 +5,8 @@ import { LinearProgress, Stack, Typography, Box } from '@mui/material';
type TStrength = 'weak' | 'medium' | 'strong' | 'init';
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
const medium = /^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})/;
const strong = /^(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
const medium = /^(((?=.*[a-z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[0-9])))(?=.{6,})/;
const colorMap = {
init: 'inherit' as 'inherit',
@@ -41,7 +41,24 @@ const getTextColor = (strength: TStrength) => {
}
};
export const PasswordStrength = ({ password }: { password: string }) => {
const getPasswordStrength = (strength: TStrength) => {
switch (strength) {
case 'strong':
return 100;
case 'medium':
return 50;
default:
return 0;
}
};
export const PasswordStrength = ({
password,
onChange,
}: {
password: string;
onChange: (isStrong: boolean) => void;
}) => {
const [strength, setStrength] = useState<TStrength>('init');
useEffect(() => {
@@ -62,13 +79,17 @@ export const PasswordStrength = ({ password }: { password: string }) => {
setStrength('weak');
}, [password]);
useEffect(() => {
if (strength === 'strong') {
onChange(true);
} else {
onChange(false);
}
}, [strength]);
return (
<Stack spacing={0.5}>
<LinearProgress
variant="determinate"
color={colorMap[strength]}
value={strength === 'strong' ? 100 : strength === 'medium' ? 50 : 0}
/>
<LinearProgress variant="determinate" color={colorMap[strength]} value={getPasswordStrength(strength)} />
<Box display="flex" alignItems="center">
<LockOutlined sx={{ fontSize: 15, color: getTextColor(strength) }} />
<Typography variant="caption" sx={{ ml: 0.5, color: getTextColor(strength) }}>
@@ -0,0 +1,30 @@
import React, { useCallback } from 'react';
import { Typography } from '@mui/material';
import { useLocation } from 'react-router-dom';
export const Step = () => {
const location = useLocation();
const mapPage = useCallback(() => {
switch (location.pathname) {
case '/create-mnemonic':
return { value: 1, type: 'account', total: 3 };
case '/verify-mnemonic':
return { value: 2, type: 'account', total: 3 };
case '/create-password':
return { value: 3, type: 'account', total: 3 };
case '/confirm-mnemonic':
return { value: 1, type: 'account password', total: 2 };
case '/connect-password':
return { value: 2, type: 'account password', total: 2 };
default:
return { value: 0, type: '', total: 0 };
}
}, [location.pathname]);
if (mapPage().value === 0) {
return null;
}
const { value, type, total } = mapPage();
return <Typography sx={{ color: 'grey.400' }}>{`Create ${type}. Step ${value}/${total}`}</Typography>;
};
@@ -0,0 +1,80 @@
import React, { useState } from 'react';
import { Box, IconButton, Link, Stack, TextField } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { Error } from './error';
export const MnemonicInput: React.FC<{
mnemonic: string;
error?: string;
onUpdateMnemonic: (mnemonic: string) => void;
}> = ({ mnemonic, error, onUpdateMnemonic }) => {
const [showPassword, setShowPassword] = useState(false);
return (
<Stack spacing={2}>
<TextField
label="Mnemonic"
type={showPassword ? 'input' : 'password'}
value={mnemonic}
onChange={(e) => onUpdateMnemonic(e.target.value)}
multiline={!!showPassword}
rows={4}
autoFocus
fullWidth
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowPassword((show) => !show)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
{error && <Error message={error} />}
</Stack>
);
};
export const PasswordInput: React.FC<{
password: string;
error?: string;
label: string;
showForgottenPassword?: boolean;
autoFocus?: boolean;
onUpdatePassword: (password: string) => void;
}> = ({ password, label, error, showForgottenPassword, autoFocus, onUpdatePassword }) => {
const [showPassword, setShowPassword] = useState(false);
return (
<Stack spacing={2}>
<Box>
<TextField
label={label}
fullWidth
value={password}
onChange={(e) => onUpdatePassword(e.target.value)}
type={showPassword ? 'input' : 'password'}
autoFocus={autoFocus}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowPassword((show) => !show)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
{/* currently unused */}
{showForgottenPassword && (
<Link
underline="none"
variant="body2"
component="div"
sx={{ mt: 1, textAlign: 'right', color: 'info.main', cursor: 'pointer' }}
href="/forgotten-password"
>
Forgotten password?
</Link>
)}
</Box>
{error && <Error message={error} />}
</Stack>
);
};
@@ -7,17 +7,19 @@ export const WordTile = ({
index,
disabled,
onClick,
button,
}: {
mnemonicWord: string;
index?: number;
disabled?: boolean;
onClick?: boolean;
button?: boolean;
}) => (
<Card
variant="outlined"
sx={{
background: '#151A2C',
border: '1px solid #3A4053',
background: button ? '#151A2C' : 'transparent',
border: button ? '1px solid #3A4053' : 'none',
cursor: onClick ? 'pointer' : 'default',
opacity: disabled ? 0.2 : 1,
}}
@@ -40,10 +42,12 @@ export const WordTiles = ({
mnemonicWords,
showIndex,
onClick,
buttons,
}: {
mnemonicWords?: TMnemonicWords;
showIndex?: boolean;
onClick?: ({ name, index }: { name: string; index: number }) => void;
buttons?: boolean;
}) => {
if (mnemonicWords) {
return (
@@ -55,6 +59,7 @@ export const WordTiles = ({
index={showIndex ? index : undefined}
onClick={!!onClick}
disabled={disabled}
button={buttons}
/>
</Grid>
))}
@@ -0,0 +1,76 @@
import React, { createContext, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { createMnemonic } from 'src/requests';
import { TMnemonicWords } from '../types';
export const SignInContext = createContext({} as TSignInContent);
export type TSignInContent = {
error?: string;
password: string;
mnemonic: string;
mnemonicWords: TMnemonicWords;
setError: (err?: string) => void;
setMnemonic: (mnc: string) => void;
generateMnemonic: () => Promise<void>;
setPassword: (paswd: string) => void;
resetState: () => void;
};
const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
mnemonic
.split(' ')
.reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords);
export const SignInProvider: React.FC = ({ children }) => {
const [password, setPassword] = useState('');
const [mnemonic, setMnemonic] = useState('');
const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>([]);
const [error, setError] = useState<string>();
const history = useHistory();
const generateMnemonic = async () => {
const mnemonicPhrase = await createMnemonic();
setMnemonic(mnemonicPhrase);
};
useEffect(() => {
history.push('/welcome');
}, []);
useEffect(() => {
if (mnemonic.length > 0) {
const mnemonicArray = mnemonicToArray(mnemonic);
setMnemonicWords(mnemonicArray);
} else {
setMnemonicWords([]);
}
}, [mnemonic]);
const resetState = () => {
setPassword('');
setMnemonic('');
};
return (
<SignInContext.Provider
value={useMemo(
() => ({
error,
password,
mnemonic,
mnemonicWords,
setError,
setMnemonic,
generateMnemonic,
setPassword,
resetState,
}),
[error, password, mnemonic, mnemonicWords],
)}
>
{children}
</SignInContext.Provider>
);
};
+1
View File
@@ -0,0 +1 @@
export * from './pages';
@@ -0,0 +1,51 @@
import React, { useContext, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, Stack } from '@mui/material';
import { validateMnemonic } from 'src/requests';
import { MnemonicInput, Subtitle } from '../components';
import { SignInContext } from '../context';
export const ConfirmMnemonic = () => {
const { error, setError, setMnemonic, mnemonic } = useContext(SignInContext);
const [localMnemonic, setLocalMnemonic] = useState(mnemonic);
const history = useHistory();
useEffect(() => {
setError(undefined);
}, [localMnemonic]);
return (
<Stack spacing={2}>
<Subtitle subtitle="Enter the mnemonic you wish to create a password for" />
<MnemonicInput mnemonic={localMnemonic} onUpdateMnemonic={(mnc) => setLocalMnemonic(mnc)} error={error} />
<Button
size="large"
variant="contained"
fullWidth
onClick={async () => {
const isValid = await validateMnemonic(localMnemonic);
if (isValid) {
setMnemonic(localMnemonic);
history.push('/connect-password');
} else {
setError('The mnemonic provided is not valid. Please check the mnemonic');
}
}}
disabled={localMnemonic.length === 0}
>
Next
</Button>
<Button
size="large"
color="inherit"
fullWidth
onClick={() => {
setMnemonic('');
history.goBack();
}}
>
Back
</Button>
</Stack>
);
};
@@ -0,0 +1,75 @@
import React, { useContext, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, CircularProgress, FormControl, Stack } from '@mui/material';
import { useSnackbar } from 'notistack';
import { Subtitle, Title, PasswordStrength } from '../components';
import { PasswordInput } from '../components/textfields';
import { SignInContext } from '../context';
import { createPassword } from '../../../requests';
export const ConnectPassword = () => {
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
const [isStrongPassword, setIsStrongPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { mnemonic, password, setPassword, resetState } = useContext(SignInContext);
const history = useHistory();
const { enqueueSnackbar } = useSnackbar();
const storePassword = async () => {
try {
setIsLoading(true);
await createPassword({ mnemonic, password });
resetState();
enqueueSnackbar('Password successfully created', { variant: 'success' });
history.push('/sign-in-password');
} catch (e) {
enqueueSnackbar(e as string, { variant: 'error' });
setIsLoading(false);
}
};
return (
<Stack spacing={3} alignItems="center" minWidth="50%">
<Title title="Create optional password" />
<Subtitle subtitle="Password should be min 8 characters, at least one number and one symbol" />
<FormControl fullWidth>
<Stack spacing={2}>
<>
<PasswordInput
password={password}
onUpdatePassword={(pswd) => setPassword(pswd)}
label="Password"
autoFocus
/>
<PasswordStrength password={password} onChange={(isStrong) => setIsStrongPassword(isStrong)} />
</>
<PasswordInput
password={confirmedPassword}
onUpdatePassword={(pswd) => setConfirmedPassword(pswd)}
label="Confirm password"
/>
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0 || !isStrongPassword || isLoading}
onClick={storePassword}
>
{isLoading ? <CircularProgress size={25} /> : 'Create password'}
</Button>
<Button
size="large"
color="inherit"
onClick={() => {
setPassword('');
history.goBack();
}}
>
Back
</Button>
</Stack>
</FormControl>
</Stack>
);
};
@@ -0,0 +1,69 @@
import React, { useContext, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Alert, Button, Stack, Typography } from '@mui/material';
import { Check, ContentCopySharp } from '@mui/icons-material';
import { useClipboard } from 'use-clipboard-copy';
import { WordTiles } from '../components';
import { SignInContext } from '../context';
export const CreateMnemonic = () => {
const { mnemonic, mnemonicWords, generateMnemonic, resetState } = useContext(SignInContext);
const history = useHistory();
useEffect(() => {
generateMnemonic();
}, []);
const { copy, copied } = useClipboard({ copiedTimeout: 5000 });
return (
<Stack alignItems="center" spacing={3}>
<Typography sx={{ color: 'common.white', fontWeight: 600 }} textAlign="center">
Write down your mnemonic
</Typography>
<Alert variant="outlined" severity="warning" sx={{ textAlign: 'center' }}>
<Typography>Below is your 24 word mnemonic, please store the mnemonic in a safe place.</Typography>
<Typography>This is the only way to access your wallet!</Typography>
</Alert>
<WordTiles mnemonicWords={mnemonicWords} showIndex />
<Button
color="inherit"
disableElevation
size="large"
onClick={() => {
copy(mnemonic);
}}
sx={{
width: 250,
}}
endIcon={!copied ? <ContentCopySharp /> : <Check color="success" />}
>
Copy mnemonic
</Button>
<Button
variant="contained"
color="primary"
disableElevation
size="large"
onClick={() => history.push('/verify-mnemonic')}
sx={{ width: 250 }}
disabled={!copied}
>
I saved my mnemonic
</Button>
<Button
onClick={() => {
resetState();
history.goBack();
}}
color="inherit"
>
Back
</Button>
</Stack>
);
};
@@ -0,0 +1,74 @@
import React, { useContext, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, FormControl, Stack } from '@mui/material';
import { useSnackbar } from 'notistack';
import { Subtitle, Title, PasswordStrength } from '../components';
import { PasswordInput } from '../components/textfields';
import { SignInContext } from '../context';
import { createPassword } from '../../../requests';
export const CreatePassword = () => {
const { password, setPassword, resetState } = useContext(SignInContext);
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
const [isStrongPassword, setIsStrongPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { mnemonic } = useContext(SignInContext);
const history = useHistory();
const handleSkip = () => {
setPassword('');
history.push('/sign-in-mnemonic');
};
const { enqueueSnackbar } = useSnackbar();
const storePassword = async () => {
try {
setIsLoading(true);
await createPassword({ mnemonic, password });
enqueueSnackbar('Password successfully created', { variant: 'success' });
resetState();
history.push('/sign-in-password');
} catch (e) {
setIsLoading(false);
enqueueSnackbar(e as string, { variant: 'error' });
}
};
return (
<Stack spacing={3} alignItems="center" minWidth="50%">
<Title title="Create optional password" />
<Subtitle subtitle="Password should be min 8 characters, at least one number and one symbol" />
<FormControl fullWidth>
<Stack spacing={2}>
<>
<PasswordInput
password={password}
onUpdatePassword={(pswd) => setPassword(pswd)}
label="Password"
autoFocus
/>
<PasswordStrength password={password} onChange={(isStrong) => setIsStrongPassword(isStrong)} />
</>
<PasswordInput
password={confirmedPassword}
onUpdatePassword={(pswd) => setConfirmedPassword(pswd)}
label="Confirm password"
/>
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0 || !isStrongPassword || isLoading}
onClick={storePassword}
>
Next
</Button>
<Button size="large" color="info" onClick={handleSkip}>
Skip and sign in with mnemonic
</Button>
</Stack>
</FormControl>
</Stack>
);
};
@@ -0,0 +1,32 @@
/* eslint-disable react/no-unused-prop-types */
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Box, Button, Stack, Typography } from '@mui/material';
import { SubtitleSlick, Title } from '../components';
export const ExistingAccount = () => {
const history = useHistory();
return (
<>
<Title title="Welcome to Nym" />
<SubtitleSlick subtitle="NEXT GENERATION OF PRIVACY" />
<Stack spacing={2} sx={{ width: 300 }}>
<Button variant="contained" size="large" onClick={() => history.push('/sign-in-mnemonic')} fullWidth>
Sign in with mnemonic
</Button>
<Typography sx={{ textAlign: 'center', fontWeight: 600 }}>or</Typography>
<Button variant="contained" size="large" fullWidth onClick={() => history.push('/sign-in-password')}>
Sign in with password
</Button>
<Box display="flex" justifyContent="space-between">
<Button color="inherit" onClick={() => history.goBack()}>
Back
</Button>
<Button color="info" onClick={() => history.push('/confirm-mnemonic')}>
Create a password
</Button>
</Box>
</Stack>
</>
);
};
@@ -0,0 +1,8 @@
export * from './welcome';
export * from './create-mnemonic';
export * from './verify-mnemonic';
export * from './create-password';
export * from './existing-account';
export * from './signin-mnemonic';
export * from './signin-password';
export * from './connect-password';
@@ -0,0 +1,50 @@
import React, { useContext, useState } from 'react';
import { Box, Button, FormControl, LinearProgress, Stack } from '@mui/material';
import { useHistory } from 'react-router-dom';
import { MnemonicInput, Subtitle } from '../components';
import { ClientContext } from '../../../context/main';
export const SignInMnemonic = () => {
const [mnemonic, setMnemonic] = useState('');
const { setError, logIn, error, isLoading } = useContext(ClientContext);
const history = useHistory();
if (isLoading)
return (
<Box width="25%">
<LinearProgress variant="indeterminate" />
</Box>
);
return (
<Stack spacing={2} alignItems="center" minWidth="50%">
<Subtitle subtitle="Enter a mnemonic to sign in" />
<FormControl fullWidth>
<Stack spacing={2}>
<MnemonicInput mnemonic={mnemonic} onUpdateMnemonic={(mnc) => setMnemonic(mnc)} error={error} />
<Button
variant="contained"
size="large"
fullWidth
onClick={() => logIn({ type: 'mnemonic', value: mnemonic })}
>
Sign in with mnemonic
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={() => {
setError(undefined);
history.push('/existing-account');
}}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
</FormControl>
</Stack>
);
};
@@ -0,0 +1,56 @@
import React, { useContext, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, LinearProgress, FormControl, Stack, Box } from '@mui/material';
import { PasswordInput, Subtitle } from '../components';
import { ClientContext } from '../../../context/main';
export const SignInPassword = () => {
const [password, setPassword] = useState('');
const { setError, logIn, error, isLoading } = useContext(ClientContext);
const history = useHistory();
if (isLoading)
return (
<Box width="25%">
<LinearProgress variant="indeterminate" />
</Box>
);
return (
<Stack spacing={2} alignItems="center" minWidth="50%">
<Subtitle subtitle="Enter a password to sign in" />
<FormControl fullWidth>
<Stack spacing={2}>
<PasswordInput
label="Enter password"
password={password}
onUpdatePassword={(pswd) => setPassword(pswd)}
error={error}
autoFocus
/>
<Button
variant="contained"
size="large"
fullWidth
onClick={() => logIn({ type: 'password', value: password })}
>
Sign in with password
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={() => {
setError(undefined);
history.push('/existing-account');
}}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
</FormControl>
</Stack>
);
};
@@ -1,22 +1,21 @@
import React, { useEffect, useState } from 'react';
import { Button } from '@mui/material';
import React, { useContext, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, Stack } from '@mui/material';
import { HiddenWords, Subtitle, Title, WordTiles } from '../components';
import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWord, TMnemonicWords } from '../types';
import { randomNumberBetween } from '../../../utils';
import { SignInContext } from '../context';
const numberOfRandomWords = 4;
const numberOfRandomWords = 6;
export const VerifyMnemonic = ({
mnemonicWords,
onComplete,
}: {
mnemonicWords?: TMnemonicWords;
onComplete: () => void;
}) => {
export const VerifyMnemonic = () => {
const [randomWords, setRandomWords] = useState<TMnemonicWords>();
const [hiddenRandomWords, setHiddenRandomWords] = useState<THiddenMnemonicWords>();
const [currentSelection, setCurrentSelection] = useState(0);
const { mnemonicWords } = useContext(SignInContext);
const history = useHistory();
useEffect(() => {
if (mnemonicWords) {
const newRandomWords = getRandomEntriesFromArray<TMnemonicWord>(mnemonicWords, numberOfRandomWords);
@@ -48,16 +47,19 @@ export const VerifyMnemonic = ({
<WordTiles
mnemonicWords={randomWords}
onClick={currentSelection !== numberOfRandomWords ? revealWord : undefined}
buttons
/>
<Button
variant="contained"
sx={{ width: 300 }}
size="large"
disabled={currentSelection !== numberOfRandomWords}
onClick={onComplete}
>
Next
</Button>
<Stack spacing={3} sx={{ width: 300 }}>
<Button
variant="contained"
fullWidth
size="large"
disabled={currentSelection !== numberOfRandomWords}
onClick={() => history.push('/create-password')}
>
Next
</Button>
</Stack>
</>
);
}
@@ -0,0 +1,36 @@
/* eslint-disable react/no-unused-prop-types */
import React from 'react';
import { Button, Stack } from '@mui/material';
import { useHistory } from 'react-router-dom';
import { SubtitleSlick, Title } from '../components';
export const WelcomeContent: React.FC<{}> = () => {
const history = useHistory();
return (
<>
<Title title="Welcome to NYM" />
<SubtitleSlick subtitle="Next generation of privacy" />
<Stack spacing={3} minWidth={300}>
<Button
fullWidth
color="primary"
variant="contained"
size="large"
onClick={() => history.push('/existing-account')}
>
Sign in
</Button>
<Button
fullWidth
color="inherit"
disableElevation
size="large"
onClick={() => history.push('/create-mnemonic')}
>
Create account
</Button>
</Stack>
</>
);
};
@@ -1,11 +1,13 @@
export type TPages =
| 'welcome'
| 'create account'
| 'create mnemonic'
| 'verify mnemonic'
| 'create password'
| 'existing account'
| 'select network'
| 'legacy create account';
| 'legacy create account'
| 'sign in with mnemonic'
| 'sign in with password';
export type TMnemonicWord = {
name: string;
@@ -17,3 +19,5 @@ export type TMnemonicWords = TMnemonicWord[];
export type THiddenMnemonicWord = { hidden: boolean } & TMnemonicWord;
export type THiddenMnemonicWords = THiddenMnemonicWord[];
export type TLoginType = 'mnemonic' | 'password';
+18 -6
View File
@@ -1,9 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
import { Alert, Box, Button, CircularProgress } from '@mui/material';
import { useSnackbar } from 'notistack';
import { Fee, NymCard } from '../../components';
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
import { ClientContext } from '../../context/main';
import { unbond } from '../../requests';
import { unbond, vestingUnbond } from '../../requests';
import { PageLayout } from '../../layouts';
export const Unbond = () => {
@@ -11,6 +12,8 @@ export const Unbond = () => {
const { checkOwnership, ownership } = useCheckOwnership();
const { userBalance, getBondDetails } = useContext(ClientContext);
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
const initialiseForm = async () => {
await checkOwnership();
@@ -32,11 +35,20 @@ export const Unbond = () => {
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
await unbond(ownership.nodeType!);
await userBalance.fetchBalance();
await getBondDetails();
await checkOwnership();
setIsLoading(false);
try {
if (ownership.vestingPledge) {
await vestingUnbond(ownership.nodeType!);
} else {
await unbond(ownership.nodeType!);
}
} catch (e) {
enqueueSnackbar(`Failed to unbond ${ownership.nodeType}}`, { variant: 'error' });
} finally {
await getBondDetails();
await checkOwnership();
await userBalance.fetchBalance();
setIsLoading(false);
}
}}
color="inherit"
>
@@ -1,56 +0,0 @@
/* eslint-disable no-unused-vars */
import React, { useEffect, useState } from 'react';
import { Alert, Button, Card, CardActions, CardContent, CardHeader, Stack, Typography } from '@mui/material';
import { createMnemonic } from '../../requests';
import { CopyToClipboard } from '../../components';
import { TPages } from './types';
export const CreateAccountContent: React.FC<{ page: TPages; showSignIn: () => void }> = ({ page, showSignIn }) => {
const [mnemonic, setMnemonic] = useState<string>();
const [error, setError] = useState<Error>();
const handleCreateMnemonic = async () => {
setError(undefined);
try {
const newMnemonic = await createMnemonic();
setMnemonic(newMnemonic);
} catch (e: any) {
setError(e);
}
};
useEffect(() => {
handleCreateMnemonic();
}, []);
return (
<Stack spacing={4} alignItems="center" sx={{ width: 700 }} id={page}>
<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 will 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">
{mnemonic}
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<CopyToClipboard text={mnemonic || ''} light />
</CardActions>
</Card>
{error && (
<Alert severity="error" variant="outlined">
{error}
</Alert>
)}
<Button variant="contained" onClick={showSignIn} data-testid="sign-in-button" size="large" sx={{ width: 360 }}>
Sign in
</Button>
</Stack>
);
};
@@ -1,66 +0,0 @@
import React, { useContext, useState } from 'react';
import { NymLogo } from '@nymproject/react';
import { Alert, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
import { ClientContext } from '../../context/main';
export const SignInContent: React.FC = () => {
const [mnemonic] = 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 {
await logIn(mnemonic || '');
setIsLoading(false);
} catch (error: any) {
setIsLoading(false);
setInputError(error);
}
};
return (
<Stack spacing={3} alignItems="center" sx={{ width: '80%' }}>
<NymLogo width={50} />
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>Welcome to NYM</Typography>
<Typography variant="caption" sx={{ color: 'grey.800', textTransform: 'uppercase', letterSpacing: 4 }}>
Next generation of privacy
</Typography>
<Grid container direction="column" spacing={2}>
<Grid item>
<Button
fullWidth
variant="contained"
color="primary"
disabled={isLoading}
endIcon={isLoading && <CircularProgress size={20} />}
disableElevation
size="large"
onClick={handleSignIn}
type="submit"
>
Create Account
</Button>
</Grid>
<Grid item>
<Button fullWidth variant="outlined" size="large">
Use Existing Account
</Button>
</Grid>
{inputError && (
<Grid item sx={{ mt: 1 }}>
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light' }}>
{inputError}
</Alert>
</Grid>
)}
</Grid>
</Stack>
);
};
-81
View File
@@ -1,81 +0,0 @@
import React, { useContext, useState } from 'react';
import { NymLogo } from '@nymproject/react';
import { CircularProgress, Stack, Box } from '@mui/material';
import { ExistingAccount, WelcomeContent } from './pages';
import { TPages } from './types';
import { RenderPage } from './components';
import { CreateAccountContent } from './_legacy_create-account';
import { ClientContext } from '../../context/main';
// const testMnemonic =
// 'futuristic big receptive caption saw hug odd spoon internal dime bike rake helpless left distribution gusty eyes beg enormous word influence trashy pets curl';
//
// const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
// mnemonic
// .split(' ')
// .reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords);
export const Welcome = () => {
const [page, setPage] = useState<TPages>('welcome');
// const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>();
const { isLoading } = useContext(ClientContext);
// useEffect(() => {
// const mnemonicArray = mnemonicToArray(testMnemonic)
// setMnemonicWords(mnemonicArray)
// }, [])
return (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'auto',
bgcolor: 'nym.background.dark',
}}
>
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
{isLoading ? (
<CircularProgress size={72} />
) : (
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
<NymLogo width={75} />
<RenderPage page={page}>
<WelcomeContent
onUseExisting={() => setPage('existing account')}
onCreateAccountComplete={() => setPage('legacy create account')}
page="welcome"
/>
<CreateAccountContent page="legacy create account" showSignIn={() => setPage('existing account')} />
{/* <MnemonicWords
mnemonicWords={mnemonicWords}
onNext={() => setPage('verify mnemonic')}
onPrev={() => setPage('welcome')}
page="create account"
/>
<VerifyMnemonic
mnemonicWords={mnemonicWords}
onComplete={() => setPage('create password')}
page="verify mnemonic"
/>
<CreatePassword page="create password" /> */}
<ExistingAccount onPrev={() => setPage('welcome')} page="existing account" />
</RenderPage>
</Stack>
)}
</Box>
</Box>
);
};
@@ -1,60 +0,0 @@
import React, { useState } from 'react';
import { Button, FormControl, Grid, IconButton, Stack, TextField } from '@mui/material';
import { VisibilityOff, Visibility } from '@mui/icons-material';
import { Subtitle, Title, PasswordStrength } from '../components';
export const CreatePassword = () => {
const [password, setPassword] = useState<string>('');
const [confirmedPassword, setConfirmedPassword] = useState<string>();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmedPassword, setShowConfirmedPassword] = useState(false);
return (
<>
<Title title="Create password" />
<Subtitle subtitle="Create a strong password. Min 8 characters, at least one capital letter, number and special symbol" />
<Grid container justifyContent="center">
<Grid item xs={6}>
<FormControl fullWidth>
<Stack spacing={2}>
<TextField
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPassword ? 'input' : 'password'}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowPassword((show) => !show)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
<PasswordStrength password={password} />
<TextField
label="Confirm password"
value={confirmedPassword}
onChange={(e) => setConfirmedPassword(e.target.value)}
type={showConfirmedPassword ? 'input' : 'password'}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowConfirmedPassword((show) => !show)}>
{showConfirmedPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0}
>
Next
</Button>
</Stack>
</FormControl>
</Grid>
</Grid>
</>
);
};
@@ -1,55 +0,0 @@
/* eslint-disable react/no-unused-prop-types */
import React, { useContext, useState } from 'react';
import { Alert, Button, Stack, TextField } from '@mui/material';
import { Subtitle } from '../components';
import { ClientContext } from '../../../context/main';
import { TPages } from '../types';
export const ExistingAccount: React.FC<{ page: TPages; onPrev: () => void }> = ({ onPrev }) => {
const [mnemonic, setMnemonic] = useState<string>('');
const { logIn, error } = useContext(ClientContext);
const handleSignIn = async () => {
await logIn(mnemonic);
};
const handleSignInOnEnter = ({ key }: React.KeyboardEvent<HTMLDivElement>) => {
if (key.toLowerCase() === 'enter') {
logIn(mnemonic);
}
};
return (
<Stack spacing={2} sx={{ width: 400 }} alignItems="center">
<Subtitle subtitle="Enter your mnemonic from existing wallet" />
<TextField
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
multiline
rows={5}
fullWidth
onKeyDown={handleSignInOnEnter}
/>
{error && (
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light', width: '100%' }}>
{error}
</Alert>
)}
<Button variant="contained" size="large" fullWidth onClick={handleSignIn}>
Sign in
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={onPrev}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
);
};
@@ -1,5 +0,0 @@
export * from './welcome-content';
export * from './mnemonic-words';
export * from './verify-mnemonic';
export * from './create-password';
export * from './existing-account';
@@ -1,34 +0,0 @@
import React from 'react';
import { Alert, Button, Typography } from '@mui/material';
import { WordTiles } from '../components';
import { TMnemonicWords } from '../types';
export const MnemonicWords = ({
mnemonicWords,
onNext,
onPrev,
}: {
mnemonicWords?: TMnemonicWords;
onNext: () => void;
onPrev: () => void;
}) => (
<>
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>Write down your mnemonic</Typography>
<Alert icon={false} severity="info" sx={{ bgcolor: '#18263B', color: '#50ABFF', width: 625 }}>
Please store your mnemonic in a safe place. This is the only way to access your wallet!
</Alert>
<WordTiles mnemonicWords={mnemonicWords} showIndex />
<Button variant="contained" color="primary" disableElevation size="large" onClick={onNext} sx={{ width: 250 }}>
Verify mnemonic
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={onPrev}
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' }, width: 250 }}
>
Back
</Button>
</>
);
@@ -1,42 +0,0 @@
/* eslint-disable react/no-unused-prop-types */
import React from 'react';
import { Button, Stack } from '@mui/material';
import { SubtitleSlick, Title } from '../components';
import { TPages } from '../types';
export const WelcomeContent: React.FC<{
page: TPages;
onUseExisting: () => void;
onCreateAccountComplete: () => void;
}> = ({ onUseExisting, onCreateAccountComplete }) => (
<>
<Title title="Welcome to NYM" />
<SubtitleSlick subtitle="Next generation of privacy" />
<Stack spacing={3} sx={{ width: 300 }}>
<Button
fullWidth
variant="contained"
color="primary"
disableElevation
size="large"
onClick={onCreateAccountComplete}
>
Create Account
</Button>
<Button
fullWidth
variant="outlined"
size="large"
sx={{
color: 'common.white',
border: '1px solid white',
'&:hover': { border: '1px solid white', '&:hover': { background: 'none' } },
}}
onClick={onUseExisting}
disableRipple
>
Sign in
</Button>
</Stack>
</>
);
+14 -8
View File
@@ -1,14 +1,10 @@
import { invoke } from '@tauri-apps/api';
import { Account, TCreateAccount } from '../types';
import { Account } from '../types';
export const createAccount = async (): Promise<TCreateAccount> => {
const res: TCreateAccount = await invoke('create_new_account');
return res;
};
export const createMnemonic = async (): Promise<string> => invoke('create_new_mnemonic');
export const createMnemonic = async (): Promise<string> => {
const res: string = await invoke('create_new_mnemonic');
return res;
export const createPassword = async ({ mnemonic, password }: { mnemonic: string; password: string }): Promise<void> => {
await invoke('create_password', { mnemonic, password });
};
export const signInWithMnemonic = async (mnemonic: string): Promise<Account> => {
@@ -16,6 +12,16 @@ export const signInWithMnemonic = async (mnemonic: string): Promise<Account> =>
return res;
};
export const validateMnemonic = async (mnemonic: string): Promise<boolean> => {
const res: boolean = await invoke('validate_mnemonic', { mnemonic });
return res;
};
export const signInWithPassword = async (password: string): Promise<Account> => {
const res: Account = await invoke('sign_in_with_password', { password });
return res;
};
export const signOut = async (): Promise<void> => {
await invoke('logout');
};
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { Bond, Balance, Delegate, InternalDocs, Receive, Send, Unbond, Undelegate } from '../pages';
export const AppRoutes = () => (
<Switch>
<Route path="/balance">
<Balance />
</Route>
<Route path="/send">
<Send />
</Route>
<Route path="/receive">
<Receive />
</Route>
<Route path="/bond">
<Bond />
</Route>
<Route path="/unbond">
<Unbond />
</Route>
<Route path="/delegate">
<Delegate />
</Route>
<Route path="/undelegate">
<Undelegate />
</Route>
<Route path="/docs">
<InternalDocs />
</Route>
</Switch>
);
+2 -33
View File
@@ -1,33 +1,2 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { Balance } from '../pages/balance';
import { Bond, Delegate, InternalDocs, Receive, Send, Unbond, Undelegate } from '../pages';
export const Routes = () => (
<Switch>
<Route path="/balance">
<Balance />
</Route>
<Route path="/send">
<Send />
</Route>
<Route path="/receive">
<Receive />
</Route>
<Route path="/bond">
<Bond />
</Route>
<Route path="/unbond">
<Unbond />
</Route>
<Route path="/delegate">
<Delegate />
</Route>
<Route path="/undelegate">
<Undelegate />
</Route>
<Route path="/docs">
<InternalDocs />
</Route>
</Switch>
);
export * from './app';
export * from './sign-in';
+48
View File
@@ -0,0 +1,48 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { PageLayout } from 'src/pages/sign-in/components';
import {
CreateMnemonic,
CreatePassword,
ExistingAccount,
SignInMnemonic,
SignInPassword,
VerifyMnemonic,
WelcomeContent,
ConnectPassword,
} from 'src/pages/sign-in/pages';
import { ConfirmMnemonic } from 'src/pages/sign-in/pages/confirm-mnemonic';
export const SignInRoutes = () => (
<PageLayout>
<Switch>
<Route path="/welcome">
<WelcomeContent />
</Route>
<Route path="/existing-account">
<ExistingAccount />
</Route>
<Route path="/create-mnemonic">
<CreateMnemonic />
</Route>
<Route path="/verify-mnemonic">
<VerifyMnemonic />
</Route>
<Route path="/create-password">
<CreatePassword />
</Route>
<Route path="/sign-in-mnemonic">
<SignInMnemonic />
</Route>
<Route path="/sign-in-password">
<SignInPassword />
</Route>
<Route path="/confirm-mnemonic">
<ConfirmMnemonic />
</Route>
<Route path="/connect-password">
<ConnectPassword />
</Route>
</Switch>
</PageLayout>
);
+1
View File
@@ -8,6 +8,7 @@ import { NymWalletThemeWithMode } from './NymWalletTheme';
/**
* 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);
return <NymWalletThemeWithMode mode={mode}>{children}</NymWalletThemeWithMode>;