@@ -1835,6 +1835,41 @@ function Settings({ auth, setAuth, onUpgrade, upgrading }: { auth: AuthState; se
18351835 const [ proError , setProError ] = useState ( '' ) ;
18361836 const [ showProConfetti , setShowProConfetti ] = useState ( false ) ;
18371837
1838+ // v2: Notification email, aliases, expiry
1839+ const [ notifEmail , setNotifEmail ] = useState ( '' ) ;
1840+ const [ notifSaving , setNotifSaving ] = useState ( false ) ;
1841+ const [ notifSaved , setNotifSaved ] = useState ( false ) ;
1842+ const [ aliases , setAliases ] = useState < { id : string ; handle : string ; basename : string ; is_primary : number ; expiry : number | null } [ ] > ( [ ] ) ;
1843+ const [ newAliasInput , setNewAliasInput ] = useState ( '' ) ;
1844+ const [ aliasAdding , setAliasAdding ] = useState ( false ) ;
1845+ const [ aliasError , setAliasError ] = useState ( '' ) ;
1846+ const [ aliasMsg , setAliasMsg ] = useState ( '' ) ;
1847+
1848+ // Load settings on mount
1849+ useEffect ( ( ) => {
1850+ apiFetch ( '/api/settings' , auth . token ) . then ( r => r . json ( ) ) . then ( ( data : any ) => {
1851+ if ( data . notification_email ) setNotifEmail ( data . notification_email ) ;
1852+ if ( data . aliases ) setAliases ( data . aliases ) ;
1853+ } ) . catch ( ( ) => { } ) ;
1854+ } , [ auth . token ] ) ;
1855+
1856+ function getExpiryColor ( expiry : number | null ) : string {
1857+ if ( ! expiry ) return 'text-gray-400' ;
1858+ const daysLeft = ( expiry - Date . now ( ) / 1000 ) / 86400 ;
1859+ if ( daysLeft < 0 ) return 'text-red-500' ;
1860+ if ( daysLeft < 7 ) return 'text-red-400' ;
1861+ if ( daysLeft < 30 ) return 'text-orange-400' ;
1862+ if ( daysLeft < 90 ) return 'text-yellow-400' ;
1863+ return 'text-green-400' ;
1864+ }
1865+
1866+ function getExpiryText ( expiry : number | null ) : string {
1867+ if ( ! expiry ) return 'Unknown' ;
1868+ const daysLeft = Math . floor ( ( expiry - Date . now ( ) / 1000 ) / 86400 ) ;
1869+ if ( daysLeft < 0 ) return `Expired ${ Math . abs ( daysLeft ) } d ago` ;
1870+ return `${ daysLeft } d remaining` ;
1871+ }
1872+
18381873 const fullEmail = `${ auth . handle } @basemail.ai` ;
18391874 const hasBasename = ! ! auth . basename && ! / ^ 0 x / i. test ( auth . handle ! ) ;
18401875 const altEmail = hasBasename ? `${ auth . wallet . toLowerCase ( ) } @basemail.ai` : null ;
@@ -2064,6 +2099,148 @@ function Settings({ auth, setAuth, onUpgrade, upgrading }: { auth: AuthState; se
20642099 </ div >
20652100 </ div >
20662101
2102+ { /* Notification Email */ }
2103+ < div className = "bg-base-gray rounded-xl p-6 border border-gray-800" >
2104+ < h3 className = "font-bold mb-4" > Notification Email</ h3 >
2105+ < p className = "text-gray-400 text-sm mb-4" >
2106+ Where to send expiry reminders and important notifications. Defaults to your BaseMail address.
2107+ </ p >
2108+ < div className = "flex gap-2" >
2109+ < input
2110+ type = "email"
2111+ value = { notifEmail }
2112+ onChange = { ( e ) => { setNotifEmail ( e . target . value ) ; setNotifSaved ( false ) ; } }
2113+ placeholder = { `${ auth . handle } @basemail.ai` }
2114+ className = "flex-1 bg-base-dark border border-gray-700 rounded-lg px-4 py-2 text-white font-mono text-sm focus:outline-none focus:border-base-blue"
2115+ />
2116+ < button
2117+ onClick = { async ( ) => {
2118+ setNotifSaving ( true ) ;
2119+ try {
2120+ await apiFetch ( '/api/settings' , auth . token , {
2121+ method : 'PUT' ,
2122+ body : JSON . stringify ( { notification_email : notifEmail } ) ,
2123+ } ) ;
2124+ setNotifSaved ( true ) ;
2125+ } catch { }
2126+ setNotifSaving ( false ) ;
2127+ } }
2128+ disabled = { notifSaving }
2129+ className = "bg-base-blue text-white px-4 py-2 rounded-lg text-sm hover:bg-blue-600 transition disabled:opacity-50"
2130+ >
2131+ { notifSaving ? 'Saving...' : notifSaved ? 'Saved!' : 'Save' }
2132+ </ button >
2133+ </ div >
2134+ </ div >
2135+
2136+ { /* Your Basenames */ }
2137+ < div className = "bg-base-gray rounded-xl p-6 border border-gray-800" >
2138+ < h3 className = "font-bold mb-4" > Your Basenames</ h3 >
2139+ { aliases . length > 0 ? (
2140+ < div className = "space-y-3 mb-4" >
2141+ { aliases . map ( ( a ) => (
2142+ < div key = { a . handle } className = "flex items-center justify-between bg-base-dark rounded-lg p-3 border border-gray-700" >
2143+ < div className = "flex items-center gap-3" >
2144+ < input
2145+ type = "radio"
2146+ name = "primary-alias"
2147+ checked = { a . is_primary === 1 }
2148+ onChange = { async ( ) => {
2149+ try {
2150+ const res = await apiFetch ( '/api/settings/primary' , auth . token , {
2151+ method : 'PUT' ,
2152+ body : JSON . stringify ( { handle : a . handle } ) ,
2153+ } ) ;
2154+ const data = await res . json ( ) as any ;
2155+ if ( res . ok && data . token ) {
2156+ setAuth ( { ...auth , token : data . token , handle : data . handle , basename : data . basename } ) ;
2157+ // Reload aliases
2158+ const sr = await apiFetch ( '/api/settings' , data . token ) ;
2159+ const sd = await sr . json ( ) as any ;
2160+ if ( sd . aliases ) setAliases ( sd . aliases ) ;
2161+ }
2162+ } catch { }
2163+ } }
2164+ className = "accent-blue-500"
2165+ />
2166+ < div >
2167+ < span className = "font-mono text-sm text-base-blue" > { a . handle } @basemail.ai</ span >
2168+ { a . is_primary === 1 && < span className = "ml-2 text-xs bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded" > Primary</ span > }
2169+ < div className = { `text-xs mt-0.5 ${ getExpiryColor ( a . expiry ) } ` } >
2170+ { a . expiry ? (
2171+ < >
2172+ { getExpiryText ( a . expiry ) }
2173+ { ' · ' }
2174+ < a href = { `https://www.base.org/names/${ a . handle } ` } target = "_blank" rel = "noopener noreferrer" className = "text-base-blue hover:underline" > Renew</ a >
2175+ </ >
2176+ ) : (
2177+ < span className = "text-gray-500" > Expiry unknown</ span >
2178+ ) }
2179+ </ div >
2180+ </ div >
2181+ </ div >
2182+ { a . is_primary !== 1 && (
2183+ < button
2184+ onClick = { async ( ) => {
2185+ await apiFetch ( `/api/settings/alias/${ a . handle } ` , auth . token , { method : 'DELETE' } ) ;
2186+ setAliases ( aliases . filter ( x => x . handle !== a . handle ) ) ;
2187+ } }
2188+ className = "text-gray-500 hover:text-red-400 text-xs"
2189+ >
2190+ Remove
2191+ </ button >
2192+ ) }
2193+ </ div >
2194+ ) ) }
2195+ </ div >
2196+ ) : (
2197+ < p className = "text-gray-500 text-sm mb-4" > No basename aliases configured yet.</ p >
2198+ ) }
2199+ < div className = "flex gap-2" >
2200+ < div className = "flex-1 flex items-center bg-base-dark rounded-lg border border-gray-700 px-2" >
2201+ < input
2202+ type = "text"
2203+ value = { newAliasInput }
2204+ onChange = { ( e ) => { setNewAliasInput ( e . target . value . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 - ] / g, '' ) ) ; setAliasError ( '' ) ; setAliasMsg ( '' ) ; } }
2205+ placeholder = "yourname"
2206+ className = "flex-1 bg-transparent py-2 text-white font-mono text-sm focus:outline-none"
2207+ />
2208+ < span className = "text-gray-500 font-mono text-xs" > .base.eth</ span >
2209+ </ div >
2210+ < button
2211+ onClick = { async ( ) => {
2212+ if ( ! newAliasInput . trim ( ) ) return ;
2213+ setAliasAdding ( true ) ;
2214+ setAliasError ( '' ) ;
2215+ setAliasMsg ( '' ) ;
2216+ try {
2217+ const res = await apiFetch ( '/api/settings/alias' , auth . token , {
2218+ method : 'POST' ,
2219+ body : JSON . stringify ( { basename : `${ newAliasInput . trim ( ) } .base.eth` } ) ,
2220+ } ) ;
2221+ const data = await res . json ( ) as any ;
2222+ if ( ! res . ok ) throw new Error ( data . error ) ;
2223+ setAliasMsg ( `Added ${ data . handle } @basemail.ai` ) ;
2224+ setNewAliasInput ( '' ) ;
2225+ // Reload
2226+ const sr = await apiFetch ( '/api/settings' , auth . token ) ;
2227+ const sd = await sr . json ( ) as any ;
2228+ if ( sd . aliases ) setAliases ( sd . aliases ) ;
2229+ } catch ( e : any ) {
2230+ setAliasError ( e . message || 'Failed to add alias' ) ;
2231+ }
2232+ setAliasAdding ( false ) ;
2233+ } }
2234+ disabled = { aliasAdding || ! newAliasInput . trim ( ) }
2235+ className = "bg-base-blue text-white px-4 py-2 rounded-lg text-sm hover:bg-blue-500 transition disabled:opacity-50"
2236+ >
2237+ { aliasAdding ? 'Verifying...' : 'Add' }
2238+ </ button >
2239+ </ div >
2240+ { aliasError && < p className = "text-red-400 text-xs mt-2" > { aliasError } </ p > }
2241+ { aliasMsg && < p className = "text-green-400 text-xs mt-2" > { aliasMsg } </ p > }
2242+ </ div >
2243+
20672244 < div className = "bg-base-gray rounded-xl p-6 border border-gray-800" >
20682245 < h3 className = "font-bold mb-4" > API Token</ h3 >
20692246 < p className = "text-gray-400 text-sm mb-4" >
0 commit comments