import React from 'react' import { Box, Text } from 'ink' import { Surface } from '../../../ui/Surface.js' import { Select } from '../../../ui/Select.js' import { theme } from '../../../ui/theme.js' import { openImageFilePicker } from '../../profile/imagePicker.js' import { readOwnerAddressField } from '../../identityCompat.js' import type { BrowserWalletReady } from '../../wallet/browserWallet.js' import type { ProfileUpdates, Step } from '../reducer.js' import { readCustodyMode } from '../custody/state.js' import { OperatorWalletsScreen } from './EnsOperatorWalletsScreen.js' import { EditProfileFlow } from '../profile/EditProfileFlow.js' import { WalletApprovalScreen } from '../shared/components/WalletApprovalScreen.js' import type { AgentReconciliation } from '../shared/reconciliation/index.js' type StepOf = Extract type IdentityManagerEnsStep = StepOf< | 'manage-ens-operators' | 'edit-profile-menu' | 'edit-profile-name' | 'edit-profile-description' | 'edit-profile-image' | 'edit-profile-review' | 'edit-profile-ens' | 'ens-records-tx' | 'ens-setup-registry-tx' | 'ens-setup-records-tx' | 'public-profile-signing' > type EnsFlowProps = { step: IdentityManagerEnsStep walletSession: BrowserWalletReady | null reconciliation: AgentReconciliation onSetStep: (step: Step) => void onBack: () => void onWalletReady: (session: BrowserWalletReady | null) => void onTriggerRebackup: (backStep: Step, profileUpdates?: ProfileUpdates) => void onTriggerPublicProfileSave: (backStep: Step, profileUpdates: ProfileUpdates) => void onWithdrawFromVault: (step: IdentityManagerEnsStep) => void } export function isEnsStep(step: Step): step is IdentityManagerEnsStep { return step.kind === 'manage-ens-operators' || step.kind === 'edit-profile-menu' || step.kind === 'edit-profile-name' || step.kind === 'edit-profile-description' || step.kind === 'edit-profile-image' || step.kind === 'edit-profile-review' || step.kind === 'edit-profile-ens' || step.kind === 'ens-records-tx' || step.kind === 'ens-setup-registry-tx' || step.kind === 'ens-setup-records-tx' || step.kind === 'public-profile-signing' } export const EnsFlow: React.FC = ({ step, walletSession, reconciliation, onSetStep, onBack, onWalletReady, onTriggerRebackup, onTriggerPublicProfileSave, onWithdrawFromVault, }) => { if (step.kind === 'edit-profile-ens' && (reconciliation.custody === 'advanced' || reconciliation.custody === 'mid-flow-uri-pending')) { return onWithdrawFromVault(step)} onBack={onBack} /> } if (step.kind === 'manage-ens-operators') { return ( onTriggerRebackup(step.returnTo ?? { kind: 'menu' }, updates)} onWalletReady={onWalletReady} onBack={onBack} /> ) } if (isEditProfileStep(step)) { const editStep = step const menuFromDrafts = (drafts: { name?: string; description?: string; imagePath?: string }) => { const next: Step = { kind: 'edit-profile-menu', identity: editStep.identity, registry: editStep.registry, returnTo: 'returnTo' in editStep ? editStep.returnTo : undefined, ...(drafts.name !== undefined ? { name: drafts.name } : {}), ...(drafts.description !== undefined ? { description: drafts.description } : {}), ...(drafts.imagePath !== undefined ? { imagePath: drafts.imagePath } : {}), } onSetStep(next) } const currentDrafts = () => ({ name: 'name' in editStep ? editStep.name : undefined, description: 'description' in editStep ? editStep.description : undefined, imagePath: 'imagePath' in editStep ? editStep.imagePath : undefined, }) return ( { if (editStep.kind !== 'edit-profile-menu') return const carry = currentDrafts() if (field === 'name') { onSetStep({ kind: 'edit-profile-name', identity: editStep.identity, registry: editStep.registry, ...(carry.name !== undefined ? { name: carry.name } : {}), ...(carry.description !== undefined ? { description: carry.description } : {}), ...(carry.imagePath !== undefined ? { imagePath: carry.imagePath } : {}), returnTo: editStep.returnTo, }) return } if (field === 'description') { onSetStep({ kind: 'edit-profile-description', identity: editStep.identity, registry: editStep.registry, ...(carry.name !== undefined ? { name: carry.name } : {}), ...(carry.description !== undefined ? { description: carry.description } : {}), ...(carry.imagePath !== undefined ? { imagePath: carry.imagePath } : {}), returnTo: editStep.returnTo, }) return } onSetStep({ kind: 'edit-profile-image', identity: editStep.identity, registry: editStep.registry, ...(carry.name !== undefined ? { name: carry.name } : {}), ...(carry.description !== undefined ? { description: carry.description } : {}), ...(carry.imagePath !== undefined ? { imagePath: carry.imagePath } : {}), returnTo: editStep.returnTo, }) }} onSaveProfile={() => { if (editStep.kind !== 'edit-profile-menu') return const carry = currentDrafts() const savedName = (editStep.identity.state as Record | undefined)?.['name'] as string | undefined const savedDescription = (editStep.identity.state as Record | undefined)?.['description'] as string | undefined onSetStep({ kind: 'edit-profile-review', identity: editStep.identity, registry: editStep.registry, name: carry.name ?? savedName ?? '', description: carry.description ?? savedDescription ?? '', ...(carry.imagePath !== undefined ? { imagePath: carry.imagePath } : {}), returnTo: editStep.returnTo, }) }} onNameSubmit={name => { if (editStep.kind !== 'edit-profile-name') return menuFromDrafts({ ...currentDrafts(), name }) }} onDescriptionSubmit={description => { if (editStep.kind !== 'edit-profile-description') return menuFromDrafts({ ...currentDrafts(), description }) }} onIconSubmit={iconPath => { if (editStep.kind !== 'edit-profile-image') return if (iconPath === undefined) { menuFromDrafts(currentDrafts()) return } menuFromDrafts({ ...currentDrafts(), imagePath: iconPath }) }} onIconPick={() => { if (editStep.kind !== 'edit-profile-image') return const iconStep = editStep void openImageFilePicker() .then(result => { if (!result.ok) { onSetStep({ ...iconStep, error: result.cancelled ? 'icon selection cancelled.' : `${result.error}` }) return } menuFromDrafts({ ...currentDrafts(), imagePath: result.file }) }) .catch((err: unknown) => { onSetStep({ ...iconStep, error: `${(err as Error).message}` }) }) }} onReviewSave={() => { if (editStep.kind !== 'edit-profile-review') return const updates: ProfileUpdates = { name: editStep.name, description: editStep.description, ...(editStep.imagePath !== undefined ? { imagePath: editStep.imagePath } : {}), } onTriggerPublicProfileSave(editStep.returnTo ?? { kind: 'continuity-public' }, updates) }} onEnsLink={(fullName, options) => { if (step.kind !== 'edit-profile-ens') return const state = (step.identity.state ?? {}) as Record const savedOwnerAddress = readOwnerAddressField(state) ?? '' const updates: ProfileUpdates = { ensName: fullName, ...(options.mode === 'advanced' && options.ownerAddress && !savedOwnerAddress ? { ownerAddress: options.ownerAddress } : {}), ...(options.mode === 'advanced' && options.operatorWallet ? { approvedOperatorWallets: [options.operatorWallet], activeOperatorAddress: options.operatorWallet, } : {}), } onTriggerRebackup(step.returnTo ?? { kind: 'menu' }, updates) }} onEnsUnlink={() => { if (step.kind !== 'edit-profile-ens') return onTriggerRebackup(step.returnTo ?? { kind: 'menu' }, { ensName: '' }) }} onEnsRecordsUpdate={(fullName, records, options, clearRecords, currentRecords) => { if (step.kind !== 'edit-profile-ens') return onSetStep({ kind: 'ens-records-tx', identity: step.identity, registry: step.registry, fullName, records, ...(currentRecords ? { currentRecords } : {}), ...(clearRecords ? { clearRecords: true } : {}), ...(options.mode === 'advanced' && options.ownerAddress ? { ownerAddress: options.ownerAddress } : {}), returnTo: step.returnTo ?? { kind: 'menu' }, }) }} onEnsSetup={setup => { if (step.kind !== 'edit-profile-ens') return if (setup.registryAction === 'none') { onSetStep({ kind: 'ens-setup-records-tx', identity: step.identity, registry: step.registry, setup, returnTo: step.returnTo ?? { kind: 'menu' }, }) return } onSetStep({ kind: 'ens-setup-registry-tx', identity: step.identity, registry: step.registry, setup, returnTo: step.returnTo ?? { kind: 'menu' }, }) }} onManageOperatorWalletAccess={() => { if (step.kind !== 'edit-profile-ens') return onSetStep({ kind: 'manage-ens-operators', identity: step.identity, registry: step.registry, returnTo: { kind: 'edit-profile-ens', identity: step.identity, registry: step.registry, returnTo: step.returnTo, initialView: 'advanced' }, }) }} onBack={onBack} onMenu={() => onSetStep(step.returnTo ?? { kind: 'continuity-public' })} onBackToEditMenu={() => menuFromDrafts(currentDrafts())} /> ) } if (step.kind === 'ens-records-tx') { return ( onSetStep({ kind: 'edit-profile-ens', identity: step.identity, registry: step.registry, returnTo: step.returnTo })} /> ) } if (step.kind === 'ens-setup-registry-tx') { const signer = step.setup.mode === 'simple' ? 'Connected wallet' : 'Owner wallet' return ( onSetStep({ kind: 'edit-profile-ens', identity: step.identity, registry: step.registry, returnTo: step.returnTo, ...(step.setup.mode === 'advanced' ? { initialView: 'advanced' as const } : {}), })} /> ) } if (step.kind === 'ens-setup-records-tx') { const signer = step.setup.mode === 'simple' ? 'Connected wallet' : 'Owner wallet' return ( onSetStep({ kind: 'edit-profile-ens', identity: step.identity, registry: step.registry, returnTo: step.returnTo, ...(step.setup.mode === 'advanced' ? { initialView: 'advanced' as const } : {}), })} /> ) } const approval = publicProfileWalletApprovalView(step) return ( onSetStep(step.returnTo ?? { kind: 'continuity-public' })} /> ) } function publicProfileWalletApprovalView(step: StepOf<'public-profile-signing'>): { title: string subtitle: React.ReactNode label: string } { if (usesAdvancedSetup(step)) { return { title: 'Use Wallet', subtitle: 'Sign the public profile and ERC-8004 token URI update.', label: 'waiting for wallet signature...', } } return { title: 'Use Wallet', subtitle: 'Sign the public profile and ERC-8004 token URI update.', label: 'waiting for wallet signature...', } } function usesAdvancedSetup(step: StepOf<'public-profile-signing'>): boolean { const state = (step.identity.state ?? {}) as Record const custodyMode = step.profileUpdates?.custodyMode ?? readCustodyMode(state) const ownerAddress = step.profileUpdates?.ownerAddress ?? readOwnerAddressField(state) return custodyMode === 'advanced' && typeof ownerAddress === 'string' && ownerAddress.trim().length > 0 } function isEditProfileStep(step: IdentityManagerEnsStep): step is StepOf< | 'edit-profile-menu' | 'edit-profile-name' | 'edit-profile-description' | 'edit-profile-image' | 'edit-profile-review' | 'edit-profile-ens' > { return step.kind === 'edit-profile-menu' || step.kind === 'edit-profile-name' || step.kind === 'edit-profile-description' || step.kind === 'edit-profile-image' || step.kind === 'edit-profile-review' || step.kind === 'edit-profile-ens' } const EnsVaultGate: React.FC<{ onWithdraw: () => void; onBack: () => void }> = ({ onWithdraw, onBack }) => ( Token in Vault · ↵ select · esc back} > The owner wallet must hold the token directly to sign the ENS setup transaction. options={[ { value: 'withdraw', role: 'section', label: 'Proceed' }, { value: 'withdraw', label: 'Withdraw Token from Vault' }, { value: 'back', role: 'section', label: 'Navigation' }, { value: 'back', label: 'Back', role: 'utility' }, ]} hintLayout="inline" onSubmit={choice => { if (choice === 'withdraw') onWithdraw(); else onBack() }} onCancel={onBack} /> )