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
7 changes: 7 additions & 0 deletions .changeset/address-field-resolved-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@openzeppelin/ui-components': minor
---

Add `AddressFieldWithResolvedPreview` and `ResolvedAddressFieldPreview` — a reusable composition for ENS forward resolution in `AddressField` plus a rich reverse-resolved preview card below the field.

`AddressFieldWithResolvedPreview` always suppresses the forward "Resolved to `0x…`" success announcer and cross-network disclaimer (overridable), collapses the empty aria-live region, and accepts an optional `preview` slot for custom reverse-resolution bridges. `ResolvedAddressFieldPreview` is the presentational preview card; pass `resolvedName` for sync display or wrap with `AddressNameProvider` / `AddressNameResolutionProvider` for async reverse lookup.
5 changes: 5 additions & 0 deletions .changeset/renderer-resolved-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openzeppelin/ui-renderer': patch
---

Use `AddressFieldWithResolvedPreview` from `@openzeppelin/ui-components` and `ResolvedAddressFieldPreviewWithNameResolution` from `@openzeppelin/ui-renderer` for the Address Book add-alias ENS preview. Removes the duplicated renderer-local preview component.
10 changes: 6 additions & 4 deletions examples/basic-react-app/src/components/AccountAliasDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,15 @@ function App() {
<code className="bg-muted rounded px-1">enableNameResolution</code>)
</Label>
<p className="text-muted-foreground text-xs">
A single opt-in prop. When on, the Add-alias dialog becomes ENS-aware (type a name →
it resolves to hex and auto-suggests the alias) and rows render the base{' '}
A single opt-in prop. When on, the Add-alias dialog uses{' '}
<code className="bg-muted rounded px-1">AddressFieldWithResolvedPreview</code> (type
a name → it resolves to hex, shows a reverse ENS preview card, and auto-suggests the
alias) and rows render the base{' '}
<code className="bg-muted rounded px-1">AddressDisplay</code> fed by the
reverse-name bridge. Requires an ambient{' '}
<code className="bg-muted rounded px-1">WalletStateProvider</code> (present here).
Names resolve against the active network — select Ethereum Mainnet in the
header/Name Resolution demo for real names.
Names resolve against the active network — select Ethereum Mainnet in the header for
real names.
</p>
</div>
</div>
Expand Down
130 changes: 123 additions & 7 deletions examples/basic-react-app/src/components/AddressFieldDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';

import { AddressField, Input, Label } from '@openzeppelin/ui-components';
import { useForm, useWatch } from 'react-hook-form';

import {
AddressField,
AddressFieldWithResolvedPreview,
Input,
Label,
} from '@openzeppelin/ui-components';
import { ResolvedAddressFieldPreviewWithNameResolution } from '@openzeppelin/ui-renderer';
import { classifyAddressInput } from '@openzeppelin/ui-utils';

import { useEcosystem } from '../context';
import { CodeBlock } from './CodeBlock';
import { DemoSection } from './DemoSection';
import { EcosystemIndicator } from './EcosystemIndicator';
import { MainnetL1FallbackOptInToggle } from './MainnetL1FallbackOptInToggle';
Expand Down Expand Up @@ -55,7 +62,44 @@ function TransferForm({ adapter }) {
//
// <NameResolverProvider {...useRuntimeNameResolver()}>
// <TransferForm adapter={adapter} />
// </NameResolverProvider>`;
// </NameResolverProvider>
//
// For a rich ENS preview card below the field (reverse name + avatar) instead of
// the mechanism-neutral "Resolved to 0x…" announcer, use AddressFieldWithResolvedPreview
// — see the "Rich ENS preview" section below.`;

const PREVIEW_USAGE = `import { useForm, useWatch } from 'react-hook-form';
import { AddressFieldWithResolvedPreview } from '@openzeppelin/ui-components';
import { ResolvedAddressFieldPreviewWithNameResolution } from '@openzeppelin/ui-renderer';

function RecipientWithPreview({ adapter, networkId }) {
const { control } = useForm({ mode: 'onChange', defaultValues: { recipient: '' } });
const previewAddress = useWatch({ control, name: 'recipient' });

return (
<AddressFieldWithResolvedPreview
id="recipient"
name="recipient"
label="Recipient"
control={control}
addressing={adapter.addressing}
validation={{ required: true }}
previewAddress={previewAddress}
previewNetworkId={networkId}
preview={
<ResolvedAddressFieldPreviewWithNameResolution
address={previewAddress}
networkId={networkId}
addressing={adapter.addressing}
/>
}
/>
);
}

// AddressFieldWithResolvedPreview suppresses the forward success announcer and
// cross-network disclaimer by default — the preview card replaces them. Pass a
// custom preview slot when reverse resolution needs a different runtime bridge.`;

/**
* Gallery demo for the base {@link AddressField}: a chain-agnostic address
Expand Down Expand Up @@ -91,6 +135,8 @@ export function AddressFieldDemo(): React.ReactElement {

<NameResolutionSection />

<RichPreviewSection />

<InputClassifierPlayground
isValidAddress={capabilities.isValidAddress}
networkName={metadata.name}
Expand Down Expand Up @@ -201,9 +247,11 @@ function NameResolutionSection(): React.ReactElement {
With a <code className="bg-muted rounded px-1">NameResolverProvider</code> mounted, the
same field also accepts a name — e.g. ENS: try{' '}
<code className="bg-muted rounded px-1">vitalik.eth</code>. The form value is always the
resolved address, never the name, and submit stays gated until resolution completes.
Without the provider (or on networks without a resolver) the field behaves exactly as
above. This app mounts that provider globally, fed by{' '}
resolved address, never the name, and submit stays gated until resolution completes. The
field surfaces a mechanism-neutral &ldquo;Resolved to 0x…&rdquo; announcer on success (see
the rich preview section below for the recommended card UX). Without the provider (or on
networks without a resolver) the field behaves exactly as above. This app mounts that
provider globally, fed by{' '}
<code className="bg-muted rounded px-1">useRuntimeNameResolver</code> — so the field
follows the active network chosen from the header selector.
</p>
Expand Down Expand Up @@ -257,6 +305,74 @@ function NameResolutionSection(): React.ReactElement {
);
}

// ----------------------------------------------------------------------------
// Rich ENS preview (AddressFieldWithResolvedPreview)
// ----------------------------------------------------------------------------

function RichPreviewSection(): React.ReactElement {
const { capabilities, network } = useEcosystem();
const networkId = network?.id;
const networkName = network?.name ?? '…';

const { control } = useForm<ResolutionForm>({
mode: 'onChange',
defaultValues: { recipient: '' },
});

const previewAddress = useWatch({ control, name: 'recipient' });

return (
<div className="space-y-4 border-t border-border/60 pt-6">
<div>
<h3 className="text-lg font-medium">Rich ENS preview</h3>
<p className="text-muted-foreground text-sm">
<code className="bg-muted rounded px-1">AddressFieldWithResolvedPreview</code> composes
the base field with a reverse-resolved preview card below. It suppresses the redundant
forward &ldquo;Resolved to 0x…&rdquo; announcer — watch the same field name and pass the
value as <code className="bg-muted rounded px-1">previewAddress</code>. Wire async reverse
lookup through{' '}
<code className="bg-muted rounded px-1">
ResolvedAddressFieldPreviewWithNameResolution
</code>{' '}
(renderer) or pass <code className="bg-muted rounded px-1">resolvedName</code> directly to{' '}
<code className="bg-muted rounded px-1">ResolvedAddressFieldPreview</code>.
</p>
</div>

<MainnetL1FallbackOptInToggle />

<NameResolutionNetworkHint />

<div className="max-w-md space-y-4">
<AddressFieldWithResolvedPreview
id="ens-preview-recipient"
name="recipient"
label="Recipient"
placeholder={`Address or name on ${networkName}`}
helperText="Forward resolves inline; the preview card shows reverse ENS (name + avatar)."
control={control}
addressing={capabilities ?? undefined}
validation={{ required: true }}
previewAddress={previewAddress}
previewNetworkId={networkId}
preview={
<ResolvedAddressFieldPreviewWithNameResolution
address={previewAddress}
networkId={networkId}
addressing={capabilities ?? undefined}
/>
}
/>
</div>

<div className="space-y-2">
<p className="text-sm font-medium">Usage</p>
<CodeBlock code={PREVIEW_USAGE} language="tsx" />
</div>
</div>
);
}

// ----------------------------------------------------------------------------
// classifyAddressInput sub-section (drives the field's branch)
// ----------------------------------------------------------------------------
Expand Down
107 changes: 72 additions & 35 deletions examples/basic-react-app/src/components/ENSResolutionDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { ArrowRight } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';

import {
AddressField,
AddressFieldWithResolvedPreview,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@openzeppelin/ui-components';
import { ResolvedAddressFieldPreviewWithNameResolution } from '@openzeppelin/ui-renderer';
import type { NameResolutionErrorCode } from '@openzeppelin/ui-utils';
import { nameResolutionMessageForCode } from '@openzeppelin/ui-utils';

Expand Down Expand Up @@ -97,15 +98,51 @@ const resolveRuntime = useCallback(
<WalletStateProvider /* … */>{children}</WalletStateProvider>
</RuntimeProvider>;`;

const DISCLAIMER_PROPS = `import { AddressField, AddressDisplay } from '@openzeppelin/ui-components';
const PREVIEW_WIRING = `import { useForm, useWatch } from 'react-hook-form';
import { AddressFieldWithResolvedPreview } from '@openzeppelin/ui-components';
import { ResolvedAddressFieldPreviewWithNameResolution } from '@openzeppelin/ui-renderer';

function TransferRecipientField({ control, addressing, networkId }) {
const previewAddress = useWatch({ control, name: 'recipient' });

return (
<AddressFieldWithResolvedPreview
id="recipient"
name="recipient"
label="Recipient"
control={control}
addressing={addressing}
validation={{ required: true }}
previewAddress={previewAddress}
previewNetworkId={networkId}
preview={
<ResolvedAddressFieldPreviewWithNameResolution
address={previewAddress}
networkId={networkId}
addressing={addressing}
/>
}
/>
);
}

// Suppresses the forward "Resolved to 0x…" announcer by default — the preview
// card replaces it. Cross-network fallback disclaimer appears on AddressDisplay
// inside the card (amber triangle-alert) unless you pass resolvedName with custom UI.`;

const DISCLAIMER_PROPS = `import {
AddressField,
AddressFieldWithResolvedPreview,
AddressDisplay,
} from '@openzeppelin/ui-components';

// Forward: muted note under the success template (default on).
<AddressField
/* … */
showCrossNetworkFallbackDisclaimer={false} // omit duplicate when a card shows reverse disclaimer
/>
// Base field: muted note under the success template (default on).
<AddressField showCrossNetworkFallbackDisclaimer={false} />

// Reverse: amber triangle-alert + tooltip after the verified name (default on).
// Rich preview: announcers suppressed by default — disclaimer on the card instead.
<AddressFieldWithResolvedPreview previewAddress={watch('recipient')} /* … */ />

// Reverse-only: amber triangle-alert + tooltip after the verified name (default on).
<AddressDisplay address={hex} resolvedName={record} showCrossNetworkFallbackDisclaimer />`;

interface DemoProps {
Expand Down Expand Up @@ -138,7 +175,7 @@ export function ENSResolutionDemo({ onNavigate }: DemoProps): React.ReactElement
return (
<DemoSection
title="Name Resolution"
description="ENS across the UIKit: type a name into any address field (forward), render an address as its reverse-ENS name (reverse), or opt an address book into ENS. Resolution follows the app-wide active network (`ethereum-mainnet`, `ethereum-sepolia`, …) — switch networks from the header selector. On Sepolia, only brantly.eth has a native testnet record; enable mainnet fallback below to resolve mainnet-only names while staying on Sepolia. Forward and reverse both honor the toggle and show a provenance disclaimer when fallback was used."
description="ENS across the UIKit: type a name into any address field (forward), render an address as its reverse-ENS name (reverse), or opt an address book into ENS. Use AddressFieldWithResolvedPreview for the recommended forward + rich preview card UX. Resolution follows the app-wide active network (`ethereum-mainnet`, `ethereum-sepolia`, …) — switch networks from the header selector. On Sepolia, only brantly.eth has a native testnet record; enable mainnet fallback below to resolve mainnet-only names while staying on Sepolia. Forward and reverse both honor the toggle and show a provenance disclaimer when fallback was used."
codeExample={ZERO_WIRING}
>
<EcosystemIndicator
Expand Down Expand Up @@ -178,6 +215,10 @@ function OptInWiringReference(): React.ReactElement {
<p className="text-sm font-medium">Runtime opt-in (default off)</p>
<CodeBlock code={OPT_IN_WIRING} language="tsx" />
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Rich preview field (recommended)</p>
<CodeBlock code={PREVIEW_WIRING} language="tsx" />
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Disclaimer presentation (default on)</p>
<CodeBlock code={DISCLAIMER_PROPS} language="tsx" />
Expand Down Expand Up @@ -212,7 +253,6 @@ function LiveResolverWidget(): React.ReactElement {
// so watching it gives us the address to render. Gate on the active network's
// address validation so we only render a display for a settled hex.
const recipient = useWatch({ control, name: 'recipient' });
const hasResolvedAddress = Boolean(recipient && capabilities?.isValidAddress(recipient));

return (
<Card>
Expand All @@ -234,7 +274,7 @@ function LiveResolverWidget(): React.ReactElement {

{/* Forward: name → address (ambient app-wide resolver) */}
<div className="space-y-2">
<AddressField
<AddressFieldWithResolvedPreview
id="ens-recipient"
name="recipient"
label="Resolve a name"
Expand All @@ -243,29 +283,22 @@ function LiveResolverWidget(): React.ReactElement {
control={control}
addressing={capabilities ?? undefined}
validation={{ required: true }}
// Suppress the forward disclaimer here — the resolved-address card below
// shows the reverse disclaimer via AddressDisplay (default on).
showCrossNetworkFallbackDisclaimer={false}
showForwardResolutionSuccessAnnouncer={false}
/>

{hasResolvedAddress && (
<div className="bg-muted/30 space-y-1 rounded-lg p-3">
<p className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Resolved address
</p>
<ResolvedAddressDisplay
previewAddress={recipient}
previewNetworkId={networkId}
preview={
<ResolvedAddressFieldPreviewWithNameResolution
address={recipient}
truncate={false}
showCopyButton
showExplorerLink
networkId={networkId}
addressing={capabilities ?? undefined}
label="Resolved address"
displayProps={{ truncate: false, showCopyButton: true }}
/>
<p className="text-muted-foreground text-xs">
Reverse-resolved display for the hex above. Cross-network fallback provenance shows
as an amber triangle-alert inline after the name (default on).
</p>
</div>
)}
}
/>
<p className="text-muted-foreground text-xs">
Reverse-resolved display for the hex above. Cross-network fallback provenance shows as
an amber triangle-alert inline after the name (default on).
</p>
</div>

{/* Reverse: address → name + avatar */}
Expand Down Expand Up @@ -333,8 +366,12 @@ function ZeroWiringNote(): React.ReactElement {
<code className="bg-muted rounded px-1">useRuntimeNameResolver</code> (the app-wide active
network). Every dynamic-form address field becomes ENS-capable automatically — the only
requirement is an ambient <code className="bg-muted rounded px-1">WalletStateProvider</code>
. The live widget above rides that same ambient resolver, so it follows whichever network
you select in the header. See the code snippet below.
. The live widget above uses{' '}
<code className="bg-muted rounded px-1">AddressFieldWithResolvedPreview</code> with the
renderer&apos;s{' '}
<code className="bg-muted rounded px-1">ResolvedAddressFieldPreviewWithNameResolution</code>{' '}
bridge — the same ambient resolver as dynamic forms, so it follows whichever network you
select in the header. See the code snippets below.
</p>
</div>
);
Expand Down Expand Up @@ -396,7 +433,7 @@ function ComponentLinks({ onNavigate }: DemoProps): React.ReactElement {
{
key: 'address-field',
title: 'AddressField',
desc: 'The form field — name resolution is an opt-in section (Forms).',
desc: 'Base field + rich ENS preview section (Forms).',
},
{
key: 'address-display',
Expand Down
Loading
Loading