Merge pull request #14 from nimbus-gui/hn.validator-onboarding

Create validator onboarding
This commit is contained in:
Hristo Nedelkov 2023-09-11 10:48:29 +03:00 committed by GitHub
commit d161e455e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 5636 additions and 1459 deletions

View File

@ -2,23 +2,23 @@ import React from 'react'
import type { Preview } from '@storybook/react'
import { TamaguiProvider } from '@tamagui/web'
import { Provider as StatusProvider } from '@status-im/components'
import '../src/index.css'
import { Provider as ReduxProvider } from 'react-redux'
import appConfig from '../tamagui.config'
import store from '../src/redux/store'
import '../src/index.css'
const preview: Preview = {
parameters: {
// layout: 'centered',
},
decorators: [
Story => {
return (
<TamaguiProvider config={appConfig}>
<StatusProvider>
Story => (
<TamaguiProvider config={appConfig}>
<StatusProvider>
<ReduxProvider store={store}>
<Story />
</StatusProvider>
</TamaguiProvider>
)
},
</ReduxProvider>
</StatusProvider>
</TamaguiProvider>
),
],
}

View File

@ -22,7 +22,8 @@
"@nivo/pie": "^0.83.0",
"@reduxjs/toolkit": "^1.9.5",
"@status-im/colors": "*",
"@status-im/components": "^0.2.6",
"@status-im/components": "^0.3.0",
"@storybook/addon-actions": "^7.4.0",
"@tamagui/config": "1.36.4",
"@tamagui/react-17-patch": "1.36.4",
"@tamagui/vite-plugin": "1.36.4",
@ -32,12 +33,16 @@
"expo-modules-core": "^1.5.9",
"react": "18",
"react-color": "^2.19.3",
"react-confetti": "^6.1.0",
"react-dom": "18",
"react-form-stepper": "^2.0.3",
"react-native": "^0.72.3",
"react-native-svg": "^13.10.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.14.2",
"tamagui": "1.36.4"
"react-syntax-highlighter": "^15.5.0",
"tamagui": "1.36.4",
"web-bip39": "^0.0.3"
},
"devDependencies": {
"@fsouza/prettierd": "^0.24.2",
@ -52,6 +57,7 @@
"@storybook/test-runner": "^0.12.0",
"@storybook/testing-library": "^0.2.0",
"@types/react-color": "^3.0.6",
"@types/react-syntax-highlighter": "^15.5.7",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",

BIN
public/icons/Linux.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
public/icons/MAC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,5 @@
<svg width="74" height="60" viewBox="0 0 74 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 31443">
<path id="Vector" d="M59.1019 45.0375V59.0747C51.3854 59.0747 47.4072 58.373 42.7293 52.0562C38.0514 45.7393 35.0293 45.0373 31.0346 45.0373V59.0747H17.001V45.0373H31.0346V31C38.2347 31 42.7293 31.9358 47.4072 38.0187C52.0851 44.1016 54.5517 45.0375 59.1019 45.0375V31H73.1355V45.0375H59.1019Z" fill="#DCE0E5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/icons/windows.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -12,6 +12,7 @@ import { useSelector } from 'react-redux'
import PinnedNotification from './components/General/PinnedNottification'
import { RootState } from './redux/store'
import CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage'
import ValidatorOnboarding from './pages/ValidatorOnboarding/ValidatorOnboarding'
const router = createBrowserRouter([
{
@ -35,6 +36,7 @@ const router = createBrowserRouter([
element: <PairDevice />,
},
{ path: '/create-local-node', element: <CreateLocalNodePage /> },
{ path: '/validator-onboarding', element: <ValidatorOnboarding /> },
])
function App() {

View File

@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react'
import BorderBox from './BorderBox'
const meta = {
title: 'General/BorderBox',
component: BorderBox,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof BorderBox>
export default meta
type Story = StoryObj<typeof meta>
export const WithData: Story = {
args: {
children: 'BorderBox',
},
}
export const WithoutData: Story = {
args: {
children: '',
},
}

View File

@ -0,0 +1,18 @@
import { Stack } from 'tamagui'
type BorderBoxProps = {
children: React.ReactNode
style?: React.CSSProperties
}
const BorderBox = ({ children, style }: BorderBoxProps) => {
return (
<Stack
style={{ border: '1px solid #DCE0E5', borderRadius: '16px', padding: '6px 12px', ...style }}
>
{children}
</Stack>
)
}
export default BorderBox

View File

@ -7,7 +7,7 @@ type LabelInputProps = {
placeholderText: string
}
function LabelInputField({ labelText, placeholderText }: LabelInputProps) {
const LabelInputField = ({ labelText, placeholderText }: LabelInputProps) => {
return (
<Label flexDirection="column" alignItems="flex-start" my={10} width={'100%'}>
<Text size={13} weight="regular" color={'#647084'}>

View File

@ -0,0 +1,74 @@
import type { Meta, StoryObj } from '@storybook/react'
import LinkWithArrow from './LinkWithArrow'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'General/LinkWithArrow',
component: LinkWithArrow,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof LinkWithArrow>
export default meta
type Story = StoryObj<typeof meta>
export const LeftArrow: Story = {
args: {
text: 'Learn More',
to: '/',
arrowLeft: true,
},
}
export const RightArrow: Story = {
args: {
text: 'Learn More',
to: '/',
arrowRight: true,
},
}
export const BothArrows: Story = {
args: {
text: 'Learn More',
to: '/',
arrowLeft: true,
arrowRight: true,
},
}
export const WithoutArrow: Story = {
args: {
text: 'Learn More',
to: '/',
},
}
export const WithoutText: Story = {
args: {
text: '',
to: '/',
arrowLeft: true,
},
}
export const WithLongText: Story = {
args: {
text: 'This is a very long text that is used to test the component.',
to: '/',
arrowLeft: true,
},
}
export const WithCustomStyle: Story = {
args: {
text: 'Learn More',
to: '/',
arrowLeft: true,
style: { backgroundColor: 'lightgray' },
},
}

View File

@ -0,0 +1,48 @@
import { Link, useNavigate } from 'react-router-dom'
import { XStack } from 'tamagui'
import { ArrowLeftIcon, ArrowRightIcon } from '@status-im/icons'
type LinkWithArrowProps = {
text: string
to: string
arrowLeft?: boolean
arrowRight?: boolean
style?: React.CSSProperties
textColor?: string
}
const LinkWithArrow = ({
text,
to,
arrowLeft,
arrowRight,
style,
textColor,
}: LinkWithArrowProps) => {
const navigate = useNavigate()
const navigateHandler = () => {
navigate(to)
}
return (
<XStack
space={'$1.5'}
style={{
alignItems: 'center',
maxWidth: 'fit-content',
cursor: 'pointer',
...style,
}}
onClick={navigateHandler}
>
{arrowLeft && <ArrowLeftIcon size={20} color="#2A4CF4" />}
<Link style={{ color: textColor || '#2A4CF4', marginBottom: '2px' }} to={to}>
{text}
</Link>
{arrowRight && <ArrowRightIcon size={20} color="#2A4CF4" />}
</XStack>
)
}
export default LinkWithArrow

View File

@ -1,16 +0,0 @@
import { Text } from '@status-im/components'
type SubTitleProps = {
color?: string
children: string
}
const SubTitle = ({ color, children }: SubTitleProps) => {
return (
<Text size={15} color={color}>
{children}
</Text>
)
}
export default SubTitle

View File

@ -1,16 +0,0 @@
import { Text } from '@status-im/components'
type TitleProps = {
color?: string
children: string
}
const Title = ({ color, children }: TitleProps) => {
return (
<Text size={27} weight={'semibold'} color={color}>
{children}
</Text>
)
}
export default Title

View File

@ -1,28 +1,35 @@
import { XStack, YStack } from 'tamagui'
import { Button, Text } from '@status-im/components'
import Title from './Title'
import { RevealIcon } from '@status-im/icons'
type TitlesProps = {
title: string
subtitle: string
isAdvancedSettings?: boolean
titleSize?: 27 | 15 | 11 | 13 | 19
subtitleSize?: 27 | 15 | 11 | 13 | 19
}
const Titles = ({ title, subtitle, isAdvancedSettings }: TitlesProps) => {
const Titles = ({
title,
subtitle,
isAdvancedSettings,
titleSize = 27,
subtitleSize = 15,
}: TitlesProps) => {
return (
<YStack style={{ width: '100%', margin: '0 0 1em' }}>
<XStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Title color={'#09101C'}>{title}</Title>
<Text size={titleSize} weight={'semibold'}>
{title}
</Text>
{isAdvancedSettings && (
<Button size={32} variant="outline" icon={<RevealIcon size={20} />}>
Advanced Settings
</Button>
)}
</XStack>
<Text size={15} weight="regular">
{subtitle}
</Text>
<Text size={subtitleSize}>{subtitle}</Text>
</YStack>
)
}

View File

@ -26,6 +26,7 @@ const PageWrapperShadow = ({
<div className="container-inner">{children}</div>
</div>
</section>
<section className="layout-right">
<div className="image-container">
<img src={rightImageSrc} alt="background" className="background-img" />

View File

@ -9,3 +9,9 @@ export const BAD_STORAGE_TEXT =
export const BAD_CPU_CLOCK_RATE_TEXT = 'Your CPU clock rate is below the recommended 2.4GHz.'
export const BAD_RAM_MEMORY_TEXT = 'There is insufficient RAM required for selected services.'
export const BAD_NETWORK_TEXT = 'Network Latency is high.'
/* Validator Onboarding */
export const KEYSTORE_FILES = 'KeystoreFiles'
export const RECOVERY_PHRASE = 'Recovery Phrase'
export const BOTH_KEY_AND_RECOVERY = 'Both KeystoreFiles & Recovery Phrase'

View File

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client'
import { Provider as ReduxProvider } from 'react-redux'
import App from './App.tsx'
import './index.css'
import store from './redux/store.tsx'
import store from './redux/store.ts'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>

View File

@ -10,7 +10,6 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
decorators: [withRouter],
} satisfies Meta<typeof ConnectDevicePage>

View File

@ -10,7 +10,6 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
decorators: [withRouter],
} satisfies Meta<typeof CreateLocalNodePage>

View File

@ -1,18 +1,11 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Provider as ReduxProvider } from 'react-redux'
import store from '../../redux/store'
import DeviceHealthCheck from './DeviceHealthCheck'
const meta: Meta = {
title: 'Pages/DeviceHealthCheck',
component: DeviceHealthCheck,
decorators: [
StoryObj => (
<ReduxProvider store={store}>
<StoryObj />
</ReduxProvider>
),
],
decorators: [],
tags: ['autodocs'],
}

View File

@ -1,7 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Provider as ReduxProvider } from 'react-redux'
import store from '../../redux/store'
import DeviceSyncStatus from './DeviceSyncStatus'
const meta = {
@ -13,11 +11,9 @@ const meta = {
tags: ['autodocs'],
decorators: [
Story => (
<ReduxProvider store={store}>
<div style={{ height: '100%', width: '100%' }}>
<Story />
</div>
</ReduxProvider>
<div style={{ height: '100%', width: '100%' }}>
<Story />
</div>
),
],
} satisfies Meta<typeof DeviceSyncStatus>

View File

@ -13,6 +13,7 @@ import { useEffect } from 'react'
const DeviceSyncStatus = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(
setPinnedMessage({
@ -22,6 +23,7 @@ const DeviceSyncStatus = () => {
}),
)
}, [dispatch])
return (
<PageWrapperShadow rightImageSrc="./background-images/sync-status-background.png">
<YStack
@ -38,8 +40,8 @@ const DeviceSyncStatus = () => {
subtitle="Monitor your Validator Client and Beacon Node syncing progression."
/>
<YStack style={{ width: '100%' }}>
<SyncStatusCardExecution synced={132432} total={200000} />
<SyncStatusCardConsensus synced={149500} total={160000} />
<SyncStatusCardExecution synced={132.432} total={200.0} />
<SyncStatusCardConsensus synced={149.5} total={160.0} />
</YStack>
<Stack style={{ marginTop: '1rem' }}>
<Button>Continue</Button>

View File

@ -5,33 +5,31 @@ import Icon from '../../components/General/Icon'
import StandardGauge from '../../components/Charts/StandardGauge'
import IconText from '../../components/General/IconText'
import { TokenIcon } from '@status-im/icons'
import { formatNumberForGauge } from '../../utilities'
interface DeviceStorageHealthProps {
synced: number
total: number
}
const SyncStatusCardConsensus: React.FC<DeviceStorageHealthProps> = ({ synced, total }) => {
const message = synced === total ? 'Synced all data' : 'Syncing'
const data = () => {
return [
{
id: 'storage',
label: 'Used',
value: synced,
color: '#ff6161',
},
{
id: 'storage',
label: 'Free',
value: total - synced || 1,
color: '#E7EAEE',
},
]
}
const formatNumber = (n: number): string => {
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
const data = [
{
id: 'storage',
label: 'Used',
value: synced,
color: '#ff6161',
},
{
id: 'storage',
label: 'Free',
value: total - synced || 1,
color: '#E7EAEE',
},
]
return (
<Shadow
variant="$2"
@ -63,7 +61,7 @@ const SyncStatusCardConsensus: React.FC<DeviceStorageHealthProps> = ({ synced, t
width: '115px',
}}
>
<StandardGauge data={data()} />
<StandardGauge data={data} />
</Stack>
</XStack>
</YStack>
@ -74,7 +72,7 @@ const SyncStatusCardConsensus: React.FC<DeviceStorageHealthProps> = ({ synced, t
<XStack space={'$2'} style={{ padding: '10px 16px 10px 16px' }}>
<IconText icon={<TokenIcon size={16} />}>{message}</IconText>
<Text size={13}>
{formatNumber(synced)} / {formatNumber(total)}
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
</Text>
</XStack>
</YStack>

View File

@ -3,6 +3,7 @@ import { Shadow, Text } from '@status-im/components'
import StandardGauge from '../../components/Charts/StandardGauge'
import IconText from '../../components/General/IconText'
import { TokenIcon } from '@status-im/icons'
import { formatNumberForGauge } from '../../utilities'
interface DeviceStorageHealthProps {
synced: number
@ -27,9 +28,6 @@ const SyncStatusCardExecution: React.FC<DeviceStorageHealthProps> = ({ synced, t
},
]
}
const formatNumber = (n: number): string => {
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
return (
<Shadow
@ -75,8 +73,7 @@ const SyncStatusCardExecution: React.FC<DeviceStorageHealthProps> = ({ synced, t
<XStack space={'$2'} style={{ padding: '10px 16px 10px 16px' }}>
<IconText icon={<TokenIcon size={16} />}>{message}</IconText>
<Text size={13}>
{' '}
{formatNumber(synced)} / {formatNumber(total)}
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
</Text>
</XStack>
</YStack>

View File

@ -10,7 +10,6 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
decorators: [withRouter],
} satisfies Meta<typeof LandingPage>

View File

@ -1,7 +1,6 @@
import './LandingPage.css'
import { XStack, YStack } from 'tamagui'
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
import Title from '../../components/General/Title'
import NimbusLogo from '../../components/Logos/NimbusLogo'
import { NodeIcon } from '@status-im/icons'
import { Button as StatusButton, Text } from '@status-im/components'
@ -23,10 +22,10 @@ const LandingPage = () => {
<NimbusLogo />
</XStack>
<YStack style={{ width: '100%', margin: '30vh 0 4vh' }}>
<Title color="$textPrimary">
<Text size={27} weight={'semibold'}>
Light and performant clients, for all Ethereum validators.
</Title>
<Text size={15} weight="regular" color="$textPrimary">
</Text>
<Text size={15} weight="regular">
<strong>Nimbus Nodes</strong> allows you to take control and ownership of the services
you wish to run in a completely trustless and decentralized manner.
</Text>

View File

@ -10,7 +10,6 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
decorators: [withRouter],
} satisfies Meta<typeof GenerateId>

View File

@ -10,7 +10,6 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
decorators: [withRouter],
} satisfies Meta<typeof PairDevice>

View File

@ -9,7 +9,6 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof PairedSuccessfully>
export default meta

View File

@ -10,14 +10,13 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
decorators: [withRouter],
} satisfies Meta<typeof SyncStatus>
export default meta
type Story = StoryObj<typeof meta>
export const Page: Story = {
export const Default: Story = {
args: {
isPairing: true,
},

View File

@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react'
import Activation from './Activation'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'ValidatorOnboarding/Activation',
component: Activation,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof Activation>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
validatorsValue: '4',
executionSyncStatus1: {
text: "Execution Sync Status",
isGaugeIncluded: true,
gaugeColor: "$blue",
gaugeSynced: 123.524,
gaugeTotal: 172.503,
},
executionSyncStatus2: {
text: "Execution Sync Status",
isGaugeIncluded: true,
gaugeColor: "$red",
gaugeSynced: 123.524,
gaugeTotal: 172.503,
},
currentAPRValue: "4.40%",
estimatedActivationTimeValue: "32 Days",
validatorQueueValue: "92603",
},
}

View File

@ -0,0 +1,107 @@
import { useState, useEffect } from 'react'
import { XStack, Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import Confetti from 'react-confetti'
import ActivationCard from './ActivationCard'
import LinkWithArrow from '../../../components/General/LinkWithArrow'
type ActivationProps = {
validatorsValue: string
executionSyncStatus1: {
text: string
isGaugeIncluded: boolean
gaugeColor: string
gaugeSynced: number
gaugeTotal: number
}
executionSyncStatus2: {
text: string
isGaugeIncluded: boolean
gaugeColor: string
gaugeSynced: number
gaugeTotal: number
}
currentAPRValue: string
estimatedActivationTimeValue: string
validatorQueueValue: string
}
const Activation = ({
validatorsValue,
executionSyncStatus1,
executionSyncStatus2,
currentAPRValue,
estimatedActivationTimeValue,
validatorQueueValue,
}: ActivationProps) => {
const [showConfetti, setShowConfetti] = useState(true)
useEffect(() => {
const timer = setTimeout(() => {
setShowConfetti(false)
}, 10000)
return () => {
clearTimeout(timer)
}
}, [])
return (
<Stack style={styles.confettiContainer} width={'100%'}>
{showConfetti && <Confetti style={styles.confettiCanvas} />}
<YStack style={{ padding: '16px 32px' }}>
<YStack space={'$5'}>
<Text size={27} weight={'semibold'}>
Activation
</Text>
<Stack style={{ width: '66%' }}>
<Text size={27}>
Congratulations! You have successfully setup your Nimbus Validators and are currently
syncing your nodes.
</Text>
</Stack>
<YStack space={'$3'} marginTop={'10px'} width={'33%'}>
<XStack space={'$3'} justifyContent={'space-between'}>
<ActivationCard text="Validators" value={validatorsValue} />
<ActivationCard {...executionSyncStatus1} />
<ActivationCard {...executionSyncStatus2} />
</XStack>
<XStack space={'$3'}>
<ActivationCard text="Current APR" value={currentAPRValue} />
<ActivationCard
text="Estimated Activation Time"
value={estimatedActivationTimeValue}
/>
<ActivationCard text="Validator Queue" value={validatorQueueValue} />
</XStack>
</YStack>
</YStack>
<LinkWithArrow
text="Edit Validators"
to="/"
arrowLeft={true}
style={{ marginTop: '44px', marginBottom: '88px' }}
/>
</YStack>
</Stack>
)
}
export default Activation
const styles = {
confettiContainer: {
position: 'relative' as const,
width: '100%',
height: '100%',
},
confettiCanvas: {
position: 'absolute' as const,
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 0,
},
}

View File

@ -0,0 +1,80 @@
import type { Meta, StoryObj } from '@storybook/react'
import ActivationCard from './ActivationCard'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'ValidatorOnboarding/ActivationCard',
component: ActivationCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof ActivationCard>
export default meta
type Story = StoryObj<typeof meta>
export const Validators: Story = {
args: {
text: 'Validators',
value: '4',
},
}
export const ExecutionSyncStatus: Story = {
args: {
text: 'Execution Sync Status',
value: '',
isGaugeIncluded: true,
gaugeColor: '#2a4af5',
gaugeSynced: 123.524,
gaugeTotal: 172.503,
},
}
export const ExecutionSyncStatusRed: Story = {
args: {
text: 'Execution Sync Status',
value: '',
isGaugeIncluded: true,
gaugeColor: '#EB5757',
gaugeSynced: 123.524,
gaugeTotal: 172.503,
},
}
export const CurrentAPR: Story = {
args: {
text: 'Current APR',
value: '4.40%',
},
}
export const EstimatedActivationTime: Story = {
args: {
text: 'Estimated Activation Time',
value: '32 Days',
},
}
export const ValidatorQueue: Story = {
args: {
text: 'Validator Queue',
value: '92603',
},
}
export const WithoutTitle: Story = {
args: {
text: '',
value: '1',
},
}
export const WithoutValue: Story = {
args: {
text: 'Title',
},
}

View File

@ -0,0 +1,58 @@
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import ActivationSyncCard from './ActivationSyncCard'
type ActivationCardProps = {
text: string
value?: string
isGaugeIncluded?: boolean
gaugeColor?: string
gaugeSynced?: number
gaugeTotal?: number
}
const ActivationCard = ({
text,
value,
isGaugeIncluded,
gaugeColor,
gaugeSynced,
gaugeTotal,
}: ActivationCardProps) => {
return (
<YStack
style={{
borderRadius: '16px',
border: '1px solid rgba(0, 0, 0, 0.15)',
padding: '12px 16px',
backgroundColor: '#FFF',
width: '100%',
}}
>
{!isGaugeIncluded && (
<Stack>
<Text size={15} weight={'semibold'}>
{text}
</Text>
<Text size={27} color="blue" weight={'semibold'}>
{value}
</Text>
</Stack>
)}
{isGaugeIncluded && (
<Stack>
<Text size={15} weight={'semibold'}>
{text}
</Text>
<ActivationSyncCard
gaugeColor={gaugeColor || ''}
gaugeSynced={gaugeSynced || 0}
gaugeTotal={gaugeTotal || 1}
/>
</Stack>
)}
</YStack>
)
}
export default ActivationCard

View File

@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import ActivationSyncCard from './ActivationSyncCard'
const meta = {
title: 'ValidatorOnboarding/ActivationSyncCard',
component: ActivationSyncCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof ActivationSyncCard>
export default meta
type Story = StoryObj<typeof meta>
export const Blue: Story = {
args: {
gaugeColor: '#2a4af5',
gaugeSynced: 123.524,
gaugeTotal: 172.503,
},
}
export const Red: Story = {
args: {
gaugeColor: '#EB5757',
gaugeSynced: 123.524,
gaugeTotal: 172.503,
},
}
export const MaxValue: Story = {
args: {
gaugeColor: '#2a4af5',
gaugeSynced: 172.503,
gaugeTotal: 172.503,
},
}
export const OverMaxValue: Story = {
args: {
gaugeColor: '#2a4af5',
gaugeSynced: 200,
gaugeTotal: 172.503,
},
}
export const MinValue: Story = {
args: {
gaugeColor: '#2a4af5',
gaugeSynced: 0,
gaugeTotal: 172.503,
},
}
export const UnderMinValue: Story = {
args: {
gaugeColor: '#2a4af5',
gaugeSynced: -100,
gaugeTotal: 172.503,
},
}

View File

@ -0,0 +1,47 @@
import { Stack, XStack, YStack } from 'tamagui'
import StandardGauge from '../../../components/Charts/StandardGauge'
import { Text } from '@status-im/components'
import { formatNumberForGauge } from '../../../utilities'
type ActivationSyncCardProps = {
gaugeColor: string
gaugeSynced: number
gaugeTotal: number
}
const ActivationSyncCard = ({ gaugeColor, gaugeSynced, gaugeTotal }: ActivationSyncCardProps) => {
return (
<XStack space={'$2'} alignItems="center">
<Stack
style={{
height: '35px',
width: '35px',
}}
>
<StandardGauge
data={[
{
id: 'sync card',
label: 'Sync Status',
value: gaugeSynced,
color: gaugeColor,
},
{
id: 'free',
label: 'free',
value: gaugeTotal - gaugeSynced || 1,
color: '#E7EAEE',
},
]}
/>
</Stack>
<YStack>
<Text size={15} weight={'semibold'}>
{formatNumberForGauge(gaugeSynced)} / {formatNumberForGauge(gaugeTotal)}
</Text>
</YStack>
</XStack>
)
}
export default ActivationSyncCard

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import Advisories from './Advisories'
const meta = {
title: 'ValidatorOnboarding/Advisories',
component: Advisories,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof Advisories>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,92 @@
import { Text } from '@status-im/components'
import { useState } from 'react'
import { Stack, XStack, YStack } from 'tamagui'
import AdvisoriesContent from './AdvisoriesContent'
type AdvisoryTopicsType = {
[key: string]: string[]
}
const Advisories = () => {
const [selectedTitle, setSelectedTitle] = useState(Object.keys(advisoryTopics)[3])
const isSameTitle = (title: string) => selectedTitle === title
return (
<XStack
style={{ padding: '16px 32px', justifyContent: 'space-between' }}
height={'65vh'}
width={'100%'}
>
<YStack space={'$2'}>
<Stack marginBottom="$6">
<Text size={27} weight={'semibold'}>
Advisories
</Text>
</Stack>
{Object.keys(advisoryTopics).map((title, index) => (
<XStack
key={title}
onPress={() => setSelectedTitle(title)}
style={{ cursor: 'pointer', alignItems: 'center' }}
space={'$2'}
>
<Text
size={27}
weight={isSameTitle(title) && 'semibold'}
color={isSameTitle(title) ? 'blue' : ''}
>
{unicodeNumbers[index]}
</Text>
<Text
size={19}
weight={isSameTitle(title) && 'semibold'}
color={isSameTitle(title) ? 'blue' : ''}
>
{title}
</Text>
</XStack>
))}
</YStack>
<AdvisoriesContent title={selectedTitle} content={advisoryTopics[selectedTitle]} />
</XStack>
)
}
export default Advisories
const unicodeNumbers = ['➀', '➁', '➂', '➃', '➄', '➅']
export const advisoryTopics: AdvisoryTopicsType = {
'Proof of Stake': [
'Proof of Stake systems require validators to hold and lock up a certain amount of cryptocurrency to participate.',
'In Proof of Stake, the chances of creating a block is proportional to the amount of cryptocurrency held.',
'Unlike Proof of Work, Proof of Stake aims to achieve consensus without intensive computational work.',
],
Deposit: [
'Deposits are often irreversible, so ensure to double-check transaction details before confirming.',
'Delay in deposit acknowledgment might be due to network congestion or node synchronization.',
'Always keep transaction IDs or hashes for records and future references in case of disputes.',
],
'Key Management': [
'Storing your private keys on a device connected to the internet is susceptible to hacks and malware.',
'Hardware wallets provide an added layer of security by keeping private keys isolated from online systems.',
'Regularly back up and encrypt your key management solutions to prevent potential losses.',
],
'Bad Behaviour': [
'If you try to cheat the system, or act contrary to the specification, you will be liable to incur a penalty known as slashing.',
'Running your validator keys simultaneously on two or more machines will result in slashing.*',
'Simply being offline with an otherwise healthy network does not result in slashing, but will result in small inactivity penalties.',
],
Requirements: [
'Ensure your system meets the minimum software and hardware requirements before initiating any operations.',
'Staying updated with the latest versions is vital to maintain system integrity and performance.',
'Failure to meet requirements might result in operational inefficiencies or security vulnerabilities.',
],
Risks: [
'Cryptocurrency investments are subject to high volatility and can result in both significant gains and losses.',
'Always do thorough research before making investment decisions or engaging in transactions.',
'Be wary of phishing scams, malicious software, and too-good-to-be-true offers.',
],
}

View File

@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import AdvisoriesContent from './AdvisoriesContent'
import { advisoryTopics } from './Advisories'
const meta = {
title: 'ValidatorOnboarding/AdvisoriesContent',
component: AdvisoriesContent,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof AdvisoriesContent>
export default meta
type Story = StoryObj<typeof meta>
const advisoryTopicsKeys = Object.keys(advisoryTopics)
const advisoryTopicsValues = Object.values(advisoryTopics)
export const ProofOfStake: Story = {
args: {
title: advisoryTopicsKeys[0],
content: advisoryTopicsValues[0],
},
}
export const Deposit: Story = {
args: {
title: advisoryTopicsKeys[1],
content: advisoryTopicsValues[1],
},
}
export const KeyManagement: Story = {
args: {
title: advisoryTopicsKeys[2],
content: advisoryTopicsValues[2],
},
}
export const BadBehaviour: Story = {
args: {
title: advisoryTopicsKeys[3],
content: advisoryTopicsValues[3],
},
}
export const Requirements: Story = {
args: {
title: advisoryTopicsKeys[4],
content: advisoryTopicsValues[4],
},
}
export const Risks: Story = {
args: {
title: advisoryTopicsKeys[5],
content: advisoryTopicsValues[5],
},
}

View File

@ -0,0 +1,45 @@
import { Text } from '@status-im/components'
import { Link } from 'react-router-dom'
import { Stack, YStack } from 'tamagui'
type AdvisoriesContentProps = {
title: string
content: string[]
}
const AdvisoriesContent = ({ title, content }: AdvisoriesContentProps) => {
return (
<YStack space={'$1'} style={{ width: '70%' }}>
<Stack style={{ marginBottom: '5%' }}>
<Text size={27} weight={400}>
{title}
</Text>
</Stack>
<YStack space={'$4'}>
{content.map(row => (
<Text key={row} size={19}>
{row}
</Text>
))}
<Text size={19}>
<Link
to={'https://github.com/ethereum/consensus-specs'}
style={{ textDecorationLine: 'underline', color: '#484848' }}
>
The Ethereum consensus layer specification
</Link>
</Text>
<Text size={19} weight={'semibold'}>
<Link
to={'https://github.com/ethereum/consensus-specs'}
style={{ textDecorationLine: 'underline', color: '#2A4CF4', fontWeight: 'bold' }}
>
More on slashing risks
</Link>
</Text>
</YStack>
</YStack>
)
}
export default AdvisoriesContent

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import ClientSetup from './ClientSetup'
const meta = {
title: 'ValidatorOnboarding/ClientSetup',
component: ClientSetup,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof ClientSetup>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,22 @@
import { Separator, YStack } from 'tamagui'
import SetupRow from './SetupRow'
import WithdrawalAddress from './WithdrawalAddress'
import LinkWithArrow from '../../../components/General/LinkWithArrow'
const ClientSetup = () => {
return (
<YStack padding={'26px'} width={'100%'} space={'$5'}>
<SetupRow title={'Setup up Validators'} />
<Separator borderColor={'#F0F2F5'} />
<WithdrawalAddress title={'Withdrawal address'} />
<LinkWithArrow
text="Advanced Recovery Method"
to={'/'}
arrowRight={true}
style={{ marginBottom: '50px' }}
/>
</YStack>
)
}
export default ClientSetup

View File

@ -0,0 +1,29 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import SetupRow from './SetupRow'
const meta = {
title: 'ValidatorOnboarding/SetupRow',
component: SetupRow,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof SetupRow>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
title: 'Setup up Validators',
},
}
export const WithoutTitle: Story = {
args: {
title: '',
},
}

View File

@ -0,0 +1,63 @@
import { Stack, XStack, YStack } from 'tamagui'
import { Input as StatusInput, Text } from '@status-im/components'
import { AddIcon, ChevronDownIcon } from '@status-im/icons'
import { useState } from 'react'
type SetupRowProps = {
title: string
}
const SetupRow = ({ title }: SetupRowProps) => {
const [validatorCount, setValidatorCount] = useState(0)
const addValidatorHandler = () => {
setValidatorCount((state: number) => state + 1)
}
const changeValidatorCountHandler = (e: any) => {
if (!isNaN(e.target.value)) {
setValidatorCount(Number(e.target.value))
}
}
return (
<YStack space={'$4'}>
<Text size={27} weight={'semibold'}>
{title}
</Text>
<XStack justifyContent={'space-between'} width={'80%'}>
<Stack space={'$2'}>
<Text size={15} weight="regular" color={'#647084'}>
How many Validators would you like to run?
</Text>
<StatusInput
icon={<AddIcon size={16} style={{ cursor: 'pointer' }} onClick={addValidatorHandler} />}
value={validatorCount.toString()}
onChange={changeValidatorCountHandler}
style={{ fontWeight: 'bold' }}
/>
</Stack>
<YStack space={'$2'}>
<Text size={19} weight={'semibold'} color="#09101C">
ETH
</Text>
<Text size={27} weight={'semibold'} color="#09101C">
64
</Text>
</YStack>
<YStack space={'$2'}>
<XStack style={{ justifyContent: 'space-between' }}>
<Text size={19} weight={'semibold'} color="#09101C">
USD
</Text>
<ChevronDownIcon size={16} color={'#919191'} />
</XStack>
<Text size={27} weight={'semibold'} color="#09101C">
$4,273 USD
</Text>
</YStack>
</XStack>
</YStack>
)
}
export default SetupRow

View File

@ -0,0 +1,29 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import WithdrawalAddress from './WithdrawalAddress'
const meta = {
title: 'ValidatorOnboarding/WithdrawalAddress',
component: WithdrawalAddress,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof WithdrawalAddress>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
title: 'Withdrawal Address',
},
}
export const WithoutTitle: Story = {
args: {
title: '',
},
}

View File

@ -0,0 +1,55 @@
import { Stack, YStack } from 'tamagui'
import { InformationBox, Input as StatusInput, Text } from '@status-im/components'
import { ClearIcon, CloseCircleIcon } from '@status-im/icons'
import { useState } from 'react'
type WithdrawalAddressProps = {
title: string
}
const WithdrawalAddress = ({ title }: WithdrawalAddressProps) => {
const [withdrawalAddress, setWithdrawalAddress] = useState('')
const changeWithdrawalAddressHandler = (e: any) => {
setWithdrawalAddress(e.target.value)
}
const removeWithdrawalAddressHandler = () => {
setWithdrawalAddress('')
}
return (
<YStack space={'$4'}>
<Text size={27} weight={'semibold'}>
{title}
</Text>
<YStack space={'$3'}>
<Text size={13} weight="regular" color={'#647084'}>
Ethereum Address
</Text>
<Stack width={'100%'}>
<StatusInput
placeholder={'******************'}
width={'100%'}
icon={
<ClearIcon
size={16}
style={{ cursor: 'pointer' }}
onClick={removeWithdrawalAddressHandler}
/>
}
value={withdrawalAddress}
onChange={changeWithdrawalAddressHandler}
/>
</Stack>
<InformationBox
message="If withdrawal address is not provided at this step, your deposited funds will remain locked on the Beacon Chain until an address is provided. Unlocking will require signing a message with your withdrawal keys, generated from your mnemonic seed phrase (so keep it safe)."
variant="error"
icon={<CloseCircleIcon size={20} color="$red" />}
/>
</YStack>
</YStack>
)
}
export default WithdrawalAddress

View File

@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import ContinueButton from './ContinueButton'
const meta = {
title: 'ValidatorOnboarding/ContinueButton',
component: ContinueButton,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof ContinueButton>
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,
},
}

View File

@ -0,0 +1,80 @@
import { Stack, XStack } from 'tamagui'
import { Button, InformationBox } from '@status-im/components'
import { CloseCircleIcon } from '@status-im/icons'
import { useDispatch, useSelector } from 'react-redux'
import { useEffect } from 'react'
import { RootState } from '../../redux/store'
import { setIsRightPhrase } from '../../redux/ValidatorOnboarding/KeyGeneration/slice'
import LinkWithArrow from '../../components/General/LinkWithArrow'
type ContinueButton = {
continueHandler: () => void
activeStep: number
isConfirmPhraseStage: boolean
subStepValidatorSetup: number
}
const ContinueButton = ({
continueHandler,
activeStep,
isConfirmPhraseStage,
subStepValidatorSetup,
}: ContinueButton) => {
const { isCopyPastedPhrase, isRightPhrase, words, validWords } = useSelector(
(state: RootState) => state.keyGeneration,
)
const dispatch = useDispatch()
useEffect(() => {
dispatch(setIsRightPhrase(words.every(word => word !== '')))
}, [words])
const isDisabled = () => {
if (
(isConfirmPhraseStage && !isRightPhrase) ||
(isConfirmPhraseStage && validWords.some(w => w === false))
) {
return true
}
return false
}
const isActivationValScreen = activeStep === 3 && subStepValidatorSetup === 3
return (
<XStack style={{ width: '100%', alignItems: 'center', zIndex: 999, marginTop: '40px' }}>
<Stack style={{ width: '100%' }}>
{isCopyPastedPhrase && (
<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} />}
/>
)}
{isActivationValScreen && (
<LinkWithArrow
text="Skip to Dashboard"
to="/"
arrowRight={true}
style={{ fontWeight: 'bold', zIndex: 1000 }}
/>
)}
</Stack>
<Stack
style={{
width: '100%',
zIndex: 999,
alignItems: 'end',
position: 'absolute',
}}
>
<Button onPress={continueHandler} size={40} disabled={isDisabled()}>
{activeStep < 5 ? 'Continue' : 'Continue to Dashboard'}
</Button>
</Stack>
</XStack>
)
}
export default ContinueButton

View File

@ -0,0 +1,68 @@
.custom-step {
background-color: #ffffff;
}
.custom-step.StepMain--active,
.custom-step.StepMain--completed {
background-color: #2a4cf4;
}
.custom-step::before {
content: '';
display: block;
position: absolute;
width: 20px;
height: 20px;
border: 2px solid #e0e0e0;
border-radius: 50%;
}
.custom-step.StepMain--active::before,
.custom-step.StepMain--completed::before {
border-color: #2a4cf4;
}
.custom-step.StepMain--active::after {
content: '';
display: block;
position: absolute;
width: 20px;
height: 20px;
background-color: #2a4cf4;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.custom-step::after {
content: attr(data-subtitle);
position: absolute;
top: calc(100% + 4px);
font-size: 12px;
color: #A2A9B0;
background: transparent;
border: none;
}
[data-step="Overview"]::after {
left: 35%;
}
[data-step="Advisories"]::after {
left: 30%;
}
[data-step="Client Setup"]::after {
left: 32%;
}
[data-step="Validator Setup"]::after {
left: 25%;
}
[data-step="Key Generation"]::after {
left: 24.5%;
}
[data-step="Activation"]::after {
left: 33%;
}

View File

@ -0,0 +1,64 @@
import type { Meta, StoryObj } from '@storybook/react'
import FormStepper from './FormStepper'
const meta = {
title: 'ValidatorOnboarding/FormStepper',
component: FormStepper,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof FormStepper>
export default meta
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

@ -0,0 +1,70 @@
import { Stepper, Step } from 'react-form-stepper'
import './FormStepper.css'
type FormStepperProps = {
activeStep: number
changeActiveStep: (step: number) => void
}
const steps = [
{ label: 'Overview', subtitle: 'Get Started' },
{ label: 'Advisories', subtitle: 'Understand your Duties' },
{ label: 'Client Setup', subtitle: 'Execution & Consensus' },
{ label: 'Validator Setup', subtitle: 'Validators & Withdrawal' },
{ label: 'Key Generation', subtitle: 'Secure your Keypairs' },
{ label: 'Activation', subtitle: 'Complete Setup' },
]
const FormStepper = ({ activeStep, changeActiveStep }: FormStepperProps) => {
return (
<Stepper
activeStep={activeStep}
nonLinear={true}
styleConfig={stepStyle}
connectorStyleConfig={customConnectorStyle}
style={{ fontSize: '14px', zIndex: 999, width: '100%', padding: 0, marginBottom: '2rem' }}
>
{steps.map((step, index) => (
<Step
key={index}
label={step.label}
className="custom-step"
onClick={() => changeActiveStep(index)}
completed={activeStep > index - 1}
data-subtitle={step.subtitle}
data-step={step.label}
/>
))}
</Stepper>
)
}
const stepStyle = {
// For default dots:
inactiveBgColor: '#FFFFFF',
inactiveBorderColor: '#E0E0E0',
inactiveBorderWidth: '2px',
// For active dots:
activeBgColor: '#FFFFFF',
activeBorderColor: '#2A4CF4',
activeBorderWidth: '2px',
// For completed dots:
completedBgColor: '#2A4CF4',
activeTextColor: '#ffffff',
completedTextColor: '#ffffff',
inactiveTextColor: '#000000',
size: '20px',
circleFontSize: '10px',
labelFontSize: '14px',
borderRadius: '50%',
fontWeight: 700,
}
const customConnectorStyle = {
size: '2px',
activeColor: '#2A4CF4',
disabledColor: '#bdbdbd',
completedColor: '#a10308',
style: 'solid',
}
export default FormStepper

View File

@ -0,0 +1,79 @@
.autocomplete-container {
position: relative;
width: 100%;
background-color: #f9fafa;
border: 2px solid #f9fafa;
}
.suggestion-list {
box-sizing: border-box;
overflow-y: scroll;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #f9fafa;
padding: 10px;
scrollbar-width: thin;
scrollbar-color: #f9fafa transparent;
border: '2px solid #f9fafa';
max-height: 250px;
position: absolute;
z-index: 1000;
width: 100%;
border-bottom-left-radius: 24px;
border-bottom-right-radius: 24px;
}
.autocomplete-input {
box-sizing: border-box;
border: 2px solid #f7f8f9;
border-radius: 24px;
padding: 12px;
background-color: #f7f8f9;
width: 100%;
font-size: 16px;
}
.input-wrapper {
position: relative;
}
.input-number {
position: absolute;
left: 15px;
transform: translateY(-50%);
pointer-events: none;
z-index: 2;
color: #0D162566;
top: 48%;
}
.suggestion-item {
padding: 12px 16px;
cursor: pointer;
border-radius: 24px;
font-weight: 500;
}
.suggestion-item:hover {
background-color: #f1f2f4;
}
.suggestion-list::-webkit-scrollbar {
width: 1px;
}
.suggestion-list::-webkit-scrollbar-thumb {
background-color: #f1f2f4;
border-radius: 2px;
}
.suggestion-list::-webkit-scrollbar-track {
background-color: transparent;
}
.suggestion-list::-webkit-scrollbar-thumb {
background-color: transparent;
}
.suggestion-list::-webkit-scrollbar-thumb:hover {
background-color: transparent;
}

View File

@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react'
import AutocompleteInput from './AutocompleteInput'
const meta = {
title: 'ValidatorOnboarding/AutocompleteInput',
component: AutocompleteInput,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof AutocompleteInput>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
index: 0,
},
}
export const OtherWord: Story = {
args: {
index: 2,
},
}

View File

@ -0,0 +1,151 @@
import React, { useState, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import wordlist from 'web-bip39/wordlists/english'
import { RootState } from '../../../../redux/store'
import {
setIsCopyPastedPhrase,
setMnemonic,
setValidWords,
setWord,
} from '../../../../redux/ValidatorOnboarding/KeyGeneration/slice'
import styles from './AutocompleteInput.module.css'
type AutocompleteInputProps = {
index: number
}
const AutocompleteInput = ({ index }: AutocompleteInputProps) => {
const [suggestions, setSuggestions] = useState<string[]>([])
const [isFocused, setIsFocused] = useState(false)
const word = useSelector((state: RootState) => state.keyGeneration.words[index])
const isValidWord = useSelector((state: RootState) => state.keyGeneration.validWords[index])
const validWords = useSelector((state: RootState) => state.keyGeneration.validWords)
const dispatch = useDispatch()
useEffect(() => {
setSuggestions(getNewSuggestions(word))
}, [word])
const getNewSuggestions = (word: string) => {
return wordlist.filter(w => w.startsWith(word.toLowerCase()))
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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))
}
dispatch(setValidWords(newValidWords))
}
const handleSuggestionClick = (e: React.MouseEvent, suggestion: string) => {
e.preventDefault()
setIsFocused(false)
dispatch(setWord({ index, word: suggestion }))
let newValidWords = [...validWords]
newValidWords[index] = wordlist.includes(suggestion)
dispatch(setValidWords(newValidWords))
}
const handleInputFocus = () => {
setIsFocused(true)
}
const handleInputBlur = () => {
setIsFocused(false)
let newValidWords = [...validWords]
newValidWords[index] = wordlist.includes(word)
dispatch(setValidWords(newValidWords))
}
return (
<div style={autocompleteContainerStyle(isFocused)} className={styles['autocomplete-container']}>
<div className={styles['input-wrapper']}>
<span className={styles['input-number']}>{index + 1}.</span>
<input
className={styles['autocomplete-input']}
value={word}
onChange={handleInputChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
style={inputStyle(index, isFocused, isValidWord)}
/>
</div>
<div className={isFocused ? styles['suggestion-list'] : ''}>
{isFocused &&
suggestions.map(suggestion => (
<div
key={suggestion}
className={styles['suggestion-item']}
onMouseDown={e => handleSuggestionClick(e, suggestion)}
>
{suggestion}
</div>
))}
</div>
</div>
)
}
export default AutocompleteInput
const inputStyle = (index: number, isFocused: boolean, isValidWord: boolean) => {
const style = {
outline: 'none',
padding: `12px 16px 12px ${index + 1 < 10 ? '35px' : '45px'}`,
border: isValidWord ? '2px solid #f7f8f9' : '2px solid #E53E3E',
}
if (isFocused) {
return {
...style,
border: isValidWord ? '2px solid #4360DF' : '2px solid #E53E3E',
backgroundColor: '#f1f2f4',
}
} else {
return style
}
}
const autocompleteContainerStyle = (isFocused: boolean) => {
if (isFocused) {
return {
borderTopLeftRadius: '24px',
borderTopRightRadius: '24px',
}
} else {
return {
borderRadius: '24px',
}
}
}

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ConfirmRecoveryPhrase from './ConfirmRecoveryPhrase'
const meta = {
title: 'ValidatorOnboarding/ConfirmRecoveryPhrase',
component: ConfirmRecoveryPhrase,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ConfirmRecoveryPhrase>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,33 @@
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import AutocompleteInput from './AutocompleteInput'
import KeyGenerationTitle from '../KeyGenerationTitle'
import { useSelector } from 'react-redux'
import { RootState } from '../../../../redux/store'
const ConfirmRecoveryPhrase = () => {
const { validWords } = useSelector((state: RootState) => state.keyGeneration)
return (
<YStack space={'$3'} style={{ width: '100%', marginTop: '20px' }}>
<KeyGenerationTitle />
<Text size={27}>Confirm Recovery Phrase</Text>
<Stack
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '20px 40px',
width: '72%',
marginBottom: '10px',
}}
>
{validWords.map((_, index) => (
<AutocompleteInput key={index} index={index} />
))}
</Stack>
</YStack>
)
}
export default ConfirmRecoveryPhrase

View File

@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react'
import KeyGeneration from './KeyGeneration'
const meta = {
title: 'ValidatorOnboarding/KeyGeneration',
component: KeyGeneration,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof KeyGeneration>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
isConfirmPhraseStage: false,
},
}
export const ConfirmPhraseStage: Story = {
args: {
isConfirmPhraseStage: true,
},
}

View File

@ -0,0 +1,52 @@
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import { useState } from 'react'
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'
type KeyGenerationProps = {
isConfirmPhraseStage: boolean
}
const KeyGeneration = ({ isConfirmPhraseStage }: KeyGenerationProps) => {
const [recoveryMechanism, setRecoveryMechanism] = useState(KEYSTORE_FILES)
const isKeystoreFiles =
recoveryMechanism === KEYSTORE_FILES || recoveryMechanism === BOTH_KEY_AND_RECOVERY
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}
/>
<Stack style={{ margin: '30px 0' }}>
<Text size={27} weight={'semibold'}>
4 Validators
</Text>
</Stack>
{isKeystoreFiles && <KeystoreFiles />}
{isRecoveryPhrase && <RecoveryPhrase isKeystoreFiles={isKeystoreFiles} />}
</>
)}
</YStack>
)
}
export default KeyGeneration

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import KeyGenerationHeader from './KeyGenerationHeader'
const meta = {
title: 'ValidatorOnboarding/KeyGenerationHeader',
component: KeyGenerationHeader,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof KeyGenerationHeader>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,28 @@
import { XStack } from 'tamagui'
import KeyGenerationSyncCard from './KeyGenerationSyncCard'
import KeyGenerationTitle from '../KeyGenerationTitle'
const KeyGenerationHeader = () => {
return (
<XStack style={{ width: '100%', alignItems: 'center', justifyContent: 'space-between' }}>
<KeyGenerationTitle />
<XStack space={'$2'}>
<KeyGenerationSyncCard
synced={123.524}
total={172.503}
title="Execution Sync Status"
color="#2a4af5"
/>
<KeyGenerationSyncCard
synced={123.524}
total={172.503}
title="Consensus Sync Status"
color="#ff6161"
/>
</XStack>
</XStack>
)
}
export default KeyGenerationHeader

View File

@ -0,0 +1,78 @@
import type { Meta, StoryObj } from '@storybook/react'
import KeyGenerationSyncCard from './KeyGenerationSyncCard'
const meta = {
title: 'ValidatorOnboarding/KeyGenerationSyncCard',
component: KeyGenerationSyncCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof KeyGenerationSyncCard>
export default meta
type Story = StoryObj<typeof meta>
export const Blue: Story = {
args: {
synced: 123.524,
total: 172.503,
title: 'Execution Sync Status',
color: '#2a4af5',
},
}
export const Red: Story = {
args: {
synced: 123.524,
total: 172.503,
title: 'Execution Sync Status',
color: '#ff6161',
},
}
export const MaxValue: Story = {
args: {
synced: 172.503,
total: 172.503,
title: 'Execution Sync Status',
color: '#2a4af5',
},
}
export const OverMaxValue: Story = {
args: {
synced: 200,
total: 172.503,
title: 'Execution Sync Status',
color: '#2a4af5',
},
}
export const MinValue: Story = {
args: {
synced: 0,
total: 172.503,
title: 'Execution Sync Status',
color: '#2a4af5',
},
}
export const UnderMinValue: Story = {
args: {
synced: -100,
total: 172.503,
title: 'Execution Sync Status',
color: '#2a4af5',
},
}
export const WithoutTitle: Story = {
args: {
synced: 123.524,
total: 172.503,
title: '',
color: '#2a4af5',
},
}

View File

@ -0,0 +1,57 @@
import { Stack, XStack, YStack } from 'tamagui'
import { ClearIcon } from '@status-im/icons'
import { Text } from '@status-im/components'
import StandardGauge from '../../../../components/Charts/StandardGauge'
import BorderBox from '../../../../components/General/BorderBox'
import { formatNumberForGauge } from '../../../../utilities'
type KeyGenerationSyncCardProps = {
synced: number
total: number
title: string
color: string
}
const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyncCardProps) => {
return (
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
<XStack space={'$2'} alignItems="center">
<Stack
style={{
height: '35px',
width: '35px',
}}
>
<StandardGauge
data={[
{
id: title,
label: title,
value: synced,
color: color,
},
{
id: 'free',
label: 'free',
value: total - synced || 1,
color: '#E7EAEE',
},
]}
/>
</Stack>
<YStack>
<Text size={11} color="#84888e" weight={'semibold'}>
{title}
</Text>
<Text size={15} weight={'semibold'}>
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
</Text>
</YStack>
<ClearIcon size={20} color="#A1ABBD" />
</XStack>
</BorderBox>
)
}
export default KeyGenerationSyncCard

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import KeyGenerationTitle from './KeyGenerationTitle'
const meta = {
title: 'ValidatorOnboarding/KeyGenerationTitle',
component: KeyGenerationTitle,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof KeyGenerationTitle>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,11 @@
import { Text } from '@status-im/components'
const KeyGenerationTitle = () => {
return (
<Text size={27} weight={'semibold'}>
Key Generation
</Text>
)
}
export default KeyGenerationTitle

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import KeystoreFiles from './KeystoreFiles'
const meta = {
title: 'ValidatorOnboarding/KeystoreFiles',
component: KeystoreFiles,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof KeystoreFiles>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,99 @@
import { Stack, XStack, YStack } from 'tamagui'
import { Button, InformationBox, Input, Text } from '@status-im/components'
import { ClearIcon, CloseCircleIcon } from '@status-im/icons'
import { useState } from 'react'
const KeystoreFiles = () => {
const [encryptedPassword, setEncryptedPassword] = useState('')
const [confirmEncryptedPassword, setConfirmEncryptedPassword] = useState('')
const generateKeystoreFilesHandler = () => {}
const changeEncryptedPasswordHandler = (e: any) => {
setEncryptedPassword(e.target.value)
}
const changeConfirmEncryptedPasswordHandler = (e: any) => {
setConfirmEncryptedPassword(e.target.value)
}
const clearEncryptedPasswordHandler = () => {
setEncryptedPassword('')
}
const clearConfirmEncryptedPasswordHandler = () => {
setConfirmEncryptedPassword('')
}
const downloadKeyFilesHandler = () => {}
return (
<YStack space={'$4'}>
<XStack space={'$2'} style={{ justifyContent: 'space-between', width: '100%' }}>
<YStack space={'$4'} style={{ width: '66%' }}>
<YStack space={'$4'}>
<Text size={15} color={'#647084'}>
Encryption Password
</Text>
<Input
placeholder={'******************'}
icon={
<ClearIcon
size={16}
color="#A1ABBD"
style={{ cursor: 'pointer' }}
onClick={clearEncryptedPasswordHandler}
/>
}
value={encryptedPassword}
onChange={changeEncryptedPasswordHandler}
/>
</YStack>
<YStack space={'$2'}>
<Text size={15} color={'#647084'}>
Confirm Encryption Password
</Text>
<Input
placeholder={'******************'}
icon={
<ClearIcon
size={16}
color="#A1ABBD"
style={{ cursor: 'pointer' }}
onClick={clearConfirmEncryptedPasswordHandler}
/>
}
value={confirmEncryptedPassword}
onChange={changeConfirmEncryptedPasswordHandler}
/>
</YStack>
</YStack>
<YStack
style={{
border: '1px solid #DCE0E5',
borderRadius: '16px',
padding: '12px 16px',
width: '32%',
marginTop: '3.4%',
cursor: 'pointer',
}}
onClick={downloadKeyFilesHandler}
>
<Text size={15} weight={'semibold'}>
Download Key Files
</Text>
</YStack>
</XStack>
<Stack style={{ width: 'fit-content' }}>
<Button onPress={generateKeystoreFilesHandler}>Generate Key files</Button>
</Stack>
<InformationBox
message="You should see that you have one keystore per validator. This keystore contains your signing key, encrypted with your password. Warning: Do not store keys on multiple (backup) validator clients at once"
variant="error"
icon={<CloseCircleIcon size={20} />}
/>
</YStack>
)
}
export default KeystoreFiles

View File

@ -0,0 +1,44 @@
import type { Meta, StoryObj } from '@storybook/react'
import RecoveryMechanism from './RecoveryMechanism'
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../../constants'
const meta = {
title: 'ValidatorOnboarding/RecoveryMechanism',
component: RecoveryMechanism,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof RecoveryMechanism>
export default meta
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

@ -0,0 +1,37 @@
import { Text } from '@status-im/components'
import { XStack, YStack } from 'tamagui'
import RecoveryMechanismCard from './RecoveryMechanismCard'
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../../constants'
type RecoveryMechanismProps = {
recoveryMechanism: string
handleRecMechanismChange: (value: string) => void
}
const cards = [RECOVERY_PHRASE, KEYSTORE_FILES, BOTH_KEY_AND_RECOVERY]
const RecoveryMechanism = ({
recoveryMechanism,
handleRecMechanismChange,
}: RecoveryMechanismProps) => {
return (
<YStack style={{ width: '100%' }}>
<Text size={19} weight={'semibold'}>
Select Recovery Mechanism
</Text>
<XStack space={'$4'} style={{ justifyContent: 'space-between', marginTop: '40px' }}>
{cards.map(value => (
<RecoveryMechanismCard
key={value}
value={value}
recoveryMechanism={recoveryMechanism}
handleRecMechanismChange={handleRecMechanismChange}
/>
))}
</XStack>
</YStack>
)
}
export default RecoveryMechanism

View File

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react'
import RecoveryMechanismCard from './RecoveryMechanismCard'
import { KEYSTORE_FILES } from '../../../../constants'
const meta = {
title: 'ValidatorOnboarding/RecoveryMechanismCard',
component: RecoveryMechanismCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof RecoveryMechanismCard>
export default meta
type Story = StoryObj<typeof meta>
export const Selected: Story = {
args: {
value: KEYSTORE_FILES,
recoveryMechanism: KEYSTORE_FILES,
handleRecMechanismChange: () => {},
},
}
export const NotSelected: Story = {
args: {
value: KEYSTORE_FILES,
recoveryMechanism: '',
handleRecMechanismChange: () => {},
},
}
export const WithoutValue: Story = {
args: {
value: '',
recoveryMechanism: KEYSTORE_FILES,
handleRecMechanismChange: () => {},
},
}

View File

@ -0,0 +1,34 @@
import { Text } from '@status-im/components'
type RecoveryMechanismProps = {
value: string
recoveryMechanism: string
handleRecMechanismChange: (value: string) => void
}
const RecoveryMechanismCard = ({
value,
recoveryMechanism,
handleRecMechanismChange,
}: RecoveryMechanismProps) => {
return (
<div
style={{
border: `1px solid ${recoveryMechanism === value ? '#2A4AF566' : '#DCE0E5'}`,
borderRadius: '16px',
padding: '12px 16px',
cursor: 'pointer',
backgroundColor: recoveryMechanism === value ? '#f4f6fe' : '#fff',
width: '100%',
height: '140px',
}}
onClick={() => handleRecMechanismChange(value)}
>
<Text size={15} weight={'semibold'}>
{value}
</Text>
</div>
)
}
export default RecoveryMechanismCard

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'
import RecoveryPhrase from './RecoveryPhrase'
const meta = {
title: 'ValidatorOnboarding/RecoveryPhrase',
component: RecoveryPhrase,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof RecoveryPhrase>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
isKeystoreFiles: false,
},
}

View File

@ -0,0 +1,60 @@
import { Stack, XStack, YStack } from 'tamagui'
import { Button, InformationBox, Text } from '@status-im/components'
import { CloseCircleIcon } from '@status-im/icons'
import { useState } from 'react'
type RecoveryPhraseProps = {
isKeystoreFiles: boolean
}
const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
const [isReveal, setIsReveal] = useState(false)
const revealHandler = () => {
setIsReveal(state => !state)
}
return (
<YStack space={'$4'} style={{ width: '100%', marginTop: isKeystoreFiles ? '20px' : '0px' }}>
<Stack
style={{
border: `1px solid #2A4AF566`,
borderRadius: '16px',
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>
</XStack>
</YStack>
</Stack>
<Stack style={{ width: 'fit-content', marginBottom: '12px' }}>
<Button onPress={revealHandler}>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."
variant="error"
icon={<CloseCircleIcon size={20} />}
/>
</YStack>
)
}
export default RecoveryPhrase

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import Overview from './Overview'
const meta = {
title: 'ValidatorOnboarding/Overview',
component: Overview,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof Overview>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,53 @@
import { Text as TextTam, XStack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import OverviewCard from './OverviewCard'
import LinkWithArrow from '../../../components/General/LinkWithArrow'
const Overview = () => {
return (
<>
<YStack className="layout-left" space={'$5'} style={{ padding: '16px 32px' }}>
<TextTam fontSize={27} fontWeight={'600'}>
Overview
</TextTam>
<Text size={27}>
Becoming a validator is a big responsibility with important preparation steps. Only start
the deposit process when youre ready.
</Text>
<Text size={15} color="#939BA1">
By running a validator, you'll be responsible for securing the network and receive
continuous payouts for actions that help the network reach consensus.
</Text>
<Text size={15} color="#939BA1">
Since the successful transition to proof-of-stake via The Merge, Ethereum is fully secured
by proof-of-stake validators. By running a validator, you'll be helping to secure the
Ethereum network.
</Text>
<LinkWithArrow
text="Learn more"
to={'/'}
arrowRight={true}
style={{ marginBottom: '1%' }}
/>
<XStack space={'$3'}>
<OverviewCard text={'Current APR'} value={'4.40%'} />
<OverviewCard text={'Total ETH Staked'} value={'9,451,123'} />
<OverviewCard text={'Estimated Activation Time'} value={'32 Days'} />
<OverviewCard text={'Validator Queue'} value={'92603'} />
</XStack>
</YStack>
<section className="layout-right">
<div className="image-container">
<img
src="./background-images/sync-status-background.png"
alt="background"
className="background-img"
/>
</div>
</section>
</>
)
}
export default Overview

View File

@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import OverviewCard from './OverviewCard'
const meta = {
title: 'ValidatorOnboarding/OverviewCard',
component: OverviewCard,
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof OverviewCard>
export default meta
type Story = StoryObj<typeof meta>
export const CurrentAPR: Story = {
args: {
text: 'Current APR',
value: '4.40%',
},
}
export const TotalETHStaked: Story = {
args: {
text: 'Total ETH Staked',
value: '9,451,123',
},
}
export const EstimatedActivationTime: Story = {
args: {
text: 'Estimated Activation Time',
value: '32 Days%',
},
}
export const ValidatorQueue: Story = {
args: {
text: 'Validator Queue',
value: '92603',
},
}
export const WithoutTitle: Story = {
args: {
text: '',
value: '92603',
},
}
export const WithoutValue: Story = {
args: {
text: 'Validator Queue',
value: '',
},
}
export const WithoutData: Story = {
args: {
text: '',
value: '',
},
}

View File

@ -0,0 +1,30 @@
import { YStack } from 'tamagui'
import { Text } from '@status-im/components'
type OverviewCardProps = {
text: string
value: string
}
const OverviewCard = ({ text, value }: OverviewCardProps) => {
return (
<YStack
style={{
borderRadius: '16px',
border: '1px solid rgba(0, 0, 0, 0.15)',
width: '46%',
padding: '12px 16px',
backgroundColor: '#FFF',
}}
>
<Text size={19} weight={'semibold'}>
{text}
</Text>
<Text size={27} color="blue" weight={'semibold'}>
{value}
</Text>
</YStack>
)
}
export default OverviewCard

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'
import ValidatorBoxWrapper from './ValidatorBoxWrapper'
const meta = {
title: 'ValidatorOnboarding/ValidatorBoxWrapper',
component: ValidatorBoxWrapper,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ValidatorBoxWrapper>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: 'ValidatorBoxWrapper',
},
}

View File

@ -0,0 +1,26 @@
import { Shadow } from '@status-im/components'
import { ReactNode } from 'react'
type ValidatorBoxWrapperProps = {
children: ReactNode
}
const ValidatorBoxWrapper = ({ children }: ValidatorBoxWrapperProps) => {
return (
<Shadow
variant="$2"
style={{
borderRadius: '16px',
border: 'none',
flexDirection: 'row',
backgroundColor: '#fff',
zIndex: 999,
width: '100%',
}}
>
{children}
</Shadow>
)
}
export default ValidatorBoxWrapper

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import ValidatorOnboarding from './ValidatorOnboarding'
const meta = {
title: 'Pages/ValidatorOnboarding',
component: ValidatorOnboarding,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof ValidatorOnboarding>
export default meta
type Story = StoryObj<typeof meta>
export const Page: Story = {
args: {},
}

View File

@ -0,0 +1,140 @@
import { YStack } from 'tamagui'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import FormStepper from './FormStepper/FormStepper'
import Titles from '../../components/General/Titles'
import Overview from './Overview/Overview'
import KeyGeneration from './KeyGeneration/KeyGeneration'
import Activation from './Activation/Activation'
import ValidatorBoxWrapper from './ValidatorBoxWrapper/ValidatorBoxWrapper'
import ClientSetup from './ClientSetup/ClientSetup'
import ConsensusSelection from './ValidatorSetup/ConsensusClient/ConsensusSelection'
import Advisories from './Advisories/Advisories'
import ValidatorSetup from './ValidatorSetup/ValidatorSetup/ValidatorSetup'
import ValidatorSetupInstall from './ValidatorSetup/ValidatorInstalling/ValidatorInstall'
import ContinueButton from './ContinueButton'
import {
setIsCopyPastedPhrase,
setValidWords,
} from '../../redux/ValidatorOnboarding/KeyGeneration/slice'
import { RootState } from '../../redux/store'
import './layoutGradient.css'
import ActivationValidatorSetup from './ValidatorSetup/ValidatorActivation/ActivationValidatorSetup'
import wordlist from 'web-bip39/wordlists/english'
const ValidatorOnboarding = () => {
const [activeStep, setActiveStep] = useState(0)
const [isConfirmPhraseStage, setIsConfirmPhraseStage] = useState(false)
const [subStepValidatorSetup, setSubStepValidatorSetup] = useState(0)
const { isCopyPastedPhrase, words } = useSelector((state: RootState) => state.keyGeneration)
const navigate = useNavigate()
const dispatch = useDispatch()
const changeActiveStep = (step: number) => {
setActiveStep(step)
removeCopyPastePhraseInfoBox()
removeConfirmPhraseStage()
}
const continueHandler = () => {
if (activeStep === 4 && isConfirmPhraseStage === false) {
return setIsConfirmPhraseStage(true)
} else if (activeStep === 4 && isConfirmPhraseStage === true) {
const newValidWords = words.map(w => wordlist.includes(w))
dispatch(setValidWords(newValidWords))
if (newValidWords.every(w => w === true)) {
setActiveStep(activeStep + 1)
} else {
return
}
} else if (activeStep === 3 && subStepValidatorSetup < 3) {
setSubStepValidatorSetup(subStepValidatorSetup + 1)
} else if (activeStep < 5) {
setActiveStep(activeStep + 1)
if (activeStep === 3 && subStepValidatorSetup === 2) {
setSubStepValidatorSetup(0)
}
} else {
navigate('/')
}
removeCopyPastePhraseInfoBox()
removeConfirmPhraseStage()
}
const removeCopyPastePhraseInfoBox = () => {
if (isCopyPastedPhrase) {
dispatch(setIsCopyPastedPhrase(false))
}
}
const removeConfirmPhraseStage = () => {
if (isConfirmPhraseStage) {
setIsConfirmPhraseStage(false)
}
}
return (
<div className="gradient-wrapper">
<YStack
style={{
width: '100%',
margin: '0 auto',
padding: '2% 10% 2%',
justifyContent: 'start',
alignItems: 'start',
}}
>
<Titles
title="Create Nimbus Validator"
titleSize={19}
subtitle="Earn Rewards for securing the Ethereum Network"
/>
<FormStepper activeStep={activeStep} changeActiveStep={changeActiveStep} />
<ValidatorBoxWrapper>
{activeStep === 0 && <Overview />}
{activeStep === 1 && <Advisories />}
{activeStep === 2 && <ClientSetup />}
{activeStep === 3 && subStepValidatorSetup === 0 && <ValidatorSetup />}
{activeStep === 3 && subStepValidatorSetup === 1 && <ValidatorSetupInstall />}
{activeStep === 3 && subStepValidatorSetup === 2 && <ConsensusSelection />}
{activeStep === 3 && subStepValidatorSetup === 3 && <ActivationValidatorSetup />}
{activeStep === 4 && <KeyGeneration isConfirmPhraseStage={isConfirmPhraseStage} />}
{activeStep === 5 && <Activation
validatorsValue='4'
executionSyncStatus1={{
text: "Execution Sync Status",
isGaugeIncluded: true,
gaugeColor: "$blue",
gaugeSynced: 123.524,
gaugeTotal: 172.503,
}}
executionSyncStatus2={{
text: "Execution Sync Status",
isGaugeIncluded: true,
gaugeColor: "$red",
gaugeSynced: 123.524,
gaugeTotal: 172.503,
}}
currentAPRValue="4.40%"
estimatedActivationTimeValue="32 Days"
validatorQueueValue="92603"
/>}
</ValidatorBoxWrapper>
<ContinueButton
activeStep={activeStep}
continueHandler={continueHandler}
isConfirmPhraseStage={isConfirmPhraseStage}
subStepValidatorSetup={subStepValidatorSetup}
/>
</YStack>
</div>
)
}
export default ValidatorOnboarding

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ConsensusClientCard from './ConsensusClientCard'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'ValidatorOnboarding/ConsensusClientCard',
component: ConsensusClientCard,
tags: ['autodocs'],
decorators: [withRouter()],
} satisfies Meta<typeof ConsensusClientCard>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: { name: 'Erigon', icon: '/icons/erigon-circle.png' },
}

View File

@ -0,0 +1,33 @@
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import Icon from '../../../../components/General/Icon'
type ConsensusClientCardProps = {
name: string
icon: string
}
const ConsensusClientCard = ({ name, icon }: ConsensusClientCardProps) => {
return (
<YStack
style={{
backgroundColor: '#2A4AF50D',
border: '1px solid #2A4AF5',
borderRadius: '16px',
padding: '12px 16px',
width: '29%',
}}
space={'$10'}
>
<Stack>
<Text size={27} weight={'semibold'}>
{name}
</Text>
</Stack>
<Icon src={icon} width={100} height={100} />
</YStack>
)
}
export default ConsensusClientCard

View File

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react'
import ConsensusGaugeCard from './ConsensusGaugeCard'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'ValidatorOnboarding/ConsensusGaugeCard',
component: ConsensusGaugeCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter()],
} satisfies Meta<typeof ConsensusGaugeCard>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
color: 'orange', synced: 140000, total: 200000, title: 'Synced Files'
},
}

View File

@ -0,0 +1,57 @@
import { Stack, XStack, YStack } from 'tamagui'
import { ClearIcon } from '@status-im/icons'
import { Text } from '@status-im/components'
import StandardGauge from '../../../../components/Charts/StandardGauge'
import BorderBox from '../../../../components/General/BorderBox'
import { formatNumberForGauge } from '../../../../utilities'
type ConsensusGaugeCardProps = {
synced: number
total: number
title: string
color: string
}
const ConsensusGaugeCard = ({ synced, total, title, color }: ConsensusGaugeCardProps) => {
return (
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
<XStack space={'$2'} alignItems="center">
<Stack
style={{
height: '35px',
width: '35px',
}}
>
<StandardGauge
data={[
{
id: title,
label: title,
value: synced,
color: color,
},
{
id: 'free',
label: 'free',
value: total - synced || 1,
color: '#E7EAEE',
},
]}
/>
</Stack>
<YStack>
<Text size={11} color="#84888e" weight={'semibold'}>
{title}
</Text>
<Text size={15} weight={'semibold'}>
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
</Text>
</YStack>
<ClearIcon size={20} color="#A1ABBD" />
</XStack>
</BorderBox>
)
}
export default ConsensusGaugeCard

View File

@ -0,0 +1,20 @@
import { withRouter } from 'storybook-addon-react-router-v6'
import ConsensusSelection from './ConsensusSelection'
import { StoryObj } from '@storybook/react'
const meta = {
title: 'ValidatorOnboarding/ConsensusSelection',
component: ConsensusSelection,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,94 @@
import { XStack, Stack, Text as TextTam, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import { useSelector } from 'react-redux'
import PairedDeviceCard from './PairedDeviceCard'
import ConsensusGaugeCard from './ConsensusGaugeCard'
import ConsensusClientCard from './ConsensusClientCard'
import LinkWithArrow from '../../../../components/General/LinkWithArrow'
import { RootState } from '../../../../redux/store'
const clientIcons = {
Nethermind: '/icons/nethermind-circle.png',
Besu: '/icons/hyperledger-besu-circle.png',
Geth: '/icons/gethereum-mascot-circle.png',
Erigon: '/icons/erigon-circle.png',
Nimbus: '/icons/NimbusDisabled.svg',
}
const ConsensusSelection = () => {
const selectedClient = useSelector((state: RootState) => state.execClient.selectedClient) as
| 'Nethermind'
| 'Besu'
| 'Geth'
| 'Erigon'
| 'Nimbus'
const clients = [
{
name: selectedClient,
icon: clientIcons[selectedClient],
},
]
return (
<YStack style={{ width: '100%', padding: '32px' }}>
<XStack justifyContent={'space-between'}>
<Text size={27} weight={'semibold'}>
Validator Setup
</Text>
<XStack space={'$2'}>
<ConsensusGaugeCard
color="blue"
synced={134879}
title="Execution Sync Status"
total={150000}
/>
<PairedDeviceCard isVisibleState={true} />
</XStack>
</XStack>
<YStack>
<Stack style={{ marginBottom: '4px' }}>
<Text size={13} color="#647084">
Consensus Client Detection
</Text>
</Stack>
<Text size={15} weight={'regular'}>
No existing execution client installations have been detected on paired device.
</Text>
<Text size={13} color="#828282">
If you believe this to be incorrect please test your pairing to the correct device and try
again.
</Text>
</YStack>
<TextTam fontSize={27} style={{ margin: '5px', marginLeft: 0, marginTop: '50px' }}>
Install Consensus client
</TextTam>
<XStack space={'$8'}>
<ConsensusClientCard name={clients[0].name} icon={clients[0].icon} />
<YStack width={'67%'} space={'$4'}>
<Text size={27}>The resource efficient Ethereum Clients.</Text>
<Text size={15}>
{selectedClient} is a client implementation for both execution and consensus layers that
strives to be as lightweight as possible in terms of resources used. This allows it to
perform well on embedded systems, resource-restricted devices -- including Raspberry Pis
-- and multi-purpose servers.
</Text>
<Text size={19} weight={'semibold'}>
<LinkWithArrow
textColor="black"
text="Nimbus Documentation"
arrowRight={true}
to="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
/>
</Text>
</YStack>
</XStack>
</YStack>
)
}
export default ConsensusSelection

View File

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react'
import PairedDeviceCard from './PairedDeviceCard'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'ValidatorOnboarding/PairedDeviceCard',
component: PairedDeviceCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter],
} satisfies Meta<typeof PairedDeviceCard>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
isVisibleState: true,
},
}

View File

@ -0,0 +1,45 @@
import { useEffect, useState } from 'react'
import { XStack, YStack } from 'tamagui'
import { ClearIcon } from '@status-im/icons'
import { Avatar, Text } from '@status-im/components'
type PairedDeviceCardProps = {
isVisibleState: boolean
}
const PairedDeviceCard = ({ isVisibleState }: PairedDeviceCardProps) => {
const [isVisible, setIsVisible] = useState(true)
useEffect(() => {
setIsVisible(isVisibleState)
}, [isVisibleState])
if (!isVisible) return null
return (
<XStack
space={'$7'}
style={{
padding: '2px 6px',
border: '1px solid #DCE0E5',
borderRadius: '15px',
}}
alignItems={'center'}
>
<XStack space={'$3'} alignItems={'center'}>
<Avatar backgroundColor="pink" size={32} type="user" name="RP" />
<YStack>
<Text size={13} color="#647084">
Paired Device
</Text>
<Text size={15} weight={'semibold'}>
Stake & Chips
</Text>
</YStack>
</XStack>
<ClearIcon size={20} color="#A1ABBD" cursor={'pointer'} onClick={() => setIsVisible(false)} />
</XStack>
)
}
export default PairedDeviceCard

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ActivationValidatorSetup from './ActivationValidatorSetup'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'ValidatorOnboarding/ActivationValidatorSetup',
component: ActivationValidatorSetup,
tags: ['autodocs'],
decorators: [withRouter()],
} satisfies Meta<typeof ActivationValidatorSetup>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: { },
}

View File

@ -0,0 +1,82 @@
import { useState, useEffect } from 'react'
import { XStack, Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import Confetti from 'react-confetti'
import ActivationCard from '../../Activation/ActivationCard'
const ActivationValidatorSetup = () => {
const [showConfetti, setShowConfetti] = useState(true)
useEffect(() => {
const timer = setTimeout(() => {
setShowConfetti(false)
}, 10000)
return () => {
clearTimeout(timer)
}
}, [])
return (
<Stack style={styles.confettiContainer} width={'100%'}>
{showConfetti && <Confetti style={styles.confettiCanvas} />}
<YStack style={{ padding: '16px 32px' }}>
<YStack space={'$3'}>
<Text size={27} weight={'semibold'}>
Activation
</Text>
<Stack>
<Text size={27}>
Congratulations! You have successfully setup your Execution and Consensus clients and
are currently syncing your nodes. You need to be sufficiently synced prior to setting
up your validators and making a deposit.
</Text>
</Stack>
<YStack space={'$3'} marginTop={'10px'} width={'33%'}>
<XStack width={'151%'} space={'$3'}>
<ActivationCard
text="Execution Sync Status"
isGaugeIncluded={true}
gaugeColor={'#2a4af5'}
gaugeSynced={123.524}
gaugeTotal={172.503}
/>
<ActivationCard
text="Execution Sync Status"
isGaugeIncluded={true}
gaugeColor={'#EB5757'}
gaugeSynced={123.524}
gaugeTotal={172.503}
/>
</XStack>
<XStack space={'$3'}>
<ActivationCard text="Validator Queue" value="92603" />
<ActivationCard text="Estimated Activation Time" value="32 Days" />
<ActivationCard text="Current APR" value="4.40%" />
</XStack>
</YStack>
</YStack>
</YStack>
</Stack>
)
}
export default ActivationValidatorSetup
const styles = {
confettiContainer: {
position: 'relative' as const,
width: '100%',
height: '100%',
justifyContent: 'fit-content',
},
confettiCanvas: {
position: 'absolute' as const,
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 0,
},
}

View File

@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react'
import OsCard from './OsCard'
const meta = {
title: 'ValidatorOnboarding/OsCard',
component: OsCard,
tags: ['autodocs'],
} satisfies Meta<typeof OsCard>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
icon: '/icons/MAC.png',
name: 'Mac',
isSelected: true,
},
}

View File

@ -0,0 +1,36 @@
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import Icon from '../../../../components/General/Icon'
type OsCardProps = {
name: string
icon: string
onClick?: () => void
isSelected?: boolean
}
const OsCard = ({ name, icon, onClick, isSelected }: OsCardProps) => {
return (
<YStack
style={{
backgroundColor: isSelected ? '#2A4AF50D' : 'none',
border: isSelected ? '1px solid #2A4AF566' : '1px solid rgba(0, 0, 0, 0.15);',
borderRadius: '16px',
padding: '12px 16px',
width: '33%',
}}
space={'$12'}
onPress={onClick}
>
<Stack>
<Text size={27} weight={'semibold'}>
{name}
</Text>
</Stack>
<Icon src={icon} width={100} height={100} />
</YStack>
)
}
export default OsCard

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'
import SyntaxHighlighter from './SyntaxHighlighter'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = {
title: 'ValidatorOnboarding/SyntaxHighlighter',
component: SyntaxHighlighter,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [withRouter()],
} satisfies Meta<typeof SyntaxHighlighter>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: { rows: ['yarn', 'yarn build', 'yarn dev', 'house'] },
}

View File

@ -0,0 +1,31 @@
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { solarizedlight } from 'react-syntax-highlighter/dist/esm/styles/prism'
type SyntaxHighlighterBoxProps = {
rows: string[]
}
const customStyle = {
...solarizedlight,
'pre[class*="language-"]': {
...solarizedlight['pre[class*="language-"]'],
backgroundColor: 'white',
},
backgroundColor: 'white',
}
const SyntaxHighlighterBox = ({ rows }: SyntaxHighlighterBoxProps) => {
return (
<SyntaxHighlighter
language="bash"
showLineNumbers={true}
lineNumberStyle={{ backgroundColor: '#E7EAEE' }}
lineNumberContainerStyle={{ color: 'black' }}
customStyle={customStyle}
>
{`${rows.join('\n')}`}
</SyntaxHighlighter>
)
}
export default SyntaxHighlighterBox

Some files were not shown because too many files have changed in this diff Show More