Merge pull request #14 from nimbus-gui/hn.validator-onboarding
Create validator onboarding
|
@ -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>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
|
10
package.json
|
@ -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",
|
||||
|
|
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 496 B |
|
@ -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 |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.1 KiB |
|
@ -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() {
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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'}>
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
|||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {},
|
||||
decorators: [withRouter],
|
||||
} satisfies Meta<typeof ConnectDevicePage>
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
|||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {},
|
||||
decorators: [withRouter],
|
||||
} satisfies Meta<typeof CreateLocalNodePage>
|
||||
|
||||
|
|
|
@ -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'],
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
|||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {},
|
||||
decorators: [withRouter],
|
||||
} satisfies Meta<typeof LandingPage>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
|||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {},
|
||||
decorators: [withRouter],
|
||||
} satisfies Meta<typeof GenerateId>
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
|||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {},
|
||||
decorators: [withRouter],
|
||||
} satisfies Meta<typeof PairDevice>
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ const meta = {
|
|||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof PairedSuccessfully>
|
||||
|
||||
export default meta
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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.',
|
||||
],
|
||||
}
|
|
@ -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],
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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: '',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: '',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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%;
|
||||
}
|
|
@ -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: () => {},
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Text } from '@status-im/components'
|
||||
|
||||
const KeyGenerationTitle = () => {
|
||||
return (
|
||||
<Text size={27} weight={'semibold'}>
|
||||
Key Generation
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyGenerationTitle
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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: () => {},
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: () => {},
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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: '',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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' },
|
||||
}
|
|
@ -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
|
|
@ -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'
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: { },
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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'] },
|
||||
}
|
|
@ -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
|