Merge branch 'main' into hn.UI-feedback-fixes

This commit is contained in:
Hristo Nedelkov 2023-10-10 10:04:11 +03:00
commit 01020dd26b
18 changed files with 377 additions and 212 deletions

View File

@ -17,19 +17,5 @@ export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
continueHandler: () => {},
activeStep: 0,
isConfirmPhraseStage: false,
subStepValidatorSetup: 0,
},
}
export const Disabled: Story = {
args: {
continueHandler: () => {},
activeStep: 0,
isConfirmPhraseStage: true,
subStepValidatorSetup: 0,
},
args: {},
}

View File

@ -1,84 +1,125 @@
import { Stack, YStack } from 'tamagui'
import { Stack, XStack } from 'tamagui'
import { Button, InformationBox } from '@status-im/components'
import { CloseCircleIcon } from '@status-im/icons'
import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { RootState } from '../../redux/store'
import LinkWithArrow from '../../components/General/LinkWithArrow'
import { setActiveStep, setSubStepValidatorSetup } from '../../redux/ValidatorOnboarding/slice'
import { KEYSTORE_FILES } from '../../constants'
import {
setIsConfirmPhraseStage,
setIsCopyPastedPhrase,
setValidWords,
} from '../../redux/ValidatorOnboarding/KeyGeneration/slice'
type ContinueButton = {
continueHandler: () => void
activeStep: number
isConfirmPhraseStage: boolean
subStepValidatorSetup: number
isValidatorSet?: boolean
}
const ContinueButton = ({
continueHandler,
activeStep,
isConfirmPhraseStage,
subStepValidatorSetup,
isValidatorSet,
}: ContinueButton) => {
const { isCopyPastedPhrase, words, validWords } = useSelector(
(state: RootState) => state.keyGeneration,
const ContinueButton = () => {
const [isDisabled, setIsDisabled] = useState(false)
const {
isCopyPastedPhrase,
mnemonic,
validWords,
isConfirmPhraseStage,
recoveryMechanism,
generatedMnemonic,
} = useSelector((state: RootState) => state.keyGeneration)
const { activeStep, subStepValidatorSetup } = useSelector(
(state: RootState) => state.validatorOnboarding,
)
const { isWalletConnected } = useSelector((state: RootState) => state.deposit)
const dispatch = useDispatch()
const navigate = useNavigate()
const isActivationValScreen = activeStep === 3 && subStepValidatorSetup === 3
const isActivationValidatorScreen = activeStep === 3 && subStepValidatorSetup === 3
useEffect(() => {
const getDisabledButton = () => {
if (activeStep === 4 && isConfirmPhraseStage) {
if (validWords.some(w => w === false)) {
return false
}
}
return false
}
const isDisabled = () => {
const isDepositWalletConnected = isWalletConnected === false && activeStep === 5
let isEmptyPhrase = false
let isNotValidWords = false
setIsDisabled(getDisabledButton())
}, [activeStep, subStepValidatorSetup, isConfirmPhraseStage, mnemonic, validWords])
const handleStep3 = () => {
subStepValidatorSetup < 3
? dispatch(setSubStepValidatorSetup(subStepValidatorSetup + 1))
: dispatch(setSubStepValidatorSetup(0))
}
const handleStep4 = () => {
if (!isConfirmPhraseStage && recoveryMechanism === KEYSTORE_FILES) {
return dispatch(setActiveStep(activeStep + 1))
}
if (!isConfirmPhraseStage) {
return dispatch(setIsConfirmPhraseStage(true))
}
if (isConfirmPhraseStage) {
isEmptyPhrase = words.some(word => word === '')
isNotValidWords = validWords.every(word => word === false)
}
const newValidWords = mnemonic.map((w, index) => generatedMnemonic[index] === w)
dispatch(setValidWords(newValidWords))
if (isEmptyPhrase || isNotValidWords || isDepositWalletConnected) {
return true
if (!newValidWords.includes(false)) {
setActiveStep(activeStep + 1)
dispatch(setIsConfirmPhraseStage(false))
if (isCopyPastedPhrase) {
dispatch(setIsCopyPastedPhrase(false))
}
}
}
}
const continueHandler = () => {
if (activeStep === 3) {
handleStep3()
} else if (activeStep === 4) {
handleStep4()
} else {
if (activeStep < 5) {
setActiveStep(activeStep + 1)
} else {
navigate('/')
}
}
return false
}
return (
<YStack style={{ width: '100%', alignItems: 'center', zIndex: 999, marginTop: '30px' }}>
<Stack style={{ width: '100%' }}>
{isCopyPastedPhrase && (
<XStack
style={{
width: '100%',
justifyContent: isActivationValScreen ? 'space-between' : 'end',
alignItems: 'center',
zIndex: 1000,
marginTop: '10px',
}}
>
{isCopyPastedPhrase && (
<Stack style={{ width: '100%', position: 'absolute' }}>
<InformationBox
message="You have copy and pasted the entire Recovery Phrase. Please ensure you have secured it appropriately prior to continuing."
variant="error"
icon={<CloseCircleIcon size={20} />}
/>
)}
{isActivationValidatorScreen && (
<LinkWithArrow
text="Skip to Dashboard"
to="/"
arrowRight={true}
style={{ fontWeight: 'bold', zIndex: 1000 }}
/>
)}
</Stack>
<Stack
style={{
width: '100%',
zIndex: 999,
alignItems: 'end',
}}
>
<Button
onPress={continueHandler}
size={40}
disabled={isDisabled() || (isValidatorSet === false && activeStep === 3)}
>
{activeStep < 6 ? 'Continue' : 'Continue to Dashboard'}
</Button>
</Stack>
</YStack>
</Stack>
)}
{isActivationValScreen && (
<LinkWithArrow
text="Skip to Dashboard"
to="/dashboard"
arrowRight={true}
style={{ fontWeight: 'bold', zIndex: 999 }}
/>
)}
<Button onPress={continueHandler} size={40} disabled={isDisabled || (isValidatorSet === false && activeStep === 3)}>
{activeStep < 5 ? 'Continue' : 'Continue to Dashboard'}
</Button>
</XStack>
)
}

View File

@ -9,6 +9,13 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
activeStep: {
options: [0, 1, 2, 3, 4, 5],
control: { type: 'radio' },
defaultValue: 0,
},
},
} satisfies Meta<typeof FormStepper>
export default meta
@ -17,48 +24,41 @@ type Story = StoryObj<typeof meta>
export const OverviewActive: Story = {
args: {
activeStep: 0,
changeActiveStep: () => {},
},
}
export const AdvisoriesActive: Story = {
args: {
activeStep: 1,
changeActiveStep: () => {},
},
}
export const ClientSetupActive: Story = {
args: {
activeStep: 2,
changeActiveStep: () => {},
},
}
export const ValidatorSetupActive: Story = {
args: {
activeStep: 3,
changeActiveStep: () => {},
},
}
export const KeyGenerationActive: Story = {
args: {
activeStep: 4,
changeActiveStep: () => {},
},
}
export const ActivationActive: Story = {
args: {
activeStep: 5,
changeActiveStep: () => {},
},
}
export const NoActiveStep: Story = {
args: {
activeStep: -1,
changeActiveStep: () => {},
},
}

View File

@ -1,11 +1,10 @@
import { Stepper, Step } from 'react-form-stepper'
import './FormStepper.css'
import { useDispatch } from 'react-redux'
type FormStepperProps = {
activeStep: number
changeActiveStep: (step: number) => void
}
import { setActiveStep } from '../../../redux/ValidatorOnboarding/slice'
import './FormStepper.css'
const steps = [
{ label: 'Overview', subtitle: 'Get Started' },
@ -17,7 +16,13 @@ const steps = [
{ label: 'Activation', subtitle: 'Complete Setup' },
]
const FormStepper = ({ activeStep, changeActiveStep }: FormStepperProps) => {
type FormStepperProps = {
activeStep: number
}
const FormStepper = ({ activeStep }: FormStepperProps) => {
const dispatch = useDispatch()
return (
<Stepper
activeStep={activeStep}
@ -37,7 +42,7 @@ const FormStepper = ({ activeStep, changeActiveStep }: FormStepperProps) => {
key={index}
label={step.label}
className="custom-step"
onClick={() => changeActiveStep(index)}
onClick={() => dispatch(setActiveStep(index))}
completed={activeStep > index - 1}
data-subtitle={step.subtitle}
data-step={step.label}

View File

@ -5,7 +5,6 @@ import wordlist from 'web-bip39/wordlists/english'
import { RootState } from '../../../../redux/store'
import {
setIsCopyPastedPhrase,
setMnemonic,
setValidWords,
setWord,
} from '../../../../redux/ValidatorOnboarding/KeyGeneration/slice'
@ -18,9 +17,9 @@ type AutocompleteInputProps = {
const AutocompleteInput = ({ index }: AutocompleteInputProps) => {
const [suggestions, setSuggestions] = useState<string[]>([])
const [isFocused, setIsFocused] = useState(false)
const word = useSelector((state: RootState) => state.keyGeneration.words[index])
const word = useSelector((state: RootState) => state.keyGeneration.mnemonic[index])
const isValidWord = useSelector((state: RootState) => state.keyGeneration.validWords[index])
const validWords = useSelector((state: RootState) => state.keyGeneration.validWords)
const { validWords, generatedMnemonic } = useSelector((state: RootState) => state.keyGeneration)
const dispatch = useDispatch()
useEffect(() => {
@ -32,39 +31,47 @@ const AutocompleteInput = ({ index }: AutocompleteInputProps) => {
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target
const newMnemonic = value.trim().split(' ')
const newMnemonicLength = newMnemonic.length
let newValidWords = [...validWords]
if (!isFocused) {
handleInputFocus()
}
const value = e.target.value
const mnemonic = value.trim().split(' ').slice(0, 24)
const mnemonicLength = mnemonic.length
let newValidWords = [...validWords]
if (mnemonicLength === 1) {
dispatch(setWord({ index, word: value }))
newValidWords[index] = wordlist.includes(value) || getNewSuggestions(value).length > 0
} else if (mnemonicLength === 24) {
dispatch(setMnemonic(mnemonic))
dispatch(setIsCopyPastedPhrase(true))
mnemonic.forEach((m, i) => {
newValidWords[i] = wordlist.includes(m)
})
} else {
for (let i = index; i < mnemonicLength + index; i++) {
const mnemonicWord = mnemonic.shift() || ''
dispatch(setWord({ index: i, word: mnemonicWord }))
newValidWords[i] = wordlist.includes(mnemonicWord)
}
dispatch(setIsCopyPastedPhrase(true))
switch (newMnemonicLength) {
case 1:
updateWord(index, value, newValidWords)
break
case 24:
dispatch(setIsCopyPastedPhrase(true))
updateMultipleWords(newMnemonic, newValidWords)
break
default:
const endIndex = Math.min(newMnemonicLength + index, 24)
const partialMnemonic = newMnemonic.slice(0, endIndex - index)
dispatch(setIsCopyPastedPhrase(true))
updateMultipleWords(partialMnemonic, newValidWords, index)
break
}
dispatch(setValidWords(newValidWords))
}
const updateWord = (idx: number, word: string, validWords: boolean[]) => {
dispatch(setWord({ index: idx, word }))
validWords[idx] = generatedMnemonic[idx] === word || generatedMnemonic[idx].startsWith(word)
}
const updateMultipleWords = (words: string[], validWords: boolean[], startIndex: number = 0) => {
words.forEach((word, idx) => {
const actualIdx = startIndex + idx
dispatch(setWord({ index: actualIdx, word }))
validWords[actualIdx] = generatedMnemonic[actualIdx] === word
})
}
const handleSuggestionClick = (e: React.MouseEvent, suggestion: string) => {
e.preventDefault()
@ -72,7 +79,7 @@ const AutocompleteInput = ({ index }: AutocompleteInputProps) => {
dispatch(setWord({ index, word: suggestion }))
let newValidWords = [...validWords]
newValidWords[index] = wordlist.includes(suggestion)
newValidWords[index] = generatedMnemonic[index] === suggestion
dispatch(setValidWords(newValidWords))
}
@ -84,7 +91,7 @@ const AutocompleteInput = ({ index }: AutocompleteInputProps) => {
setIsFocused(false)
let newValidWords = [...validWords]
newValidWords[index] = wordlist.includes(word)
newValidWords[index] = generatedMnemonic[index] === word
dispatch(setValidWords(newValidWords))
}

View File

@ -1,20 +1,21 @@
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import { useState } from 'react'
import { useSelector } from 'react-redux'
import KeyGenerationHeader from './KeyGenerationHeader/KeyGenerationHeader'
import RecoveryMechanism from './RecoveryMechanism/RecoveryMechanism'
import KeystoreFiles from './KeystoreFiles'
import RecoveryPhrase from './RecoveryPhrase'
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../constants'
import ConfirmRecoveryPhrase from './ConfirmRecoveryPhrase/ConfirmRecoveryPhrase'
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../constants'
import { RootState } from '../../../redux/store'
type KeyGenerationProps = {
isConfirmPhraseStage: boolean
}
const KeyGeneration = ({ isConfirmPhraseStage }: KeyGenerationProps) => {
const [recoveryMechanism, setRecoveryMechanism] = useState(KEYSTORE_FILES)
const { recoveryMechanism } = useSelector((state: RootState) => state.keyGeneration)
const isKeystoreFiles =
recoveryMechanism === KEYSTORE_FILES || recoveryMechanism === BOTH_KEY_AND_RECOVERY
@ -22,20 +23,13 @@ const KeyGeneration = ({ isConfirmPhraseStage }: KeyGenerationProps) => {
const isRecoveryPhrase =
recoveryMechanism === RECOVERY_PHRASE || recoveryMechanism === BOTH_KEY_AND_RECOVERY
const handleRecMechanismChange = (value: string) => {
setRecoveryMechanism(value)
}
return (
<YStack space={'$2'} style={{ width: '100%', padding: '16px 32px', alignItems: 'start' }}>
{isConfirmPhraseStage && <ConfirmRecoveryPhrase />}
{isConfirmPhraseStage === false && (
<>
<KeyGenerationHeader />
<RecoveryMechanism
recoveryMechanism={recoveryMechanism}
handleRecMechanismChange={handleRecMechanismChange}
/>
<RecoveryMechanism recoveryMechanism={recoveryMechanism} />
<Stack style={{ margin: '30px 0' }}>
<Text size={27} weight={'semibold'}>
4 Validators

View File

@ -5,7 +5,7 @@ import KeyGenerationTitle from '../KeyGenerationTitle'
const KeyGenerationHeader = () => {
return (
<XStack style={{ width: '100%', alignItems: 'center', justifyContent: 'space-between' }}>
<XStack style={{ width: '100%', justifyContent: 'space-between' }}>
<KeyGenerationTitle />
<XStack space={'$2'}>
<KeyGenerationSyncCard

View File

@ -1,6 +1,7 @@
import { Stack, XStack, YStack } from 'tamagui'
import { ClearIcon } from '@status-im/icons'
import { Text } from '@status-im/components'
import { useState } from 'react'
import StandardGauge from '../../../../components/Charts/StandardGauge'
import BorderBox from '../../../../components/General/BorderBox'
@ -14,6 +15,16 @@ type KeyGenerationSyncCardProps = {
}
const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyncCardProps) => {
const [isOpen, setIsOpen] = useState(true)
const closeCardHanlder = () => {
setIsOpen(false)
}
if (isOpen === false) {
return null
}
return (
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
<XStack space={'$2'} alignItems="center">
@ -48,7 +59,12 @@ const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyn
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
</Text>
</YStack>
<ClearIcon size={20} color="#A1ABBD" />
<ClearIcon
size={20}
color="#A1ABBD"
onClick={closeCardHanlder}
style={{ cursor: 'pointer' }}
/>
</XStack>
</BorderBox>
)

View File

@ -6,26 +6,49 @@ import { useState } from 'react'
const KeystoreFiles = () => {
const [encryptedPassword, setEncryptedPassword] = useState('')
const [confirmEncryptedPassword, setConfirmEncryptedPassword] = useState('')
const [encryptedPasswordError, setEncryptedPasswordError] = useState(false)
const [confirmEncryptedPasswordError, setConfirmEncryptedPasswordError] = useState(false)
const [displayEncryptedPassword, setDisplayEncryptedPassword] = useState('')
const [displayConfirmEncryptedPassword, setDisplayConfirmEncryptedPassword] = useState('')
const generateKeystoreFilesHandler = () => {}
const generateKeystoreFilesHandler = () => {
if (
encryptedPassword !== confirmEncryptedPassword ||
encryptedPassword === '' ||
confirmEncryptedPassword === ''
) {
setEncryptedPasswordError(true)
return setConfirmEncryptedPasswordError(true)
}
const changeEncryptedPasswordHandler = (value: string) => {
setEncryptedPassword(value)
setEncryptedPasswordError(false)
setConfirmEncryptedPasswordError(false)
}
const changeConfirmEncryptedPasswordHandler = (value: string) => {
setConfirmEncryptedPassword(value)
const changeEncryptedPasswordHandler = (e: any) => {
const password = e.target.value
setEncryptedPassword(password)
setDisplayEncryptedPassword(getHidedPassword(password.length))
}
const clearEncryptedPasswordHandler = () => {
setEncryptedPassword('')
const changeConfirmEncryptedPasswordHandler = (e: any) => {
const password = e.target.value
setConfirmEncryptedPassword(password)
setDisplayConfirmEncryptedPassword(getHidedPassword(password.length))
}
const clearConfirmEncryptedPasswordHandler = () => {
setConfirmEncryptedPassword('')
const downloadKeyFilesHandler = () => {
const element = document.createElement('a')
const file = new Blob([''], { type: 'text/plain' })
element.href = URL.createObjectURL(file)
element.download = 'keystore_files.txt'
document.body.appendChild(element)
element.click()
}
const downloadKeyFilesHandler = () => {}
const getHidedPassword = (passwordLength: number) => {
return '*'.repeat(passwordLength)
}
return (
<YStack space={'$4'}>
@ -42,11 +65,12 @@ const KeystoreFiles = () => {
size={16}
color="#A1ABBD"
style={{ cursor: 'pointer' }}
onClick={clearEncryptedPasswordHandler}
onClick={() => setEncryptedPassword('')}
/>
}
value={encryptedPassword}
onChangeText={changeEncryptedPasswordHandler}
error={encryptedPasswordError}
value={displayEncryptedPassword}
onChange={changeEncryptedPasswordHandler}
/>
</YStack>
<YStack space={'$2'}>
@ -60,11 +84,12 @@ const KeystoreFiles = () => {
size={16}
color="#A1ABBD"
style={{ cursor: 'pointer' }}
onClick={clearConfirmEncryptedPasswordHandler}
onClick={() => setConfirmEncryptedPassword('')}
/>
}
value={confirmEncryptedPassword}
onChangeText={changeConfirmEncryptedPasswordHandler}
error={confirmEncryptedPasswordError}
value={displayConfirmEncryptedPassword}
onChange={changeConfirmEncryptedPasswordHandler}
/>
</YStack>
</YStack>

View File

@ -10,6 +10,13 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
recoveryMechanism: {
options: [RECOVERY_PHRASE, KEYSTORE_FILES, BOTH_KEY_AND_RECOVERY],
control: { type: 'radio' },
defaultValue: KEYSTORE_FILES,
},
},
} satisfies Meta<typeof RecoveryMechanism>
export default meta
@ -18,27 +25,23 @@ type Story = StoryObj<typeof meta>
export const KeystoreFiles: Story = {
args: {
recoveryMechanism: KEYSTORE_FILES,
handleRecMechanismChange: () => {},
},
}
export const RecoveryPhrase: Story = {
args: {
recoveryMechanism: RECOVERY_PHRASE,
handleRecMechanismChange: () => {},
},
}
export const BothKeystoreAndRecovery: Story = {
args: {
recoveryMechanism: BOTH_KEY_AND_RECOVERY,
handleRecMechanismChange: () => {},
},
}
export const WithoutRecMechanism: Story = {
args: {
recoveryMechanism: '',
handleRecMechanismChange: () => {},
},
}

View File

@ -6,15 +6,11 @@ import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../..
type RecoveryMechanismProps = {
recoveryMechanism: string
handleRecMechanismChange: (value: string) => void
}
const cards = [RECOVERY_PHRASE, KEYSTORE_FILES, BOTH_KEY_AND_RECOVERY]
const RecoveryMechanism = ({
recoveryMechanism,
handleRecMechanismChange,
}: RecoveryMechanismProps) => {
const RecoveryMechanism = ({ recoveryMechanism }: RecoveryMechanismProps) => {
return (
<YStack style={{ width: '100%' }}>
<Text size={19} weight={'semibold'}>
@ -22,12 +18,7 @@ const RecoveryMechanism = ({
</Text>
<XStack space={'$4'} style={{ justifyContent: 'space-between', marginTop: '40px' }}>
{cards.map(value => (
<RecoveryMechanismCard
key={value}
value={value}
recoveryMechanism={recoveryMechanism}
handleRecMechanismChange={handleRecMechanismChange}
/>
<RecoveryMechanismCard key={value} value={value} recoveryMechanism={recoveryMechanism} />
))}
</XStack>
</YStack>

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react'
import RecoveryMechanismCard from './RecoveryMechanismCard'
import { KEYSTORE_FILES } from '../../../../constants'
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../../constants'
const meta = {
title: 'ValidatorOnboarding/RecoveryMechanismCard',
@ -15,11 +15,24 @@ const meta = {
export default meta
type Story = StoryObj<typeof meta>
export const Selected: Story = {
export const KeystoreFiles: Story = {
args: {
value: KEYSTORE_FILES,
recoveryMechanism: KEYSTORE_FILES,
handleRecMechanismChange: () => {},
},
}
export const RecoveryPhrase: Story = {
args: {
value: RECOVERY_PHRASE,
recoveryMechanism: RECOVERY_PHRASE,
},
}
export const BothKeyAndRecovery: Story = {
args: {
value: BOTH_KEY_AND_RECOVERY,
recoveryMechanism: BOTH_KEY_AND_RECOVERY,
},
}
@ -27,7 +40,6 @@ export const NotSelected: Story = {
args: {
value: KEYSTORE_FILES,
recoveryMechanism: '',
handleRecMechanismChange: () => {},
},
}
@ -35,6 +47,5 @@ export const WithoutValue: Story = {
args: {
value: '',
recoveryMechanism: KEYSTORE_FILES,
handleRecMechanismChange: () => {},
},
}

View File

@ -1,16 +1,20 @@
import { Text } from '@status-im/components'
import { useDispatch } from 'react-redux'
import { setRecoveryMechanism } from '../../../../redux/ValidatorOnboarding/KeyGeneration/slice'
type RecoveryMechanismProps = {
value: string
recoveryMechanism: string
handleRecMechanismChange: (value: string) => void
}
const RecoveryMechanismCard = ({
value,
recoveryMechanism,
handleRecMechanismChange,
}: RecoveryMechanismProps) => {
const RecoveryMechanismCard = ({ value, recoveryMechanism }: RecoveryMechanismProps) => {
const dispatch = useDispatch()
const handleRecMechanismChange = () => {
dispatch(setRecoveryMechanism(value))
}
return (
<div
style={{
@ -22,7 +26,7 @@ const RecoveryMechanismCard = ({
width: '100%',
height: '140px',
}}
onClick={() => handleRecMechanismChange(value)}
onClick={handleRecMechanismChange}
>
<Text size={15} weight={'semibold'}>
{value}

View File

@ -1,7 +1,13 @@
import { Stack, XStack, YStack } from 'tamagui'
import { Button, InformationBox, Text } from '@status-im/components'
import { CloseCircleIcon } from '@status-im/icons'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { generateMnemonic } from 'web-bip39'
import wordlist from 'web-bip39/wordlists/english'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../redux/store'
import { setGeneratedMnemonic } from '../../../redux/ValidatorOnboarding/KeyGeneration/slice'
type RecoveryPhraseProps = {
isKeystoreFiles: boolean
@ -9,11 +15,31 @@ type RecoveryPhraseProps = {
const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
const [isReveal, setIsReveal] = useState(false)
const { generatedMnemonic } = useSelector((state: RootState) => state.keyGeneration)
const dispatch = useDispatch()
useEffect(() => {
getMnemonic()
}, [])
const getMnemonic = async () => {
const mnemonic = await generateMnemonic(wordlist, 256)
dispatch(setGeneratedMnemonic(mnemonic.split(' ')))
}
const revealHandler = () => {
setIsReveal(state => !state)
}
const copyRecoveryPhraseHandler = () => {
if (isKeystoreFiles) {
return
}
const text = generatedMnemonic.join(' ')
navigator.clipboard.writeText(text)
}
return (
<YStack space={'$4'} style={{ width: '100%', marginTop: isKeystoreFiles ? '20px' : '0px' }}>
<Stack
@ -23,30 +49,32 @@ const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
padding: '28px 18px',
backgroundColor: '#f4f6fe',
width: '100%',
height: '176px',
}}
>
<YStack space={'$2'} style={{ filter: `blur(${isReveal ? '0px' : '4px'})` }}>
<XStack space={'$6'}>
<Text size={19} weight={'semibold'}>
this is your secret recovery phrase for the validator
</Text>
<Text size={19} weight={'semibold'}>
this is your secret recovery phrase for the validator
</Text>
</XStack>
<XStack space={'$6'}>
<Text size={19} weight={'semibold'}>
this is your secret recovery phrase for the validator
</Text>
<Text size={19} weight={'semibold'}>
this is your secret recovery phrase for the validator
</Text>
<YStack
space={'$2'}
style={{ filter: `blur(${isReveal ? '0px' : '4px'})`, cursor: 'pointer' }}
onClick={copyRecoveryPhraseHandler}
>
<XStack
style={{
display: 'grid',
gridTemplateColumns: 'repeat(6, 1fr)',
gap: '5px 0px',
}}
>
{generatedMnemonic.map((word, index) => (
<Text key={index} size={19} weight={'semibold'}>
{word}
</Text>
))}
</XStack>
</YStack>
</Stack>
<Stack style={{ width: 'fit-content', marginBottom: '12px' }}>
<Button onPress={revealHandler}>Reveal Recovery Phrase</Button>
<Button onPress={revealHandler}>
{isReveal ? 'Hide Recovery Phrase' : 'Reveal Recovery Phrase'}
</Button>
</Stack>
<InformationBox
message="Write down and keep your Secret Recovery Phrase in a secure place. Make sure no one is looking at your screen."

View File

@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import wordlist from 'web-bip39/wordlists/english'
ц
import {
setIsCopyPastedPhrase,
@ -21,15 +22,24 @@ import Advisories from './Advisories/Advisories'
import ValidatorSetup from './ValidatorSetup/ValidatorSetup/ValidatorSetup'
import ValidatorSetupInstall from './ValidatorSetup/ValidatorInstalling/ValidatorInstall'
import ContinueButton from './ContinueButton'
import ActivationValidatorSetup from './ValidatorSetup/ValidatorActivation/ActivationValidatorSetup'
import './layoutGradient.css'
import Deposit from './Deposit/Deposit'
import { setIsTransactionConfirmation } from '../../redux/ValidatorOnboarding/Deposit/slice'
import './layoutGradient.css'
const ValidatorOnboarding = () => {
const [activeStep, setActiveStep] = useState(0)
const { activeStep, subStepValidatorSetup } = useSelector(
(state: RootState) => state.validatorOnboarding,
)
const { isConfirmPhraseStage } = useSelector((state: RootState) => state.keyGeneration)
const [isConfirmPhraseStage, setIsConfirmPhraseStage] = useState(false)
const [subStepValidatorSetup, setSubStepValidatorSetup] = useState(0)
const [subStepAdvisories, setSubStepAdvisories] = useState(0)
@ -101,7 +111,7 @@ const ValidatorOnboarding = () => {
setIsConfirmPhraseStage(false)
}
}
return (
<div className="gradient-wrapper">
<YStack
@ -118,7 +128,7 @@ const ValidatorOnboarding = () => {
titleSize={19}
subtitle="Earn Rewards for securing the Ethereum Network"
/>
<FormStepper activeStep={activeStep} changeActiveStep={changeActiveStep} />
<FormStepper activeStep={activeStep} />
<ValidatorBoxWrapper>
{activeStep === 0 && <Overview />}
{activeStep === 1 && (
@ -156,13 +166,7 @@ const ValidatorOnboarding = () => {
/>
)}
</ValidatorBoxWrapper>
<ContinueButton
activeStep={activeStep}
continueHandler={continueHandler}
isConfirmPhraseStage={isConfirmPhraseStage}
subStepValidatorSetup={subStepValidatorSetup}
isValidatorSet={isValidatorSet}
/>
<ContinueButton />
</YStack>
</div>
)

View File

@ -1,9 +1,14 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { KEYSTORE_FILES } from '../../../constants'
type KeyGenerationState = {
words: string[]
mnemonic: string[]
isCopyPastedPhrase: boolean
validWords: boolean[]
generatedMnemonic: string[]
isConfirmPhraseStage: boolean
recoveryMechanism: string
}
type wordProps = {
@ -12,9 +17,12 @@ type wordProps = {
}
const initialState: KeyGenerationState = {
words: Array(24).fill(''),
mnemonic: Array(24).fill(''),
isCopyPastedPhrase: false,
validWords: Array(24).fill(true),
generatedMnemonic: Array(24).fill(''),
isConfirmPhraseStage: false,
recoveryMechanism: KEYSTORE_FILES,
}
const keyGenerationSlice = createSlice({
@ -22,12 +30,9 @@ const keyGenerationSlice = createSlice({
initialState,
reducers: {
setWord: (state, action: PayloadAction<wordProps>) => {
const newWords = [...state.words]
newWords[action.payload.index] = action.payload.word
return { ...state, words: newWords }
},
setMnemonic: (state, action: PayloadAction<string[]>) => {
state.words = action.payload
const newMnemonic = [...state.mnemonic]
newMnemonic[action.payload.index] = action.payload.word
state.mnemonic = newMnemonic
},
setIsCopyPastedPhrase: (state, action: PayloadAction<boolean>) => {
state.isCopyPastedPhrase = action.payload
@ -35,10 +40,25 @@ const keyGenerationSlice = createSlice({
setValidWords: (state, action: PayloadAction<boolean[]>) => {
state.validWords = action.payload
},
setGeneratedMnemonic: (state, action: PayloadAction<string[]>) => {
state.generatedMnemonic = action.payload
},
setIsConfirmPhraseStage: (state, action: PayloadAction<boolean>) => {
state.isConfirmPhraseStage = action.payload
},
setRecoveryMechanism: (state, action: PayloadAction<string>) => {
state.recoveryMechanism = action.payload
},
},
})
export const { setWord, setMnemonic, setIsCopyPastedPhrase, setValidWords } =
keyGenerationSlice.actions
export const {
setWord,
setIsCopyPastedPhrase,
setValidWords,
setGeneratedMnemonic,
setIsConfirmPhraseStage,
setRecoveryMechanism,
} = keyGenerationSlice.actions
export default keyGenerationSlice.reducer

View File

@ -0,0 +1,28 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
type ValidatorOnboardingState = {
activeStep: number
subStepValidatorSetup: number
}
const initialState: ValidatorOnboardingState = {
activeStep: 0,
subStepValidatorSetup: 0,
}
const validatorOnboardingSlice = createSlice({
name: 'validatorOnboarding',
initialState,
reducers: {
setActiveStep(state, action: PayloadAction<number>) {
state.activeStep = action.payload
},
setSubStepValidatorSetup(state, action: PayloadAction<number>) {
state.subStepValidatorSetup = action.payload
},
},
})
export const { setActiveStep, setSubStepValidatorSetup } = validatorOnboardingSlice.actions
export default validatorOnboardingSlice.reducer

View File

@ -6,6 +6,7 @@ import pinnedMessageReducer from './PinnedMessage/slice'
import execClientReducer from './ValidatorOnboarding/ValidatorSetup/slice'
import keyGenerationReducer from './ValidatorOnboarding/KeyGeneration/slice'
import depositReducer from './ValidatorOnboarding/Deposit/slice'
import validatorOnboardingReducer from './ValidatorOnboarding/slice'
const store = configureStore({
reducer: {
@ -15,6 +16,7 @@ const store = configureStore({
theme: themeReducer,
keyGeneration: keyGenerationReducer,
deposit: depositReducer,
validatorOnboarding: validatorOnboardingReducer,
},
})