diff --git a/custom.d.ts b/custom.d.ts index 760bfd53..eb73ea37 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -1,12 +1,14 @@ declare module '*.svg' { - import React = require('react'); - export const ReactComponent: React.FC>; - const src: string; - export default src; + import React = require('react') + export const ReactComponent: React.FC> + const src: string + export default src } -declare module '*.png'; +declare module '*.png' -declare module 'rodal'; +declare module 'rodal' -declare module 'svg-identicon' \ No newline at end of file +declare module 'svg-identicon' + +declare module 'crypto-js' diff --git a/package.json b/package.json index 5d7eef7d..79ab05b2 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,9 @@ "axios": "^0.27.2", "bootstrap-icons": "^1.9.1", "chart.js": "^4.2.0", - "dotenv": "^16.0.3", "concurrently": "5.2.0", + "crypto-js": "^4.1.1", + "dotenv": "^16.0.3", "electron-is-dev": "^2.0.0", "electron-squirrel-startup": "^1.0.0", "eslint-import-resolver-typescript": "^3.5.1", diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 095e17bc..78d5ccd0 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -54,7 +54,7 @@ const Button: FC = ({ case ButtonFace.TERTIARY: return 'border border-primary text-primary' case ButtonFace.SECONDARY: - return 'bg-primary text-white' + return 'bg-primary text-white disabled:cursor-default disabled:opacity-30' case ButtonFace.WHITE: return 'bg-transparent disabled:border-dark300 disabled:text-dark300 disabled:cursor-default border border-white text-white' default: diff --git a/src/components/CurrencySelect/CurrencySelect.spec.tsx b/src/components/CurrencySelect/CurrencySelect.spec.tsx index dcbdb3bd..b493274c 100644 --- a/src/components/CurrencySelect/CurrencySelect.spec.tsx +++ b/src/components/CurrencySelect/CurrencySelect.spec.tsx @@ -3,13 +3,6 @@ import CurrencySelect from './CurrencySelect' import { mockedRecoilState, mockedRecoilValue } from '../../../test.helpers' import { mockCurrencies } from '../../mocks/currencyResults' -jest.mock('recoil', () => ({ - useRecoilValue: jest.fn(), - useRecoilState: jest.fn(() => ['mock-value', jest.fn()]), - atom: jest.fn(), - selector: jest.fn(), -})) - const mockSetState = jest.fn() describe('Currency select component', () => { diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index cb5d4f0f..9eed02f5 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -1,4 +1,11 @@ -import { FC, HTMLInputTypeAttribute, InputHTMLAttributes, useState } from 'react' +import { + FC, + HTMLInputTypeAttribute, + InputHTMLAttributes, + useState, + ClipboardEvent, + ChangeEvent, +} from 'react' import Typography from '../Typography/Typography' import { UiMode } from '../../constants/enums' import Tooltip from '../ToolTip/Tooltip' @@ -12,6 +19,8 @@ export interface InputProps extends InputHTMLAttributes { toolTipMaxWidth?: number className?: string uiMode?: UiMode + isDisableToggle?: boolean + isDisablePaste?: boolean inputStyle?: 'primary' | 'secondary' icon?: string } @@ -27,11 +36,25 @@ const Input: FC = ({ toolTipId, toolTipMode, toolTipMaxWidth = 250, + isDisableToggle, + isDisablePaste, icon, + onChange, ...props }) => { const [inputType, setType] = useState(type || 'text') const isPasswordType = type === 'password' + const sanitizeInput = (e: ChangeEvent) => { + return { + ...e, + target: { + ...e.target, + value: e.target.value + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/onerror\s*=\s*["'][^"']*["']/gi, ''), + }, + } + } const generateInputStyle = () => { switch (inputStyle) { @@ -40,9 +63,15 @@ const Input: FC = ({ default: return `${ uiMode === UiMode.LIGHT - ? 'text-dark500 bg-dark10 border-dark500' - : 'bg-transparent border-white placeholder:text-dark500' - } font-light text-white border-b text-body md:text-subtitle1` + ? 'text-dark500 bg-dark10 border-dark500 px-2' + : 'bg-transparent text-white border-white placeholder:text-dark500' + } font-light border-b text-body md:text-subtitle1` + } + } + + const handlePaste = (e: ClipboardEvent) => { + if (isDisablePaste) { + e.preventDefault() } } @@ -75,12 +104,14 @@ const Input: FC = ({
onChange?.(sanitizeInput(value))} + onPaste={handlePaste} type={inputType} className={`${isPasswordType || icon ? 'pr-5' : ''} ${ className ? className : '' } pb-2 w-full font-openSauce outline-none ${generateInputStyle()}`} /> - {isPasswordType ? ( + {isPasswordType && !isDisableToggle ? ( + setValue: UseFormSetValue + getValues: UseFormGetValues +} export default { key: 'Protocol Input', component: ProtocolInput, } +const MockConnectionForm: FC<{ children: (props: RenderProps) => React.ReactElement }> = ({ + children, +}) => { + const { control, setValue, getValues } = useForm() + + return ( +
+ {children && + children({ + control, + setValue, + getValues, + })} +
+ ) +} + const Template: Story = ({ type, id, isValid }) => ( - - {({ control, setValue, getValues }) => ( + + {({ control, getValues, setValue }) => (
= ({ type, id, isValid }) => (
)} -
+ ) export const Basic = Template.bind({}) diff --git a/src/components/SelectDropDown/SelectDropDown.tsx b/src/components/SelectDropDown/SelectDropDown.tsx index 071c927c..99e51c5d 100644 --- a/src/components/SelectDropDown/SelectDropDown.tsx +++ b/src/components/SelectDropDown/SelectDropDown.tsx @@ -3,6 +3,7 @@ import useClickOutside from '../../hooks/useClickOutside' import Typography, { TypographyColor } from '../Typography/Typography' import DropDown from '../DropDown/DropDown' import { useTranslation } from 'react-i18next' +import addClassString from '../../utilities/addClassString' export type OptionType = string | number @@ -13,6 +14,7 @@ export type SelectOption = { export interface SelectDropDownProps { options: SelectOption[] + className?: string label?: string placeholder?: string value?: OptionType @@ -26,6 +28,7 @@ const SelectDropDown: FC = ({ onSelect, value, color, + className, label, isFilter, placeholder, @@ -33,6 +36,7 @@ const SelectDropDown: FC = ({ const { t } = useTranslation() const [query, setQuery] = useState('') const [isOpen, toggle] = useState(false) + const classes = addClassString('w-36', [className]) const toggleDropdown = () => toggle((prevState) => !prevState) const makeSelection = (selection: OptionType) => { @@ -60,7 +64,7 @@ const SelectDropDown: FC = ({ }, [query, options]) return ( -
+
{label && ( {label} diff --git a/src/components/SessionAuthModal/SessionAuthModal.tsx b/src/components/SessionAuthModal/SessionAuthModal.tsx new file mode 100644 index 00000000..389c49b9 --- /dev/null +++ b/src/components/SessionAuthModal/SessionAuthModal.tsx @@ -0,0 +1,158 @@ +import RodalModal from '../RodalModal/RodalModal' +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' +import { apiToken, appView, onBoardView, sessionAuthErrorCount } from '../../recoil/atoms' +import Typography from '../Typography/Typography' +import useLocalStorage from '../../hooks/useLocalStorage' +import { ChangeEvent, FC, ReactElement, useEffect, useState } from 'react' +import addClassString from '../../utilities/addClassString' +import { AppView, OnboardView, UiMode } from '../../constants/enums' +import CryptoJS from 'crypto-js' +import Button, { ButtonFace } from '../Button/Button' +import Input from '../Input/Input' +import { MAX_SESSION_UNLOCK_ATTEMPTS } from '../../constants/constants' +import { useTranslation } from 'react-i18next' + +export interface SessionAuthModalProps { + onSuccess: (token?: string) => void + onFail?: () => void + isOpen: boolean + onClose?: () => void + children?: ReactElement | ReactElement[] + mode?: UiMode +} + +const SessionAuthModal: FC = ({ + onSuccess, + children, + isOpen, + onFail, + onClose, + mode, +}) => { + const { t } = useTranslation() + const [localStorageApiToken, storeApiToken] = useLocalStorage('api-token', '') + const memoryApiToken = useRecoilValue(apiToken) + const [password, setPassword] = useState('') + const [errorCount, setCount] = useRecoilState(sessionAuthErrorCount) + const [isError, setIsError] = useState(false) + const setView = useSetRecoilState(onBoardView) + const setAppView = useSetRecoilState(appView) + + useEffect(() => { + if (errorCount >= MAX_SESSION_UNLOCK_ATTEMPTS) { + onFail?.() + } + }, [errorCount]) + + const classes = addClassString('', [isError && 'animate-shake']) + + const setInput = (event: ChangeEvent) => setPassword(event.target.value) + + const viewConfig = () => { + storeApiToken('') + setView(OnboardView.CONFIGURE) + setAppView(AppView.ONBOARD) + } + + const playErrorAnim = () => { + setIsError(true) + + setTimeout(() => { + setIsError(false) + }, 1000) + } + + const handleError = () => { + playErrorAnim() + setCount((prevState) => prevState + 1) + } + + const confirmApiToken = () => { + if (password !== memoryApiToken) { + handleError() + return + } + + setCount(0) + onSuccess(memoryApiToken) + } + + const confirmPassword = () => { + const pattern = /^api-token-0x\w*$/ + try { + const token = CryptoJS.AES.decrypt(localStorageApiToken, password).toString(CryptoJS.enc.Utf8) + if (!token.length || !pattern.test(token)) { + handleError() + return + } + setCount(0) + onSuccess(token) + } catch (e) { + handleError() + } + } + + const authenticateAction = () => { + if (localStorageApiToken) { + confirmPassword() + return + } + + confirmApiToken() + } + + const renderNoPasswordRedirect = () => ( + <> + {t('sessionAuthModal.failedResponse')} +
+ +
+ + ) + const renderPasswordInput = () => ( + <> + {t('sessionAuthModal.passwordPrompt')} + +
+ +
+ + ) + + return ( + <> + +
+
+ + {t('sessionAuthModal.title')} + +
+
+ {errorCount < MAX_SESSION_UNLOCK_ATTEMPTS + ? renderPasswordInput() + : renderNoPasswordRedirect()} +
+
+
+ {children} + + ) +} + +export default SessionAuthModal diff --git a/src/components/ValidatorModal/ValidatorCardAction.tsx b/src/components/ValidatorModal/ValidatorCardAction.tsx index 5e4c17c4..fe3fbbef 100644 --- a/src/components/ValidatorModal/ValidatorCardAction.tsx +++ b/src/components/ValidatorModal/ValidatorCardAction.tsx @@ -12,7 +12,10 @@ export interface ValidatorCardActionProps { const ValidatorCardAction: FC = ({ icon, title, onClick, className }) => { return ( -
+
diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 543b0e89..8016a222 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -231,3 +231,4 @@ export const REQUIRED_VALIDATOR_VERSION = { } export const DEFAULT_MAX_NETWORK_ERROR = 3 +export const MAX_SESSION_UNLOCK_ATTEMPTS = 3 diff --git a/src/constants/enums.ts b/src/constants/enums.ts index 3c76ad60..31ebdd37 100644 --- a/src/constants/enums.ts +++ b/src/constants/enums.ts @@ -19,6 +19,7 @@ export enum AppView { export enum OnboardView { PROVIDER = 'PROVIDER', CONFIGURE = 'CONFIGURE', + SESSION = 'SESSION', SETUP = 'SETUP', } diff --git a/src/forms/ConfigConnectionForm.tsx b/src/forms/ConfigConnectionForm.tsx index 02411656..7d92749f 100644 --- a/src/forms/ConfigConnectionForm.tsx +++ b/src/forms/ConfigConnectionForm.tsx @@ -71,15 +71,13 @@ const ConfigConnectionForm: FC = ({ children }) => { const [isVersionError, setVersionError] = useState(false) const [storedBnNode, storeBeaconNode] = useLocalStorage('beaconNode', undefined) - const [storedToken, storeApiToken] = useLocalStorage('api-token', '') const [storedVc, storeValidatorClient] = useLocalStorage( 'validatorClient', undefined, ) const [storedName, storeUserName] = useLocalStorage('username', '') - const hasCache = - Boolean(storedBnNode) && Boolean(storedVc) && Boolean(storedToken) && Boolean(storedName) + const hasCache = Boolean(storedBnNode) && Boolean(storedVc) && Boolean(storedName) const endPointDefault = { protocol: Protocol.HTTP, @@ -96,7 +94,7 @@ const ConfigConnectionForm: FC = ({ children }) => { defaultValues: { beaconNode: storedBnNode || endPointDefault, validatorClient: storedVc || vcDefaultEndpoint, - apiToken: storedToken, + apiToken: '', deviceName: '', userName: storedName, isRemember: hasCache, @@ -223,7 +221,6 @@ const ConfigConnectionForm: FC = ({ children }) => { if (isRemember) { storeBeaconNode(beaconNode) storeValidatorClient(validatorClient) - storeApiToken(apiToken) storeUserName(userName) } @@ -232,7 +229,7 @@ const ConfigConnectionForm: FC = ({ children }) => { setBeaconNode(beaconNode) setValidatorClient(validatorClient) setDashView(ContentView.MAIN) - setView(OnboardView.SETUP) + setView(OnboardView.SESSION) } catch (e) { if (!isValidBeaconNode || !isValidValidatorClient) { if (!isValidBeaconNode) { diff --git a/src/forms/SessionAuthForm.tsx b/src/forms/SessionAuthForm.tsx new file mode 100644 index 00000000..954f7dae --- /dev/null +++ b/src/forms/SessionAuthForm.tsx @@ -0,0 +1,83 @@ +import { FC, ReactElement } from 'react' +import { Control, useForm } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' +import { sessionAuthValidation } from '../validation/sessionAuthValidation' +import { OnboardView } from '../constants/enums' +import { useRecoilValue, useSetRecoilState } from 'recoil' +import { apiToken, onBoardView } from '../recoil/atoms' +import { toast } from 'react-toastify' +import useLocalStorage from '../hooks/useLocalStorage' +import CryptoJS from 'crypto-js' +import { useTranslation } from 'react-i18next' + +export interface SessionAuthForm { + password: string + password_confirmation: string +} + +export interface RenderProps { + control: Control + isLoading: boolean + onSubmit: () => void +} + +export interface SessionAuthFormProps { + children: (props: RenderProps) => ReactElement +} + +const SessionAuthForm: FC = ({ children }) => { + const { t } = useTranslation() + const setView = useSetRecoilState(onBoardView) + const viewSetup = () => setView(OnboardView.SETUP) + const token = useRecoilValue(apiToken) + const [, storeApiToken] = useLocalStorage('api-token', '') + + const { + control, + watch, + formState: { isValid }, + } = useForm({ + defaultValues: { + password: '', + password_confirmation: '', + }, + mode: 'onChange', + resolver: yupResolver(sessionAuthValidation), + }) + + const password = watch('password') + + const onSubmit = () => { + if (!password) { + viewSetup() + return + } + if (!isValid && password) { + toast.error(t('error.sessionAuth.invalidPassword') as string, { + position: 'top-right', + autoClose: 5000, + theme: 'colored', + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + }) + return + } + + storeApiToken(CryptoJS.AES.encrypt(token, password).toString()) + viewSetup() + } + + return ( +
+ {children && + children({ + control, + isLoading: false, + onSubmit, + })} +
+ ) +} + +export default SessionAuthForm diff --git a/src/global.css b/src/global.css index 9088f207..5c2948c4 100644 --- a/src/global.css +++ b/src/global.css @@ -134,5 +134,5 @@ input[type="number"]::-webkit-outer-spin-button { padding: 0!important; } .rodal { - margin-top: 0!important; + margin: 0!important; } \ No newline at end of file diff --git a/src/locales/translations/en-US.json b/src/locales/translations/en-US.json index 44a809e4..90d39a4f 100644 --- a/src/locales/translations/en-US.json +++ b/src/locales/translations/en-US.json @@ -24,9 +24,12 @@ "saturday": "Sat" } }, + "password": "Password", + "confirmPassword": "Confirm Password", "helloUser": "Hello {{user}},", "lighthouseVersion": "Lighthouse Version", "lighthouseUiVersion": "Lighthouse UI Version", + "sessionAuth": "Session Auth", "configure": "Configure", "continue": "Continue", "comingSoon": "Coming Soon", @@ -231,6 +234,16 @@ "vcPort": "VC Port" }, "error": { + "sessionAuth": { + "invalidPassword": "Invalid Session Password", + "length": "Password must be at least 12 characters", + "passwordMatch": "Confirmation must match password", + "confirmationRequired": "Password confirmation is required", + "lowercaseRequired": "Password must contain at least one lowercase letter", + "uppercaseRequired": "Password must contain at least one uppercase letter", + "numberRequired": "Password must contain at least one number", + "specialCharRequired": "Password must contain at least one special character" + }, "networkError": "{{type}} NODE NETWORK ERROR: Make sure your IP is correct and/or CORS is correctly configured. Use --http-allow-origin in {{type}} config", "unknownError": "Unknown {{type}} NODE error.", "validatorInfo": "Error Loading Validator Info...", @@ -325,5 +338,17 @@ "validatorClient": "Validator Client", "affectedNetworks": "Siren failed to maintain connection to the designated <0>{{network}}.", "reconfigureOrContact": "Please review and update configuration settings. If this issue persists please contact our team in <0>discord." + }, + "sessionConfiguration": { + "prevStep": "Configuration", + "currentStep": "Session Auth", + "title": "Session Authentication", + "description": "Protect your account with an optional session password. This extra layer of security ensures that only you have access to your account, even if someone gains access to your device. If no password is set, Siren will automatically default back to the api-token you set in your configuration settings." + }, + "sessionAuthModal": { + "title": "Session Authentication", + "passwordPrompt": "To ensure the safety of your account, password authentication is required to complete this action. Please confirm your password to proceed. Please be aware that you have a maximum of three attempts.", + "failedResponse": "Authentication failed. Please return to the configuration settings to re-enter your validator credentials.", + "configSettings": "Configuration Settings" } } \ No newline at end of file diff --git a/src/recoil/atoms.ts b/src/recoil/atoms.ts index c5b954b6..a473d1fc 100644 --- a/src/recoil/atoms.ts +++ b/src/recoil/atoms.ts @@ -159,3 +159,8 @@ export const validatorMetrics = atom({ key: 'validatorMetrics', default: undefined, }) + +export const sessionAuthErrorCount = atom({ + key: 'sessionAuthErrorCount', + default: 0, +}) diff --git a/src/validation/sessionAuthValidation.ts b/src/validation/sessionAuthValidation.ts new file mode 100644 index 00000000..a5bc9b02 --- /dev/null +++ b/src/validation/sessionAuthValidation.ts @@ -0,0 +1,16 @@ +import * as yup from 'yup' +import i18n from 'i18next' + +export const sessionAuthValidation = yup.object().shape({ + password: yup + .string() + .min(12, i18n.t('error.sessionAuth.length')) + .matches(/[a-z]/, i18n.t('error.sessionAuth.lowercaseRequired')) + .matches(/[A-Z]/, i18n.t('error.sessionAuth.uppercaseRequired')) + .matches(/[0-9]/, i18n.t('error.sessionAuth.numberRequired')) + .matches(/[$&+,:;=?@#|'<>.^*()%!-]/, i18n.t('error.sessionAuth.specialCharRequired')), + password_confirmation: yup + .string() + .oneOf([yup.ref('password'), null], i18n.t('error.sessionAuth.passwordMatch')) + .required(i18n.t('error.sessionAuth.confirmationRequired')), +}) diff --git a/src/views/DashBoard/Content/Settings.tsx b/src/views/DashBoard/Content/Settings.tsx index 3b566ffe..d31b88f5 100644 --- a/src/views/DashBoard/Content/Settings.tsx +++ b/src/views/DashBoard/Content/Settings.tsx @@ -31,12 +31,14 @@ const Settings = () => { const [username, setUsername] = useRecoilState(userName) const setView = useSetRecoilState(onBoardView) const setAppView = useSetRecoilState(appView) + const [, storeApiToken] = useLocalStorage('api-token', '') const [, storeUserName] = useLocalStorage('username', undefined) const svgClasses = addClassString('hidden md:block absolute top-14 right-10', [ mode === UiMode.DARK ? 'opacity-20' : 'opacity-40', ]) const viewConfig = () => { + storeApiToken('') setView(OnboardView.CONFIGURE) setAppView(AppView.ONBOARD) } diff --git a/src/views/DashBoard/Dashboard.tsx b/src/views/DashBoard/Dashboard.tsx index 4be8a98e..a2afd1fc 100644 --- a/src/views/DashBoard/Dashboard.tsx +++ b/src/views/DashBoard/Dashboard.tsx @@ -7,6 +7,7 @@ import { activeCurrency, beaconNetworkError, dashView, + sessionAuthErrorCount, uiMode, validatorNetworkError, } from '../../recoil/atoms' @@ -41,11 +42,13 @@ const Dashboard = () => { const [isReadySync, setSync] = useState(false) const isBeaconNetworkError = useSetRecoilState(beaconNetworkError) const isValidatorNetworkError = useSetRecoilState(validatorNetworkError) + const setSessionAuthErrorCount = useSetRecoilState(sessionAuthErrorCount) useEffect(() => { return () => { isBeaconNetworkError(false) isValidatorNetworkError(false) + setSessionAuthErrorCount(0) } }, []) diff --git a/src/views/InitScreen.tsx b/src/views/InitScreen.tsx index eb08390b..3f5180e9 100644 --- a/src/views/InitScreen.tsx +++ b/src/views/InitScreen.tsx @@ -1,25 +1,26 @@ import Typography from '../components/Typography/Typography' import useLocalStorage from '../hooks/useLocalStorage' import { Endpoint } from '../types' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useSetRecoilState } from 'recoil' import { - apiToken, appView, + userName, + onBoardView, beaconNodeEndpoint, beaconVersionData, - onBoardView, - userName, - validatorClientEndpoint, + apiToken, validatorVersionData, + validatorClientEndpoint, } from '../recoil/atoms' -import { AppView, OnboardView } from '../constants/enums' +import { AppView, OnboardView, UiMode } from '../constants/enums' import LoadingSpinner from '../components/LoadingSpinner/LoadingSpinner' import { fetchVersion } from '../api/lighthouse' import { fetchBeaconVersion, fetchSyncStatus } from '../api/beacon' import { useTranslation } from 'react-i18next' import { UsernameStorage } from '../types/storage' import AppDescription from '../components/AppDescription/AppDescription' +import SessionAuthModal from '../components/SessionAuthModal/SessionAuthModal' import isRequiredVersion from '../utilities/isRequiredVersion' import { REQUIRED_VALIDATOR_VERSION } from '../constants/constants' @@ -28,6 +29,7 @@ const InitScreen = () => { const [isReady, setReady] = useState(false) const [step, setStep] = useState(0) const setView = useSetRecoilState(appView) + const [isAuthModal, toggleAuthModal] = useState(false) const setOnboardView = useSetRecoilState(onBoardView) const setBeaconNode = useSetRecoilState(beaconNodeEndpoint) const setApiToken = useSetRecoilState(apiToken) @@ -37,7 +39,7 @@ const InitScreen = () => { const setValidatorClient = useSetRecoilState(validatorClientEndpoint) const [validatorClient] = useLocalStorage('validatorClient', undefined) const [beaconNode] = useLocalStorage('beaconNode', undefined) - const [token] = useLocalStorage('api-token', undefined) + const [encryptedToken] = useLocalStorage('api-token', undefined) const [username] = useLocalStorage('username', undefined) const moveToView = (view: AppView) => { @@ -45,7 +47,6 @@ const InitScreen = () => { setView(view) }, 1000) } - const moveToOnboard = () => moveToView(AppView.ONBOARD) const incrementStep = () => setStep((prev) => prev + 1) @@ -96,21 +97,31 @@ const InitScreen = () => { moveToOnboard() } } + const finishInit = useCallback( + (token?: string) => { + if (validatorClient && beaconNode && token) { + toggleAuthModal(false) + void setNodeInfo(validatorClient, beaconNode, token) + setReady(true) + } + }, + [validatorClient, beaconNode, encryptedToken], + ) useEffect(() => { if (isReady) return - if (!validatorClient || !beaconNode || !token || !username) { + if (!validatorClient || !beaconNode || !encryptedToken || !username) { moveToView(AppView.ONBOARD) return } setUserName(username) - void setNodeInfo(validatorClient, beaconNode, token) - setReady(true) - }, [validatorClient, beaconNode, token, isReady]) + toggleAuthModal(true) + }, [validatorClient, beaconNode, encryptedToken]) return (
+
diff --git a/src/views/Onboard/Onboard.tsx b/src/views/Onboard/Onboard.tsx index 801c10b3..498bc92e 100644 --- a/src/views/Onboard/Onboard.tsx +++ b/src/views/Onboard/Onboard.tsx @@ -4,6 +4,7 @@ import { OnboardView } from '../../constants/enums' import SelectProvider from './views/SelectProvider' import ConfigureConnection from './views/ConfigureConnection' import ValidatorSetup from './views/ValidatorSetup/ValidatorSetup' +import SessionConfig from './views/SessionConfig' const Onboard = () => { const view = useRecoilValue(onBoardView) @@ -14,6 +15,8 @@ const Onboard = () => { return case OnboardView.SETUP: return + case OnboardView.SESSION: + return default: return } diff --git a/src/views/Onboard/views/SessionConfig.tsx b/src/views/Onboard/views/SessionConfig.tsx new file mode 100644 index 00000000..9674453c --- /dev/null +++ b/src/views/Onboard/views/SessionConfig.tsx @@ -0,0 +1,70 @@ +import ValidatorSetupLayout from '../../../components/ValidatorSetupLayout/ValidatorSetupLayout' +import { OnboardView, UiMode } from '../../../constants/enums' +import { useSetRecoilState } from 'recoil' +import { onBoardView } from '../../../recoil/atoms' +import Typography from '../../../components/Typography/Typography' +import Input from '../../../components/Input/Input' +import SessionAuthForm from '../../../forms/SessionAuthForm' +import { Controller } from 'react-hook-form' +import { ButtonFace } from '../../../components/Button/Button' +import { useTranslation } from 'react-i18next' + +const SessionConfig = () => { + const { t } = useTranslation() + const setView = useSetRecoilState(onBoardView) + const viewConfig = () => setView(OnboardView.CONFIGURE) + + return ( + + {({ control, onSubmit }) => ( + +
+ {t('sessionConfiguration.description')} +
+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+
+ )} +
+ ) +} + +export default SessionConfig diff --git a/src/views/Onboard/views/ValidatorSetup/Steps/HealthCheck.tsx b/src/views/Onboard/views/ValidatorSetup/Steps/HealthCheck.tsx index 17cb851b..30e41c31 100644 --- a/src/views/Onboard/views/ValidatorSetup/Steps/HealthCheck.tsx +++ b/src/views/Onboard/views/ValidatorSetup/Steps/HealthCheck.tsx @@ -17,9 +17,13 @@ const HealthCheck = () => { const { t } = useTranslation() const setView = useSetRecoilState(onBoardView) const setStep = useSetRecoilState(setupStep) + const [, storeApiToken] = useLocalStorage('api-token', '') const [, setHealthChecked] = useLocalStorage('health-check', undefined) - const viewConfig = () => setView(OnboardView.CONFIGURE) + const viewSessionAuth = () => { + storeApiToken('') + setView(OnboardView.SESSION) + } const viewSync = () => { setHealthChecked(true) setStep(SetupSteps.SYNC) @@ -29,9 +33,9 @@ const HealthCheck = () => { return (