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
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { AddressDisplay } from './index'

const meta: Meta<typeof AddressDisplay> = {
title: 'SmartRoutingAddress/AddressDisplay',
component: AddressDisplay,
parameters: { layout: 'centered' },
decorators: [
(Story) => (
<div style={{ width: 368 }}>
<Story />
</div>
),
],
argTypes: {
onQrClick: { action: 'qr-clicked' },
},
}

export default meta
type Story = StoryObj<typeof meta>

/**
* Loading variant — matches Figma 17634:104343 ("Smart Funding" card).
* Rendered while the parent is watching for a deposit at the generated
* address.
*/
export const LoadingWatching: Story = {
args: {
loadingText: 'Watching for your deposit on Base…',
},
}

/**
* Same loading variant, used earlier in the flow while the SRA server is
* still generating the deposit address itself.
*/
export const LoadingGenerating: Story = {
args: {
loadingText: 'Generating deposit address…',
},
}

/**
* Ready variant — matches Figma 17762:78875. Shows the full deposit address
* on the left and a 52×52 white QR button on the right.
*/
export const Ready: Story = {
args: {
address: '0x8f527b33CD7c791aEDe7EbA077140D81A0000001',
onQrClick: () => {},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @vitest-environment happy-dom
*/
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AddressDisplay } from './index'

afterEach(cleanup)

const ADDRESS = '0x8f527b33CD7c791aEDe7EbA077140D81A0000001'

describe('AddressDisplay', () => {
describe('loading variant', () => {
it('renders the loading variant when address is omitted', () => {
render(<AddressDisplay />)
expect(screen.getByTestId('address-display-loading')).toBeDefined()
expect(screen.queryByTestId('address-display-ready')).toBeNull()
})

it('renders the default loading text', () => {
render(<AddressDisplay />)
expect(screen.getByText('Watching for your deposit…')).toBeDefined()
})

it('respects the loadingText prop', () => {
render(<AddressDisplay loadingText="Generating deposit address…" />)
expect(screen.getByText('Generating deposit address…')).toBeDefined()
})

it('does not render the QR button', () => {
render(<AddressDisplay />)
expect(screen.queryByTestId('address-display-qr-button')).toBeNull()
})
})

describe('ready variant', () => {
it('renders the ready variant when address is supplied', () => {
render(<AddressDisplay address={ADDRESS} />)
expect(screen.getByTestId('address-display-ready')).toBeDefined()
expect(screen.queryByTestId('address-display-loading')).toBeNull()
})

it('renders the full address text', () => {
render(<AddressDisplay address={ADDRESS} />)
expect(screen.getByTestId('address-display-address').textContent).toBe(
ADDRESS,
)
})

it('renders the QR button', () => {
render(<AddressDisplay address={ADDRESS} />)
expect(screen.getByTestId('address-display-qr-button')).toBeDefined()
expect(screen.getByRole('button', { name: 'Show QR code' })).toBeDefined()
})

it('fires onQrClick when the QR button is clicked', () => {
const onQrClick = vi.fn()
render(<AddressDisplay address={ADDRESS} onQrClick={onQrClick} />)
fireEvent.click(screen.getByTestId('address-display-qr-button'))
expect(onQrClick).toHaveBeenCalledTimes(1)
})

it('does not throw when onQrClick is omitted and the button is clicked', () => {
render(<AddressDisplay address={ADDRESS} />)
expect(() =>
fireEvent.click(screen.getByTestId('address-display-qr-button')),
).not.toThrow()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { cn, Icon, Text, Wrapper } from '@zerodev/react-ui'

/**
* The card that renders the deposit address in the SRA "Deposit funds" screen.
*
* Two visual states, each mapped 1:1 to a Figma node:
* - Loading (Figma 17634:104343, "Smart Funding" card): 16px radius, 20%
* white surface, centered loading icon + `loadingText`. Rendered when
* `address` is omitted.
* - Ready (Figma 17762:78875, address+QR row): 14px radius, 40% offWhite
* surface, left-aligned address text + a 52×52 white QR button on the
* right. Rendered when `address` is supplied.
*
* The two states have deliberately different visuals; they share this
* component because the parent renders the same slot in both cases.
*/
export interface AddressDisplayProps {
/** Deposit address to render; when omitted, the loading variant is shown. */
address?: string
/**
* Message rendered in the loading variant. Include chain-specific context
* from the caller, e.g., `Watching for your deposit on Base…`.
*/
loadingText?: string
/** Handler for the QR icon button (ready variant). */
onQrClick?: () => void
className?: string
}

export function AddressDisplay({
address,
loadingText = 'Watching for your deposit…',
onQrClick,
className,
}: AddressDisplayProps) {
if (address === undefined) {
return (
<Wrapper
variant="ghost"
className={cn(
// Figma: rounded-2xl (16px), p-16, centered content, gap 8px, 20%
// white surface from Wrapper's ghost variant. Height pinned to
// 68px so it matches the ready variant.
'zd:relative zd:flex zd:h-[68px] zd:w-full zd:items-center zd:justify-center zd:gap-2 zd:overflow-hidden zd:rounded-2xl zd:px-4',
'zd:shadow-[inset_0_-4px_4px_0_rgba(255,255,255,0.1),inset_0_3px_4px_0_rgba(0,0,0,0.02)]',
className,
)}
data-testid="address-display-loading"
>
<Icon name="lineLoading" className="zd:size-4 zd:text-greyScale/50" />
<Text className="zd:text-body1 zd:text-greyScale/50">
{loadingText}
</Text>
</Wrapper>
)
}

return (
<Wrapper
variant="ghost"
// Figma's 40% offWhite surface — none of Wrapper's white-only variants
// hit this tint, so override the backgroundColor. Inline style wins
// over Wrapper's own style prop.
style={{ backgroundColor: 'rgba(247, 245, 240, 0.4)' }}
className={cn(
// Figma: rounded-[14px], pl-16 pr-8 py-8, gap-12 items-center.
// Height pinned to 68px so the loading and ready variants swap without
// the layout jumping.
'zd:relative zd:flex zd:h-[68px] zd:w-full zd:items-center zd:gap-3 zd:overflow-hidden zd:rounded-[14px] zd:pl-4 zd:pr-2 zd:py-2',
'zd:shadow-[inset_0_-4px_4px_0_rgba(255,255,255,0.1),inset_0_3px_4px_0_rgba(0,0,0,0.02)]',
className,
)}
data-testid="address-display-ready"
>
<Text
className="zd:min-w-0 zd:flex-1 zd:break-all zd:text-body2"
data-testid="address-display-address"
>
{address}
</Text>
<button
type="button"
onClick={onQrClick}
aria-label="Show QR code"
className="zd:flex zd:size-13 zd:shrink-0 zd:cursor-pointer zd:items-center zd:justify-center zd:rounded-2xl zd:bg-white"
data-testid="address-display-qr-button"
>
<Icon name="qrCode" className="zd:size-5 zd:text-greyScale" />
</button>
</Wrapper>
)
}
Loading