Skip to content

Commit ccade14

Browse files
committed
feat: AddressDisplay component
# Conflicts: # packages/smart-routing-address-react-ui/src/index.ts
1 parent ac2a72d commit ccade14

3 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import { AddressDisplay } from './index'
3+
4+
const meta: Meta<typeof AddressDisplay> = {
5+
title: 'SmartRoutingAddress/AddressDisplay',
6+
component: AddressDisplay,
7+
parameters: { layout: 'centered' },
8+
decorators: [
9+
(Story) => (
10+
<div style={{ width: 368 }}>
11+
<Story />
12+
</div>
13+
),
14+
],
15+
argTypes: {
16+
onQrClick: { action: 'qr-clicked' },
17+
},
18+
}
19+
20+
export default meta
21+
type Story = StoryObj<typeof meta>
22+
23+
/**
24+
* Loading variant — matches Figma 17634:104343 ("Smart Funding" card).
25+
* Rendered while the parent is watching for a deposit at the generated
26+
* address.
27+
*/
28+
export const LoadingWatching: Story = {
29+
args: {
30+
loadingText: 'Watching for your deposit on Base…',
31+
},
32+
}
33+
34+
/**
35+
* Same loading variant, used earlier in the flow while the SRA server is
36+
* still generating the deposit address itself.
37+
*/
38+
export const LoadingGenerating: Story = {
39+
args: {
40+
loadingText: 'Generating deposit address…',
41+
},
42+
}
43+
44+
/**
45+
* Ready variant — matches Figma 17762:78875. Shows the full deposit address
46+
* on the left and a 52×52 white QR button on the right.
47+
*/
48+
export const Ready: Story = {
49+
args: {
50+
address: '0x8f527b33CD7c791aEDe7EbA077140D81A0000001',
51+
onQrClick: () => {},
52+
},
53+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @vitest-environment happy-dom
3+
*/
4+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
5+
import { afterEach, describe, expect, it, vi } from 'vitest'
6+
import { AddressDisplay } from './index'
7+
8+
afterEach(cleanup)
9+
10+
const ADDRESS = '0x8f527b33CD7c791aEDe7EbA077140D81A0000001'
11+
12+
describe('AddressDisplay', () => {
13+
describe('loading variant', () => {
14+
it('renders the loading variant when address is omitted', () => {
15+
render(<AddressDisplay />)
16+
expect(screen.getByTestId('address-display-loading')).toBeDefined()
17+
expect(screen.queryByTestId('address-display-ready')).toBeNull()
18+
})
19+
20+
it('renders the default loading text', () => {
21+
render(<AddressDisplay />)
22+
expect(screen.getByText('Watching for your deposit…')).toBeDefined()
23+
})
24+
25+
it('respects the loadingText prop', () => {
26+
render(<AddressDisplay loadingText="Generating deposit address…" />)
27+
expect(screen.getByText('Generating deposit address…')).toBeDefined()
28+
})
29+
30+
it('does not render the QR button', () => {
31+
render(<AddressDisplay />)
32+
expect(screen.queryByTestId('address-display-qr-button')).toBeNull()
33+
})
34+
})
35+
36+
describe('ready variant', () => {
37+
it('renders the ready variant when address is supplied', () => {
38+
render(<AddressDisplay address={ADDRESS} />)
39+
expect(screen.getByTestId('address-display-ready')).toBeDefined()
40+
expect(screen.queryByTestId('address-display-loading')).toBeNull()
41+
})
42+
43+
it('renders the full address text', () => {
44+
render(<AddressDisplay address={ADDRESS} />)
45+
expect(screen.getByTestId('address-display-address').textContent).toBe(
46+
ADDRESS,
47+
)
48+
})
49+
50+
it('renders the QR button', () => {
51+
render(<AddressDisplay address={ADDRESS} />)
52+
expect(screen.getByTestId('address-display-qr-button')).toBeDefined()
53+
expect(screen.getByRole('button', { name: 'Show QR code' })).toBeDefined()
54+
})
55+
56+
it('fires onQrClick when the QR button is clicked', () => {
57+
const onQrClick = vi.fn()
58+
render(<AddressDisplay address={ADDRESS} onQrClick={onQrClick} />)
59+
fireEvent.click(screen.getByTestId('address-display-qr-button'))
60+
expect(onQrClick).toHaveBeenCalledTimes(1)
61+
})
62+
63+
it('does not throw when onQrClick is omitted and the button is clicked', () => {
64+
render(<AddressDisplay address={ADDRESS} />)
65+
expect(() =>
66+
fireEvent.click(screen.getByTestId('address-display-qr-button')),
67+
).not.toThrow()
68+
})
69+
})
70+
})
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { cn, Icon, Text, Wrapper } from '@zerodev/react-ui'
2+
3+
/**
4+
* The card that renders the deposit address in the SRA "Deposit funds" screen.
5+
*
6+
* Two visual states, each mapped 1:1 to a Figma node:
7+
* - Loading (Figma 17634:104343, "Smart Funding" card): 16px radius, 20%
8+
* white surface, centered loading icon + `loadingText`. Rendered when
9+
* `address` is omitted.
10+
* - Ready (Figma 17762:78875, address+QR row): 14px radius, 40% offWhite
11+
* surface, left-aligned address text + a 52×52 white QR button on the
12+
* right. Rendered when `address` is supplied.
13+
*
14+
* The two states have deliberately different visuals; they share this
15+
* component because the parent renders the same slot in both cases.
16+
*/
17+
export interface AddressDisplayProps {
18+
/** Deposit address to render; when omitted, the loading variant is shown. */
19+
address?: string
20+
/**
21+
* Message rendered in the loading variant. Include chain-specific context
22+
* from the caller, e.g., `Watching for your deposit on Base…`.
23+
*/
24+
loadingText?: string
25+
/** Handler for the QR icon button (ready variant). */
26+
onQrClick?: () => void
27+
className?: string
28+
}
29+
30+
export function AddressDisplay({
31+
address,
32+
loadingText = 'Watching for your deposit…',
33+
onQrClick,
34+
className,
35+
}: AddressDisplayProps) {
36+
if (address === undefined) {
37+
return (
38+
<Wrapper
39+
variant="ghost"
40+
className={cn(
41+
// Figma: rounded-2xl (16px), p-16, centered content, gap 8px, 20%
42+
// white surface from Wrapper's ghost variant. Height pinned to
43+
// 68px so it matches the ready variant.
44+
'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',
45+
'zd:shadow-[inset_0_-4px_4px_0_rgba(255,255,255,0.1),inset_0_3px_4px_0_rgba(0,0,0,0.02)]',
46+
className,
47+
)}
48+
data-testid="address-display-loading"
49+
>
50+
<Icon name="lineLoading" className="zd:size-4 zd:text-greyScale/50" />
51+
<Text className="zd:text-body1 zd:text-greyScale/50">
52+
{loadingText}
53+
</Text>
54+
</Wrapper>
55+
)
56+
}
57+
58+
return (
59+
<Wrapper
60+
variant="ghost"
61+
// Figma's 40% offWhite surface — none of Wrapper's white-only variants
62+
// hit this tint, so override the backgroundColor. Inline style wins
63+
// over Wrapper's own style prop.
64+
style={{ backgroundColor: 'rgba(247, 245, 240, 0.4)' }}
65+
className={cn(
66+
// Figma: rounded-[14px], pl-16 pr-8 py-8, gap-12 items-center.
67+
// Height pinned to 68px so the loading and ready variants swap without
68+
// the layout jumping.
69+
'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',
70+
'zd:shadow-[inset_0_-4px_4px_0_rgba(255,255,255,0.1),inset_0_3px_4px_0_rgba(0,0,0,0.02)]',
71+
className,
72+
)}
73+
data-testid="address-display-ready"
74+
>
75+
<Text
76+
className="zd:min-w-0 zd:flex-1 zd:break-all zd:text-body2"
77+
data-testid="address-display-address"
78+
>
79+
{address}
80+
</Text>
81+
<button
82+
type="button"
83+
onClick={onQrClick}
84+
aria-label="Show QR code"
85+
className="zd:flex zd:size-13 zd:shrink-0 zd:cursor-pointer zd:items-center zd:justify-center zd:rounded-2xl zd:bg-white"
86+
data-testid="address-display-qr-button"
87+
>
88+
<Icon name="qrCode" className="zd:size-5 zd:text-greyScale" />
89+
</button>
90+
</Wrapper>
91+
)
92+
}

0 commit comments

Comments
 (0)