Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions packages/react-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,57 @@
*/

export { AuthFlow } from './auth'

// Auth
export type { CodeInputProps } from './auth/components/CodeInput'
export { CodeInput } from './auth/components/CodeInput'
export { useAuth } from './auth/hooks/useAuth'
export type { AuthMethod, AuthStep } from './auth/types'

// Connector
export type {
SigningConfig,
ZeroDevKitConfig,
ZeroDevKitConnectorParams,
} from './connector.js'
export { zeroDevKitWallet } from './connector.js'

// Shared components
export type { AlertViewProps } from './shared/components/AlertView'
export { AlertView } from './shared/components/AlertView'
export type { BadgeProps } from './shared/components/Badge'
export { Badge } from './shared/components/Badge'
export type { ButtonProps } from './shared/components/Button'
export { Button } from './shared/components/Button'
export { Icon } from './shared/components/Icon'
export type { IconButtonProps } from './shared/components/IconButton'
export { IconButton } from './shared/components/IconButton'
export type { InputProps } from './shared/components/Input'
export { Input } from './shared/components/Input'
export type { ListItemProps } from './shared/components/ListItem'
export { ListItem } from './shared/components/ListItem'
export type {
StateImageName,
StatusViewProps,
} from './shared/components/StatusView'
export { StatusView } from './shared/components/StatusView'
export type { TextProps } from './shared/components/Text'
export { Text } from './shared/components/Text'
export type { ToggleButtonProps } from './shared/components/ToggleButton'
export { ToggleButton } from './shared/components/ToggleButton'
export type { WrapperProps, WrapperVariant } from './shared/components/Wrapper'
export { Wrapper } from './shared/components/Wrapper'

// Signing
export { SignatureRequest } from './signing'
export type { DetailsContainerProps } from './signing/components/DetailsContainer'
export { DetailsContainer } from './signing/components/DetailsContainer'
export type { InfoCardProps } from './signing/components/InfoCard'
export { InfoCard } from './signing/components/InfoCard'
export type {
GasFee,
GasTier,
TxGasFeesProps,
} from './signing/components/TxGasFees'
export { TxGasFees } from './signing/components/TxGasFees'
export { usePendingRequest } from './signing/hooks/usePendingRequest.js'

export type { PendingRequest, Request, RequestMethod } from './types.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react-vite'

import { AlertView } from '.'

const meta = {
title: 'Shared/AlertView',
component: AlertView,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
args: {
title: 'Heads up',
description:
'This is a short description explaining the alert context to the user.',
},
decorators: [
(Story) => (
<div
className="p-10"
style={{
backgroundImage: 'linear-gradient(135deg, #B78C71 0%, #45ABFB 100%)',
width: 400,
}}
>
<Story />
</div>
),
],
} satisfies Meta<typeof AlertView>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const LongDescription: Story = {
args: {
title: 'Review transaction',
description:
'Please carefully review the transaction details below before confirming. Once submitted, this action cannot be reversed and funds will be sent to the destination address.',
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'

vi.mock('../Icon', async () => {
const React = await import('react')

const MockIcon = ({
name,
...props
}: { name: string } & React.SVGProps<SVGSVGElement>) =>
React.createElement('svg', { 'data-testid': `icon-${name}`, ...props })

return {
Icon: MockIcon,
icons: {},
}
})

import { AlertView } from './index'

afterEach(() => {
cleanup()
})

describe('AlertView', () => {
it('renders the title', () => {
render(<AlertView title="Heads up" description="Something happened" />)
expect(screen.getByText('Heads up')).toBeDefined()
})

it('renders the description', () => {
render(<AlertView title="Heads up" description="Something happened" />)
expect(screen.getByText('Something happened')).toBeDefined()
})

it('renders the info icon', () => {
render(<AlertView title="Heads up" description="Detail" />)
expect(screen.getByTestId('icon-info')).toBeDefined()
})

it('uses the solid Wrapper variant (rgba alpha 0.8)', () => {
const { container } = render(
<AlertView title="Heads up" description="Detail" />,
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.style.backgroundColor).toBe('rgba(247, 245, 240, 0.8)')
})
})
23 changes: 23 additions & 0 deletions packages/react-kit/src/shared/components/AlertView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Icon } from '../Icon'
import { Text } from '../Text'
import { Wrapper } from '../Wrapper'

export interface AlertViewProps {
title: string
description: string
}

export function AlertView({ title, description }: AlertViewProps) {
return (
<Wrapper
className="w-full py-5 px-4 flex flex-col gap-4 rounded-xl"
variant="solid"
>
<div className="flex flex-row items-center gap-2">
<Icon name="info" className="h-3.5 w-3.5 text-solarOrange" />
<Text className="text-body1">{title}</Text>
</div>
<Text className="text-body3">{description}</Text>
</Wrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import { useArgs } from 'storybook/preview-api'

import { ToggleButton } from '.'

const meta = {
title: 'Shared/ToggleButton',
component: ToggleButton,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
value: { control: 'boolean' },
},
render: function Render(args) {
const [, setArgs] = useArgs<{ value: boolean }>()
return (
<ToggleButton
{...args}
onValueChange={() => setArgs({ value: !args.value })}
/>
)
},
} satisfies Meta<typeof ToggleButton>

export default meta
type Story = StoryObj<typeof meta>

export const Off: Story = {
args: { value: false },
}

export const On: Story = {
args: { value: true },
}

export const Interactive: Story = {
render: () => {
const [on, setOn] = useState(false)
return <ToggleButton value={on} onValueChange={() => setOn((v) => !v)} />
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'

import { ToggleButton } from './index'

afterEach(() => {
cleanup()
})

describe('ToggleButton', () => {
describe('rendering', () => {
it('renders as a switch element', () => {
render(<ToggleButton />)
expect(screen.getByRole('switch')).toBeDefined()
})

it('shows "Off" label when value is false', () => {
render(<ToggleButton value={false} />)
expect(screen.getByText('Off')).toBeDefined()
expect(screen.queryByText('On')).toBeNull()
})

it('shows "On" label when value is true', () => {
render(<ToggleButton value={true} />)
expect(screen.getByText('On')).toBeDefined()
expect(screen.queryByText('Off')).toBeNull()
})

it('shows "Off" label when value is undefined (default)', () => {
render(<ToggleButton />)
expect(screen.getByText('Off')).toBeDefined()
})
})

describe('aria-checked', () => {
it('is true when value is true', () => {
render(<ToggleButton value={true} />)
expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe(
'true',
)
})

it('is false when value is false', () => {
render(<ToggleButton value={false} />)
expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe(
'false',
)
})

it('is false when value is undefined', () => {
render(<ToggleButton />)
expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe(
'false',
)
})
})

describe('thumb color', () => {
it('uses the solarOrange hex color when value is true', () => {
const { container } = render(<ToggleButton value={true} />)
const thumb = container.querySelector('.rounded-full')
expect(thumb?.className).toContain('bg-solarOrange')
})

it('uses greyScale/30 when value is false', () => {
const { container } = render(<ToggleButton value={false} />)
const thumb = container.querySelector('.rounded-full')
expect(thumb?.className).toContain('bg-greyScale/30')
})
})

describe('onValueChange', () => {
it('is called when the button is clicked', () => {
const onValueChange = vi.fn()
render(<ToggleButton value={false} onValueChange={onValueChange} />)
fireEvent.click(screen.getByRole('switch'))
expect(onValueChange).toHaveBeenCalledTimes(1)
})

it('does nothing when no handler is passed', () => {
render(<ToggleButton value={false} />)
expect(() => fireEvent.click(screen.getByRole('switch'))).not.toThrow()
})
})
})
31 changes: 31 additions & 0 deletions packages/react-kit/src/shared/components/ToggleButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cn } from '../../utils/common'
import { Text } from '../Text'
import { Wrapper } from '../Wrapper'

export interface ToggleButtonProps {
value?: boolean
onValueChange?: () => void
}

export function ToggleButton({ value, onValueChange }: ToggleButtonProps) {
return (
<Wrapper variant="ghost" className="w-14 h-7 rounded-xl p-1">
<button
type="button"
role="switch"
aria-checked={value ?? false}
onClick={onValueChange}
className="flex items-center justify-center flex-row gap-1.5 w-full h-full rounded-lg bg-white/70 cursor-pointer"
>
{value && <Text className="text-body3 text-greyScale/90">On</Text>}
<div
className={cn(
'w-3.5 h-3.5 rounded-full shadow-sm',
value ? 'bg-solarOrange' : 'bg-greyScale/30',
)}
/>
{!value && <Text className="text-body3 text-greyScale/50">Off</Text>}
</button>
</Wrapper>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'

import { type GasFee, TxGasFeesSkeleton, TxGasFeesUI } from '.'
import { type GasFee, TxGasFees, TxGasFeesSkeleton } from '.'

const gasFees: GasFee[] = [
{ tier: 'low', duration: 120, fee: '0.0001 ETH', feeUsd: '$0.30' },
Expand All @@ -10,7 +10,7 @@ const gasFees: GasFee[] = [

const meta = {
title: 'Signing/TxGasFees',
component: TxGasFeesUI,
component: TxGasFees,
parameters: {
layout: 'centered',
},
Expand All @@ -35,7 +35,7 @@ const meta = {
</div>
),
],
} satisfies Meta<typeof TxGasFeesUI>
} satisfies Meta<typeof TxGasFees>

export default meta
type Story = StoryObj<typeof meta>
Expand Down
Loading
Loading