11// Copyright (c) 2025 IOTA Stiftung
22// SPDX-License-Identifier: Apache-2.0
33
4+ 'use client' ;
5+
6+ import { Warning } from '@iota/apps-ui-icons' ;
47import {
58 Button ,
69 ButtonType ,
710 Dialog ,
811 DialogBody ,
912 DialogContent ,
13+ DialogPosition ,
14+ DisplayStats ,
1015 Header ,
11- Input ,
12- InputType ,
16+ InfoBox ,
17+ InfoBoxStyle ,
18+ InfoBoxType ,
1319 LoadingIndicator ,
20+ Panel ,
21+ Select ,
22+ SelectOption ,
1423} from '@iota/apps-ui-kit' ;
1524import { useCurrentAccount , useIotaClient , useSignAndExecuteTransaction } from '@iota/dapp-kit' ;
1625import { isSubname , NameRecord } from '@iota/iota-names-sdk' ;
1726import { useMutation , useQueryClient } from '@tanstack/react-query' ;
18- import { useState } from 'react' ;
27+ import { useEffect , useState } from 'react' ;
1928
2029import { NameRecordData , queryKey , useNameRecord , useRegistrationNfts } from '@/hooks' ;
30+ import { useCoreConfig } from '@/hooks/useCoreConfig' ;
2131import { NameUpdate , useUpdateNameTransaction } from '@/hooks/useUpdateNameTransaction' ;
22- import { CANT_RENEW_NAME_FOR_MORE_TIME } from '@/lib/constants' ;
32+ import { YEAR_MS } from '@/lib/constants' ;
2333import { RegistrationNft } from '@/lib/interfaces' ;
34+ import { formatExpirationDate } from '@/lib/utils/format/formatExpirationDate' ;
35+ import { normalizeNameInput } from '@/lib/utils/format/formatNames' ;
36+ import { getDefaultExpirationDate } from '@/lib/utils/getDefaultExpirationDate' ;
2437import {
2538 getNameObject ,
2639 getNamePermissions ,
@@ -82,15 +95,15 @@ function createRenewUpdates({
8295
8396interface RenewDialogProps {
8497 name : string ;
85- open : boolean ;
8698 setOpen : ( bool : boolean ) => void ;
8799}
88100
89- export function RenewNameDialog ( { open , setOpen, name } : RenewDialogProps ) {
101+ export function RenewNameDialog ( { setOpen, name } : RenewDialogProps ) {
90102 const queryClient = useQueryClient ( ) ;
91103 const iotaClient = useIotaClient ( ) ;
92104 const account = useCurrentAccount ( ) ;
93105 const { data : nameRecordData } = useNameRecord ( name ) ;
106+ const { data : coreConfig } = useCoreConfig ( ) ;
94107
95108 // We are sure that only owned names are passed here
96109 const nameRecord = nameRecordData as
@@ -99,8 +112,8 @@ export function RenewNameDialog({ open, setOpen, name }: RenewDialogProps) {
99112
100113 const isNameSubname = nameRecord ?. nameRecord ? isSubname ( nameRecord . nameRecord . name ) : null ;
101114
102- // Editable values
103- const [ editRenewYears , setEditRenewYears ] = useState < number > ( ) ;
115+ const [ selectedYears , setSelectedYears ] = useState < string > ( '' ) ;
116+ const renewYears = selectedYears ? Number ( selectedYears ) : undefined ;
104117
105118 const { data : ownedNames } = useRegistrationNfts ( 'name' ) ;
106119 const { data : ownedSubnames } = useRegistrationNfts ( 'subname' ) ;
@@ -109,7 +122,7 @@ export function RenewNameDialog({ open, setOpen, name }: RenewDialogProps) {
109122 nameRecord : nameRecord ?. nameRecord ,
110123 ownedNames,
111124 ownedSubnames,
112- renewYears : editRenewYears ,
125+ renewYears,
113126 } ) ;
114127
115128 const {
@@ -143,57 +156,98 @@ export function RenewNameDialog({ open, setOpen, name }: RenewDialogProps) {
143156 } ,
144157 } ) ;
145158
146- const handleCancelRenewName = ( ) => {
159+ function handleCancelRenewName ( ) {
147160 setOpen ( false ) ;
148- } ;
161+ }
162+
163+ function handleYearsChange ( id : string ) {
164+ setSelectedYears ( id ) ;
165+ }
166+
167+ function remainingRenewYears ( expirationMs : number ) {
168+ if ( ! coreConfig ?. max_years ) return 0 ;
169+ const maxExpiration = Date . now ( ) + coreConfig . max_years * YEAR_MS ;
170+ const diff = maxExpiration - expirationMs ;
171+ return Math . max ( 0 , Math . floor ( diff / YEAR_MS ) ) ;
172+ }
173+
174+ const remainingYears = remainingRenewYears ( nameRecord ?. nameRecord ?. expirationTimestampMs ?? 0 ) ;
175+
176+ const RENEW_OPTIONS : SelectOption [ ] = Array . from ( { length : remainingYears } , ( _ , i ) => ( {
177+ id : String ( i + 1 ) ,
178+ label : `${ i + 1 } Year${ i ? 's' : '' } ` ,
179+ } ) ) ;
149180
150- const wantsToRenew = isNameSubname || ! ! editRenewYears ;
181+ useEffect ( ( ) => {
182+ if ( ! selectedYears && RENEW_OPTIONS . length ) {
183+ const first = RENEW_OPTIONS [ 0 ] ;
184+ setSelectedYears ( typeof first === 'string' ? first : first . id ) ;
185+ }
186+ } , [ RENEW_OPTIONS , selectedYears ] ) ;
187+
188+ const wantsToRenew = isNameSubname || ! ! renewYears ;
151189 const canRenew = nameRecord && updates . length > 0 ;
152190 const isLoading = isLoadingUpdateNameTransaction || isSendingTransaction || isSigning ;
153191 const disableEdit = isSendingTransaction || isSigning ;
154192 const disableSave = isLoading || ! canRenew || ! wantsToRenew || ! ! updateNameError ;
193+ const cleanName = normalizeNameInput ( nameRecord ?. nameRecord ?. name || name ) ;
194+ const expirationDate = nameRecord ?. nameRecord ?. expirationTimestampMs
195+ ? formatExpirationDate ( new Date ( nameRecord . nameRecord . expirationTimestampMs ) )
196+ : getDefaultExpirationDate ( ) ;
155197
156198 return (
157- < Dialog open = { open } onOpenChange = { setOpen } >
158- < DialogContent containerId = "overlay-portal-container" isFixedPosition >
159- < Header title = "Renew" titleCentered />
199+ < Dialog open onOpenChange = { setOpen } >
200+ < DialogContent containerId = "overlay-portal-container" position = { DialogPosition . Right } >
201+ < Header title = "Renew Name" />
160202 < DialogBody >
161- < div className = "flex flex-col items-center gap-y-md" >
162- < h3 className = "text-lg font-semibold mb-4" >
163- Renew name { nameRecord ?. nameRecord ?. name }
164- </ h3 >
165- { ! isNameSubname ? (
166- < div className = "mb-4" >
167- < Input
168- type = { InputType . Text }
169- onChange = { ( e ) => {
170- const val = Number ( e . target . value ) ;
171- setEditRenewYears ( isNaN ( val ) ? 0 : val ) ;
172- } }
173- placeholder = "Input renew years"
174- disabled = { disableEdit }
203+ < div className = "flex flex-col justify-between h-full items-center" >
204+ < div className = "flex flex-col w-full gap-y-md" >
205+ < Panel bgColor = "bg-names-neutral-12" >
206+ < div className = "px-md py-lg" >
207+ < span className = "text-names-neutral-100 text-headline-sm" >
208+ @{ cleanName }
209+ </ span >
210+ </ div >
211+ </ Panel >
212+ { ! isNameSubname && (
213+ < Select
214+ options = { RENEW_OPTIONS }
215+ value = { selectedYears }
216+ onValueChange = { handleYearsChange }
217+ disabled = { disableEdit || RENEW_OPTIONS . length === 0 }
218+ errorMessage = { updateNameError ?. message }
219+ />
220+ ) }
221+ { ! isNameSubname && RENEW_OPTIONS . length === 0 && (
222+ < InfoBox
223+ type = { InfoBoxType . Warning }
224+ icon = { < Warning /> }
225+ title = "Renewal Limit Reached"
226+ style = { InfoBoxStyle . Default }
227+ supportingText = { `This name has already been extended to the maximum allowed period of ${ coreConfig ?. max_years } years. You’ll be able to renew it again once it gets closer to its expiration date` }
228+ />
229+ ) }
230+ </ div >
231+ < div className = "flex flex-col w-full gap-y-md" >
232+ < div className = "flex flex-row gap-x-sm w-full" >
233+ < DisplayStats label = "Registration Expires" value = { expirationDate } />
234+ </ div >
235+ < div className = "flex w-full flex-row gap-x-xs mt-xs" >
236+ < Button
237+ type = { ButtonType . Secondary }
238+ text = "Cancel"
239+ onClick = { handleCancelRenewName }
240+ fullWidth
241+ />
242+ < Button
243+ icon = { isLoading ? < LoadingIndicator /> : null }
244+ type = { ButtonType . Primary }
245+ text = "Renew"
246+ onClick = { ( ) => handleConfirmRenewName ( ) }
247+ disabled = { disableSave }
248+ fullWidth
175249 />
176250 </ div >
177- ) : null }
178- { ! canRenew && wantsToRenew ? (
179- < div className = "text-yellow-400" > { CANT_RENEW_NAME_FOR_MORE_TIME } </ div >
180- ) : null }
181- { updateNameError ? (
182- < div className = "text-red-400" > { updateNameError . message } </ div >
183- ) : null }
184- < div className = "flex gap-2 justify-end" >
185- < Button
186- type = { ButtonType . Secondary }
187- text = "Cancel"
188- onClick = { handleCancelRenewName }
189- />
190- < Button
191- icon = { isLoading ? < LoadingIndicator /> : null }
192- type = { ButtonType . Primary }
193- text = "Confirm"
194- onClick = { ( ) => handleConfirmRenewName ( ) }
195- disabled = { disableSave }
196- />
197251 </ div >
198252 </ div >
199253 </ DialogBody >
0 commit comments