Merge branch 'main' into hn.UI-feedback-fixes
This commit is contained in:
commit
01020dd26b
|
@ -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: {},
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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: () => {},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: () => {},
|
||||
},
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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: () => {},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue