11'use client' ;
22
3- // The account-creation modal: EOA vs Smart, key minting/picking, salt. Shared
4- // by every demo that lets a user create a local EIP-8130 account — driven off
5- // the create-modal slice of `useAccountEngine`'s return value.
3+ // The account-creation modal. "Account Type" is the top-level choice:
4+ // Default — one-click EOA off your first unused key (mints one if none)
5+ // Passkey — one-click smart account owned by your first unused passkey
6+ // Advanced — hand-pick smart/EOA + initial keys + salt
7+ // Default/Passkey stay minimal (name only); Advanced reveals the full controls.
8+ // Driven off the create-modal slice of `useAccountEngine`'s return value.
69
710import { Button } from '../../../../components/ui/Button' ;
811import { cn } from '../../../../components/ui/cn' ;
912import { Text } from '../../../../components/ui/Text' ;
1013import { Modal } from '../../../../components/ui/Modal' ;
11- import { KIND_LABEL , short , signerIdentity , type WalletSigner } from '../shared' ;
14+ import { KIND_LABEL , short , signerIdentity , type CreateMode , type WalletSigner } from '../shared' ;
1215import { CheckIcon , KindBadge , TrashIcon } from '../../_shared/primitives' ;
1316import type { SignerKind } from '../library/model' ;
1417import type { AccountEngine } from '../useAccountEngine' ;
@@ -17,10 +20,19 @@ type CreateAccountModalProps = {
1720 engine : AccountEngine ;
1821} ;
1922
23+ const MODES : ReadonlyArray < readonly [ CreateMode , string , string ] > = [
24+ [ 'default' , 'Default' , 'Simplest account setup' ] ,
25+ [ 'passkey' , 'Passkey' , 'Smart account · passkey' ] ,
26+ [ 'advanced' , 'Advanced' , 'Pick type, keys & salt' ] ,
27+ ] ;
28+
2029export function CreateAccountModal ( { engine } : CreateAccountModalProps ) {
2130 const {
2231 modalOpen,
2332 setModalOpen,
33+ createMode,
34+ setCreateMode,
35+ suggestedName,
2436 modalType,
2537 setModalType,
2638 modalLabel,
@@ -43,6 +55,13 @@ export function CreateAccountModal({ engine }: CreateAccountModalProps) {
4355 randomizeCreateSalt,
4456 } = engine ;
4557
58+ const busyCreating = busy !== null ;
59+ const canCreate = createMode === 'advanced' ? Boolean ( modalAddress ) : true ;
60+
61+ const submit = ( ) => {
62+ if ( canCreate && ! busyCreating ) void createAccount ( ) ;
63+ } ;
64+
4665 return (
4766 < Modal
4867 open = { modalOpen }
@@ -56,41 +75,44 @@ export function CreateAccountModal({ engine }: CreateAccountModalProps) {
5675 < Button
5776 variant = "primary"
5877 size = "sm"
59- onClick = { createAccount }
60- disabled = { ! modalAddress }
78+ onClick = { submit }
79+ disabled = { ! canCreate || busyCreating }
6180 className = "disabled:cursor-not-allowed disabled:opacity-50"
6281 >
63- Create Account
82+ { busyCreating ? 'Creating…' : ' Create Account' }
6483 </ Button >
6584 </ >
6685 }
6786 >
6887 < label className = "flex flex-col gap-2 text-[14px] font-normal" >
69- Label
88+ Name
7089 < input
7190 value = { modalLabel }
72- placeholder = "E.g. Main account"
91+ placeholder = { suggestedName }
7392 onChange = { ( e ) => setModalLabel ( e . target . value ) }
93+ onKeyDown = { ( e ) => {
94+ if ( e . key === 'Enter' ) {
95+ e . preventDefault ( ) ;
96+ submit ( ) ;
97+ }
98+ } }
7499 className = "w-full rounded-lg border border-bds-gray-10 bg-bds-gray-0 px-3.5 py-2.5 text-[14px] font-normal outline-none transition-colors placeholder:text-bds-gray-40 focus:border-foreground dark:border-white/10 dark:bg-white/5 dark:focus:border-bds-blue-40"
75100 />
76101 </ label >
77102
78103 < div className = "flex flex-col gap-2 text-[14px] font-normal" >
79104 Account type
80- < div className = "grid grid-cols-2 gap-2" >
81- { (
82- [
83- [ 'smart' , 'Smart Account' , 'Counterfactual · keys + salt → address' ] ,
84- [ 'eoa' , 'EOA' , 'Your EOA · delegates to DefaultAccount' ] ,
85- ] as const
86- ) . map ( ( [ type , title , hint ] ) => (
105+ < div role = "radiogroup" aria-label = "Account type" className = "grid grid-cols-3 gap-2" >
106+ { MODES . map ( ( [ mode , title , hint ] ) => (
87107 < button
88- key = { type }
108+ key = { mode }
89109 type = "button"
90- onClick = { ( ) => setModalType ( type ) }
110+ role = "radio"
111+ aria-checked = { createMode === mode }
112+ onClick = { ( ) => setCreateMode ( mode ) }
91113 className = { cn (
92114 'flex flex-col gap-1 rounded-lg border p-3 text-left transition-colors' ,
93- modalType === type
115+ createMode === mode
94116 ? 'border-foreground'
95117 : 'border-bds-gray-10 hover:border-foreground dark:border-white/10 dark:hover:border-white' ,
96118 ) }
@@ -102,77 +124,110 @@ export function CreateAccountModal({ engine }: CreateAccountModalProps) {
102124 </ div >
103125 </ div >
104126
105- { modalType === 'smart ' ? (
127+ { createMode === 'advanced ' ? (
106128 < >
107- < KeyPicker
108- heading = "Initial keys"
109- empty = "Mint a key above to add it as an initial owner."
110- signers = { signers }
111- busy = { busy }
112- mintKinds = { [ 'k1' , 'p256' , 'passkey' ] }
113- canDelete = { ( s ) => ! usedSignerIds . has ( s . id ) }
114- onDelete = { deleteSigner }
115- isOn = { ( s ) => modalIds . includes ( s . id ) }
116- onToggle = { ( s ) =>
117- setModalIds ( ( prev ) => ( prev . includes ( s . id ) ? prev . filter ( ( x ) => x !== s . id ) : [ ...prev , s . id ] ) )
118- }
119- onMint = { async ( kind ) => {
120- const s = await createSigner ( kind ) ;
121- if ( s ) setModalIds ( ( prev ) => [ ...prev , s . id ] ) ;
122- } }
123- />
124- < label className = "flex flex-col gap-2 text-[14px] font-normal" >
125- Salt
126- < div className = "flex items-center gap-2 rounded-lg border border-bds-gray-10 bg-bds-gray-0 transition-colors focus-within:border-foreground dark:border-white/10 dark:bg-white/5 dark:focus-within:border-bds-blue-40" >
127- < input
128- value = { modalSalt }
129- spellCheck = { false }
130- onChange = { ( e ) => setModalSalt ( e . target . value ) }
131- placeholder = "0x… (32 bytes) or any phrase"
132- className = "min-w-0 flex-1 bg-transparent px-3.5 py-2.5 font-sans text-[13px] font-normal outline-none placeholder:text-bds-gray-40"
133- />
134- < Button
135- size = "sm"
136- variant = "secondary"
137- onClick = { randomizeCreateSalt }
138- className = "mr-1.5 shrink-0"
139- >
140- Randomize
141- </ Button >
129+ < div className = "flex flex-col gap-2 text-[14px] font-normal" >
130+ Account model
131+ < div role = "radiogroup" aria-label = "Account model" className = "grid grid-cols-2 gap-2" >
132+ { (
133+ [
134+ [ 'smart' , 'Smart Account' , 'Counterfactual · keys + salt → address' ] ,
135+ [ 'eoa' , 'EOA' , 'Your EOA · delegates to DefaultAccount' ] ,
136+ ] as const
137+ ) . map ( ( [ type , title , hint ] ) => (
138+ < button
139+ key = { type }
140+ type = "button"
141+ role = "radio"
142+ aria-checked = { modalType === type }
143+ onClick = { ( ) => setModalType ( type ) }
144+ className = { cn (
145+ 'flex flex-col gap-1 rounded-lg border p-3 text-left transition-colors' ,
146+ modalType === type
147+ ? 'border-foreground'
148+ : 'border-bds-gray-10 hover:border-foreground dark:border-white/10 dark:hover:border-white' ,
149+ ) }
150+ >
151+ < span className = "text-[14px] font-normal" > { title } </ span >
152+ < span className = "text-[12px] font-normal text-bds-gray-60 dark:text-bds-gray-40" > { hint } </ span >
153+ </ button >
154+ ) ) }
142155 </ div >
143- </ label >
144- </ >
145- ) : (
146- < KeyPicker
147- heading = "EOA key"
148- empty = "Mint a K1 key — its address becomes the account."
149- hint = "Your EOA is the account. Its key stays a full owner and it delegates to DefaultAccount on first use. Runs on Vibenet (native 8130)."
150- signers = { eoaSigners }
151- busy = { busy }
152- mintKinds = { [ 'k1' ] }
153- canDelete = { ( s ) => ! usedSignerIds . has ( s . id ) }
154- onDelete = { deleteSigner }
155- isOn = { ( s ) => modalEoaId === s . id }
156- onToggle = { ( s ) => setModalEoaId ( modalEoaId === s . id ? null : s . id ) }
157- onMint = { async ( kind ) => {
158- const s = await createSigner ( kind ) ;
159- if ( s ) setModalEoaId ( s . id ) ;
160- } }
161- />
162- ) }
156+ </ div >
163157
164- < div className = "flex flex-col gap-1" >
165- < span className = "text-[11px] tracking-[0.6px] text-bds-gray-60 dark:text-bds-gray-40" > Address</ span >
166- { modalAddress ? (
167- < span className = "break-all font-sans text-[13px] text-foreground" > { modalAddress } </ span >
168- ) : modalType === 'eoa' ? (
169- < span className = "text-[13px] text-bds-gray-60 dark:text-bds-gray-40" > Pick a K1 key</ span >
170- ) : modalSigners . length === 0 ? (
171- < span className = "text-[13px] text-bds-gray-60 dark:text-bds-gray-40" > Select at least one key</ span >
172- ) : (
173- < span className = "text-[13px] text-bds-red-60" > duplicate key — pick distinct actors</ span >
174- ) }
175- </ div >
158+ { modalType === 'smart' ? (
159+ < >
160+ < KeyPicker
161+ heading = "Initial keys"
162+ empty = "Add a key below to include it as an initial owner."
163+ signers = { signers }
164+ busy = { busy }
165+ mintKinds = { [ 'k1' , 'p256' , 'passkey' ] }
166+ canDelete = { ( s ) => ! usedSignerIds . has ( s . id ) }
167+ onDelete = { deleteSigner }
168+ isOn = { ( s ) => modalIds . includes ( s . id ) }
169+ onToggle = { ( s ) =>
170+ setModalIds ( ( prev ) => ( prev . includes ( s . id ) ? prev . filter ( ( x ) => x !== s . id ) : [ ...prev , s . id ] ) )
171+ }
172+ onMint = { async ( kind ) => {
173+ const s = await createSigner ( kind ) ;
174+ if ( s ) setModalIds ( ( prev ) => [ ...prev , s . id ] ) ;
175+ } }
176+ />
177+ < label className = "flex flex-col gap-2 text-[14px] font-normal" >
178+ Salt
179+ < div className = "flex items-center gap-2 rounded-lg border border-bds-gray-10 bg-bds-gray-0 transition-colors focus-within:border-foreground dark:border-white/10 dark:bg-white/5 dark:focus-within:border-bds-blue-40" >
180+ < input
181+ value = { modalSalt }
182+ spellCheck = { false }
183+ onChange = { ( e ) => setModalSalt ( e . target . value ) }
184+ placeholder = "0x… (32 bytes) or any phrase"
185+ className = "min-w-0 flex-1 bg-transparent px-3.5 py-2.5 font-sans text-[13px] font-normal outline-none placeholder:text-bds-gray-40"
186+ />
187+ < Button
188+ size = "sm"
189+ variant = "secondary"
190+ onClick = { randomizeCreateSalt }
191+ className = "mr-1.5 shrink-0"
192+ >
193+ Randomize
194+ </ Button >
195+ </ div >
196+ </ label >
197+ </ >
198+ ) : (
199+ < KeyPicker
200+ heading = "EOA key"
201+ empty = "Add a K1 key — its address becomes the account."
202+ hint = "Your EOA is the account. Its key stays a full owner and it delegates to DefaultAccount on first use. Runs on Vibenet (native 8130)."
203+ signers = { eoaSigners }
204+ busy = { busy }
205+ mintKinds = { [ 'k1' ] }
206+ canDelete = { ( s ) => ! usedSignerIds . has ( s . id ) }
207+ onDelete = { deleteSigner }
208+ isOn = { ( s ) => modalEoaId === s . id }
209+ onToggle = { ( s ) => setModalEoaId ( modalEoaId === s . id ? null : s . id ) }
210+ onMint = { async ( kind ) => {
211+ const s = await createSigner ( kind ) ;
212+ if ( s ) setModalEoaId ( s . id ) ;
213+ } }
214+ />
215+ ) }
216+
217+ < div className = "flex flex-col gap-1" >
218+ < span className = "text-[11px] tracking-[0.6px] text-bds-gray-60 dark:text-bds-gray-40" > Address</ span >
219+ { modalAddress ? (
220+ < span className = "break-all font-sans text-[13px] text-foreground" > { modalAddress } </ span >
221+ ) : modalType === 'eoa' ? (
222+ < span className = "text-[13px] text-bds-gray-60 dark:text-bds-gray-40" > Pick a K1 key</ span >
223+ ) : modalSigners . length === 0 ? (
224+ < span className = "text-[13px] text-bds-gray-60 dark:text-bds-gray-40" > Select at least one key</ span >
225+ ) : (
226+ < span className = "text-[13px] text-bds-red-60" > duplicate key — pick distinct actors</ span >
227+ ) }
228+ </ div >
229+ </ >
230+ ) : null }
176231 </ Modal >
177232 ) ;
178233}
@@ -207,27 +262,20 @@ function KeyPicker({
207262} : KeyPickerProps ) {
208263 return (
209264 < div className = "flex flex-col gap-2" >
210- < div className = "flex items-center justify-between gap-2" >
211- < Text variant = "label" className = "font-normal" >
212- { heading }
213- </ Text >
214- < div className = "flex gap-2" >
215- { mintKinds . map ( ( kind ) => (
216- < Button key = { kind } variant = "outline" size = "sm" onClick = { ( ) => onMint ( kind ) } disabled = { busy !== null } >
217- + { KIND_LABEL [ kind ] }
218- </ Button >
219- ) ) }
220- </ div >
221- </ div >
265+ < Text variant = "label" className = "font-normal" >
266+ { heading }
267+ </ Text >
222268 { signers . length === 0 ? (
223- < Text variant = "footnote" tone = "muted" className = "py-2 " >
269+ < Text variant = "footnote" tone = "muted" className = "py-1 " >
224270 { empty }
225271 </ Text >
226272 ) : (
227273 < ul className = "flex flex-col gap-1.5" >
228274 { signers . map ( ( s ) => {
229275 const on = isOn ( s ) ;
230276 const deletable = Boolean ( canDelete ?.( s ) && onDelete ) ;
277+ // A key that can't be deleted is bound to an existing account.
278+ const inUse = Boolean ( canDelete && ! canDelete ( s ) ) ;
231279 return (
232280 < li key = { s . id } className = "flex items-stretch gap-1.5" >
233281 < button
@@ -259,12 +307,34 @@ function KeyPicker({
259307 >
260308 < TrashIcon size = { 15 } />
261309 </ button >
310+ ) : inUse ? (
311+ < span
312+ title = "Bound to an account — can't be deleted"
313+ className = "flex shrink-0 items-center rounded-lg border border-bds-gray-10 px-2 text-[11px] text-bds-gray-50 dark:border-white/10 dark:text-bds-gray-40"
314+ >
315+ in use
316+ </ span >
262317 ) : null }
263318 </ li >
264319 ) ;
265320 } ) }
266321 </ ul >
267322 ) }
323+ { /* Mint controls sit at the foot of the list, styled like the "+ New
324+ Account" affordance — one dashed button per available key kind. */ }
325+ < div className = "flex gap-2" >
326+ { mintKinds . map ( ( kind ) => (
327+ < button
328+ key = { kind }
329+ type = "button"
330+ onClick = { ( ) => onMint ( kind ) }
331+ disabled = { busy !== null }
332+ className = "flex flex-1 items-center justify-center gap-1 rounded-lg border border-dashed border-bds-gray-15 px-3 py-2.5 text-[13px] text-bds-gray-60 transition-colors hover:border-base-blue hover:text-base-blue disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/15 dark:text-bds-gray-40 dark:hover:border-bds-blue-60"
333+ >
334+ + { KIND_LABEL [ kind ] }
335+ </ button >
336+ ) ) }
337+ </ div >
268338 { hint ? (
269339 < Text variant = "footnote" tone = "muted" >
270340 { hint }
0 commit comments