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> type Story = StoryObj<typeof meta>
export const Default: Story = { export const Default: Story = {
args: { args: {},
continueHandler: () => {},
activeStep: 0,
isConfirmPhraseStage: false,
subStepValidatorSetup: 0,
},
}
export const Disabled: Story = {
args: {
continueHandler: () => {},
activeStep: 0,
isConfirmPhraseStage: true,
subStepValidatorSetup: 0,
},
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,26 +6,49 @@ import { useState } from 'react'
const KeystoreFiles = () => { const KeystoreFiles = () => {
const [encryptedPassword, setEncryptedPassword] = useState('') const [encryptedPassword, setEncryptedPassword] = useState('')
const [confirmEncryptedPassword, setConfirmEncryptedPassword] = 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) => { setEncryptedPasswordError(false)
setEncryptedPassword(value) setConfirmEncryptedPasswordError(false)
} }
const changeConfirmEncryptedPasswordHandler = (value: string) => { const changeEncryptedPasswordHandler = (e: any) => {
setConfirmEncryptedPassword(value) const password = e.target.value
setEncryptedPassword(password)
setDisplayEncryptedPassword(getHidedPassword(password.length))
} }
const clearEncryptedPasswordHandler = () => { const changeConfirmEncryptedPasswordHandler = (e: any) => {
setEncryptedPassword('') const password = e.target.value
setConfirmEncryptedPassword(password)
setDisplayConfirmEncryptedPassword(getHidedPassword(password.length))
} }
const clearConfirmEncryptedPasswordHandler = () => { const downloadKeyFilesHandler = () => {
setConfirmEncryptedPassword('') 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 ( return (
<YStack space={'$4'}> <YStack space={'$4'}>
@ -42,11 +65,12 @@ const KeystoreFiles = () => {
size={16} size={16}
color="#A1ABBD" color="#A1ABBD"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={clearEncryptedPasswordHandler} onClick={() => setEncryptedPassword('')}
/> />
} }
value={encryptedPassword} error={encryptedPasswordError}
onChangeText={changeEncryptedPasswordHandler} value={displayEncryptedPassword}
onChange={changeEncryptedPasswordHandler}
/> />
</YStack> </YStack>
<YStack space={'$2'}> <YStack space={'$2'}>
@ -60,11 +84,12 @@ const KeystoreFiles = () => {
size={16} size={16}
color="#A1ABBD" color="#A1ABBD"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={clearConfirmEncryptedPasswordHandler} onClick={() => setConfirmEncryptedPassword('')}
/> />
} }
value={confirmEncryptedPassword} error={confirmEncryptedPasswordError}
onChangeText={changeConfirmEncryptedPasswordHandler} value={displayConfirmEncryptedPassword}
onChange={changeConfirmEncryptedPasswordHandler}
/> />
</YStack> </YStack>
</YStack> </YStack>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,13 @@
import { Stack, XStack, YStack } from 'tamagui' import { Stack, XStack, YStack } from 'tamagui'
import { Button, InformationBox, Text } from '@status-im/components' import { Button, InformationBox, Text } from '@status-im/components'
import { CloseCircleIcon } from '@status-im/icons' 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 = { type RecoveryPhraseProps = {
isKeystoreFiles: boolean isKeystoreFiles: boolean
@ -9,11 +15,31 @@ type RecoveryPhraseProps = {
const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => { const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
const [isReveal, setIsReveal] = useState(false) 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 = () => { const revealHandler = () => {
setIsReveal(state => !state) setIsReveal(state => !state)
} }
const copyRecoveryPhraseHandler = () => {
if (isKeystoreFiles) {
return
}
const text = generatedMnemonic.join(' ')
navigator.clipboard.writeText(text)
}
return ( return (
<YStack space={'$4'} style={{ width: '100%', marginTop: isKeystoreFiles ? '20px' : '0px' }}> <YStack space={'$4'} style={{ width: '100%', marginTop: isKeystoreFiles ? '20px' : '0px' }}>
<Stack <Stack
@ -23,30 +49,32 @@ const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
padding: '28px 18px', padding: '28px 18px',
backgroundColor: '#f4f6fe', backgroundColor: '#f4f6fe',
width: '100%', width: '100%',
height: '176px',
}} }}
> >
<YStack space={'$2'} style={{ filter: `blur(${isReveal ? '0px' : '4px'})` }}> <YStack
<XStack space={'$6'}> space={'$2'}
<Text size={19} weight={'semibold'}> style={{ filter: `blur(${isReveal ? '0px' : '4px'})`, cursor: 'pointer' }}
this is your secret recovery phrase for the validator onClick={copyRecoveryPhraseHandler}
</Text> >
<Text size={19} weight={'semibold'}> <XStack
this is your secret recovery phrase for the validator style={{
</Text> display: 'grid',
</XStack> gridTemplateColumns: 'repeat(6, 1fr)',
<XStack space={'$6'}> gap: '5px 0px',
<Text size={19} weight={'semibold'}> }}
this is your secret recovery phrase for the validator >
</Text> {generatedMnemonic.map((word, index) => (
<Text size={19} weight={'semibold'}> <Text key={index} size={19} weight={'semibold'}>
this is your secret recovery phrase for the validator {word}
</Text> </Text>
))}
</XStack> </XStack>
</YStack> </YStack>
</Stack> </Stack>
<Stack style={{ width: 'fit-content', marginBottom: '12px' }}> <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> </Stack>
<InformationBox <InformationBox
message="Write down and keep your Secret Recovery Phrase in a secure place. Make sure no one is looking at your screen." 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 { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import wordlist from 'web-bip39/wordlists/english' import wordlist from 'web-bip39/wordlists/english'
ц
import { import {
setIsCopyPastedPhrase, setIsCopyPastedPhrase,
@ -21,15 +22,24 @@ import Advisories from './Advisories/Advisories'
import ValidatorSetup from './ValidatorSetup/ValidatorSetup/ValidatorSetup' import ValidatorSetup from './ValidatorSetup/ValidatorSetup/ValidatorSetup'
import ValidatorSetupInstall from './ValidatorSetup/ValidatorInstalling/ValidatorInstall' import ValidatorSetupInstall from './ValidatorSetup/ValidatorInstalling/ValidatorInstall'
import ContinueButton from './ContinueButton' import ContinueButton from './ContinueButton'
import ActivationValidatorSetup from './ValidatorSetup/ValidatorActivation/ActivationValidatorSetup' import ActivationValidatorSetup from './ValidatorSetup/ValidatorActivation/ActivationValidatorSetup'
import './layoutGradient.css'
import Deposit from './Deposit/Deposit' import Deposit from './Deposit/Deposit'
import { setIsTransactionConfirmation } from '../../redux/ValidatorOnboarding/Deposit/slice' import { setIsTransactionConfirmation } from '../../redux/ValidatorOnboarding/Deposit/slice'
import './layoutGradient.css' import './layoutGradient.css'
const ValidatorOnboarding = () => { 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 [isConfirmPhraseStage, setIsConfirmPhraseStage] = useState(false)
const [subStepValidatorSetup, setSubStepValidatorSetup] = useState(0)
const [subStepAdvisories, setSubStepAdvisories] = useState(0) const [subStepAdvisories, setSubStepAdvisories] = useState(0)
@ -101,7 +111,7 @@ const ValidatorOnboarding = () => {
setIsConfirmPhraseStage(false) setIsConfirmPhraseStage(false)
} }
} }
return ( return (
<div className="gradient-wrapper"> <div className="gradient-wrapper">
<YStack <YStack
@ -118,7 +128,7 @@ const ValidatorOnboarding = () => {
titleSize={19} titleSize={19}
subtitle="Earn Rewards for securing the Ethereum Network" subtitle="Earn Rewards for securing the Ethereum Network"
/> />
<FormStepper activeStep={activeStep} changeActiveStep={changeActiveStep} /> <FormStepper activeStep={activeStep} />
<ValidatorBoxWrapper> <ValidatorBoxWrapper>
{activeStep === 0 && <Overview />} {activeStep === 0 && <Overview />}
{activeStep === 1 && ( {activeStep === 1 && (
@ -156,13 +166,7 @@ const ValidatorOnboarding = () => {
/> />
)} )}
</ValidatorBoxWrapper> </ValidatorBoxWrapper>
<ContinueButton <ContinueButton />
activeStep={activeStep}
continueHandler={continueHandler}
isConfirmPhraseStage={isConfirmPhraseStage}
subStepValidatorSetup={subStepValidatorSetup}
isValidatorSet={isValidatorSet}
/>
</YStack> </YStack>
</div> </div>
) )

View File

@ -1,9 +1,14 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { KEYSTORE_FILES } from '../../../constants'
type KeyGenerationState = { type KeyGenerationState = {
words: string[] mnemonic: string[]
isCopyPastedPhrase: boolean isCopyPastedPhrase: boolean
validWords: boolean[] validWords: boolean[]
generatedMnemonic: string[]
isConfirmPhraseStage: boolean
recoveryMechanism: string
} }
type wordProps = { type wordProps = {
@ -12,9 +17,12 @@ type wordProps = {
} }
const initialState: KeyGenerationState = { const initialState: KeyGenerationState = {
words: Array(24).fill(''), mnemonic: Array(24).fill(''),
isCopyPastedPhrase: false, isCopyPastedPhrase: false,
validWords: Array(24).fill(true), validWords: Array(24).fill(true),
generatedMnemonic: Array(24).fill(''),
isConfirmPhraseStage: false,
recoveryMechanism: KEYSTORE_FILES,
} }
const keyGenerationSlice = createSlice({ const keyGenerationSlice = createSlice({
@ -22,12 +30,9 @@ const keyGenerationSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setWord: (state, action: PayloadAction<wordProps>) => { setWord: (state, action: PayloadAction<wordProps>) => {
const newWords = [...state.words] const newMnemonic = [...state.mnemonic]
newWords[action.payload.index] = action.payload.word newMnemonic[action.payload.index] = action.payload.word
return { ...state, words: newWords } state.mnemonic = newMnemonic
},
setMnemonic: (state, action: PayloadAction<string[]>) => {
state.words = action.payload
}, },
setIsCopyPastedPhrase: (state, action: PayloadAction<boolean>) => { setIsCopyPastedPhrase: (state, action: PayloadAction<boolean>) => {
state.isCopyPastedPhrase = action.payload state.isCopyPastedPhrase = action.payload
@ -35,10 +40,25 @@ const keyGenerationSlice = createSlice({
setValidWords: (state, action: PayloadAction<boolean[]>) => { setValidWords: (state, action: PayloadAction<boolean[]>) => {
state.validWords = action.payload 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 } = export const {
keyGenerationSlice.actions setWord,
setIsCopyPastedPhrase,
setValidWords,
setGeneratedMnemonic,
setIsConfirmPhraseStage,
setRecoveryMechanism,
} = keyGenerationSlice.actions
export default keyGenerationSlice.reducer 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 execClientReducer from './ValidatorOnboarding/ValidatorSetup/slice'
import keyGenerationReducer from './ValidatorOnboarding/KeyGeneration/slice' import keyGenerationReducer from './ValidatorOnboarding/KeyGeneration/slice'
import depositReducer from './ValidatorOnboarding/Deposit/slice' import depositReducer from './ValidatorOnboarding/Deposit/slice'
import validatorOnboardingReducer from './ValidatorOnboarding/slice'
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
@ -15,6 +16,7 @@ const store = configureStore({
theme: themeReducer, theme: themeReducer,
keyGeneration: keyGenerationReducer, keyGeneration: keyGenerationReducer,
deposit: depositReducer, deposit: depositReducer,
validatorOnboarding: validatorOnboardingReducer,
}, },
}) })