Skip to content

Commit 35d866f

Browse files
evavirsedamsarcev
andauthored
feat: update Renew popup (#442)
* style renew dialog * open popup * fix build * cleanup * add remainingYears * use coreconfig hook to use maxyears * remove select renoval option * fix prettier * add infobox * update message --------- Co-authored-by: msarcev <mario.sarcevic@iota.org>
1 parent ac4f41c commit 35d866f

File tree

8 files changed

+144
-80
lines changed

8 files changed

+144
-80
lines changed

dapp/src/app/(protected)/my-names/page.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import {
77
Add,
88
Assets,
9+
Calendar,
910
// Calendar,
1011
// Delete,
1112
// Info,
@@ -21,6 +22,7 @@ import { UserAuctions } from '@/auctions/components/UserAuctions';
2122
import { DeleteNameDialog, UpdateNameDialog } from '@/components';
2223
import { CreateSubnameDialog } from '@/components/dialogs/CreateSubnameDialog';
2324
import { PersonalizeAvatarDialog } from '@/components/dialogs/PersonalizeAvatarDialog';
25+
import { RenewNameDialog } from '@/components/dialogs/RenewNameDialog';
2426
import { DropdownMenuOption } from '@/components/DropdownMenuOptions';
2527
import { NameCard } from '@/components/name-card/NameCard';
2628
import { NameCardBody } from '@/components/name-card/NameCardBody';
@@ -35,6 +37,7 @@ export default function MyNamesPage(): JSX.Element {
3537
const [deleteNameDialog, setDeleteNameDialog] = useState<RegistrationNft | null>(null);
3638
const [createSubnameDialog, setCreateSubnameDialog] = useState<RegistrationNft | null>(null);
3739
const [personalizeAvatarName, setPersonalizeAvatarName] = useState<string | null>(null);
40+
const [renewName, setRenewName] = useState<RegistrationNft | null>(null);
3841

3942
const { data: names } = useRegistrationNfts('name');
4043
const { data: subnames } = useRegistrationNfts('subname');
@@ -88,11 +91,11 @@ export default function MyNamesPage(): JSX.Element {
8891
// onClick: () => {},
8992
// children: <DropdownMenuOption icon={<Link />} label="Link to Wallet Address" />,
9093
// },
91-
// {
92-
// onClick: () => {},
93-
// children: <DropdownMenuOption icon={<Calendar />} label="Renew Name" />,
94-
// hideBottomBorder: true,
95-
// },
94+
{
95+
onClick: () => setRenewName(nft),
96+
children: <DropdownMenuOption icon={<Calendar />} label="Renew Name" />,
97+
hideBottomBorder: true,
98+
},
9699
// {
97100
// onClick: () => {},
98101
// children: <DropdownMenuOption icon={<Info />} label="View All Info" />,
@@ -194,7 +197,6 @@ export default function MyNamesPage(): JSX.Element {
194197
{!!createSubnameDialog && (
195198
<CreateSubnameDialog
196199
name={createSubnameDialog.name}
197-
open={!!createSubnameDialog}
198200
setOpen={() => setCreateSubnameDialog(null)}
199201
/>
200202
)}
@@ -204,6 +206,9 @@ export default function MyNamesPage(): JSX.Element {
204206
setOpen={() => setPersonalizeAvatarName(null)}
205207
/>
206208
)}
209+
{!!renewName && (
210+
<RenewNameDialog setOpen={() => setRenewName(null)} name={renewName.name} />
211+
)}
207212
</div>
208213
);
209214
}

dapp/src/components/dialogs/CreateSubnameDialog.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,10 @@ function createSubnameUpdates({
8080

8181
type CreateSubnameProps = {
8282
name: string;
83-
open: boolean;
8483
setOpen: (bool: boolean) => void;
8584
};
8685

87-
export function CreateSubnameDialog({ name, open, setOpen }: CreateSubnameProps) {
86+
export function CreateSubnameDialog({ name, setOpen }: CreateSubnameProps) {
8887
const queryClient = useQueryClient();
8988
const iotaClient = useIotaClient();
9089
const account = useCurrentAccount();
@@ -170,7 +169,7 @@ export function CreateSubnameDialog({ name, open, setOpen }: CreateSubnameProps)
170169
editSubname.length < MIN_LABEL_SIZE;
171170

172171
return (
173-
<Dialog open={open} onOpenChange={setOpen}>
172+
<Dialog open onOpenChange={setOpen}>
174173
<DialogContent containerId="overlay-portal-container" position={DialogPosition.Right}>
175174
<Header title="New Subname" onClose={closeDialog} />
176175
<DialogBody>

dapp/src/components/dialogs/RenewNameDialog.tsx

Lines changed: 103 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
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';
47
import {
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';
1524
import { useCurrentAccount, useIotaClient, useSignAndExecuteTransaction } from '@iota/dapp-kit';
1625
import { isSubname, NameRecord } from '@iota/iota-names-sdk';
1726
import { useMutation, useQueryClient } from '@tanstack/react-query';
18-
import { useState } from 'react';
27+
import { useEffect, useState } from 'react';
1928

2029
import { NameRecordData, queryKey, useNameRecord, useRegistrationNfts } from '@/hooks';
30+
import { useCoreConfig } from '@/hooks/useCoreConfig';
2131
import { NameUpdate, useUpdateNameTransaction } from '@/hooks/useUpdateNameTransaction';
22-
import { CANT_RENEW_NAME_FOR_MORE_TIME } from '@/lib/constants';
32+
import { YEAR_MS } from '@/lib/constants';
2333
import { 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';
2437
import {
2538
getNameObject,
2639
getNamePermissions,
@@ -82,15 +95,15 @@ function createRenewUpdates({
8295

8396
interface 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>

dapp/src/components/dialogs/UpdateNameDialog.tsx

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ import {
3535
isNameRecordExpired,
3636
} from '@/lib/utils/names';
3737

38-
import { RenewNameDialog } from './RenewNameDialog';
39-
4038
type UpdateNameDialogProps = {
4139
name: string;
4240
open: boolean;
@@ -71,9 +69,6 @@ export function UpdateNameDialog({ name, open, setOpen }: UpdateNameDialogProps)
7169
const [editIsAllowingRenew, setEditIsAllowingRenew] = useState<boolean>(false);
7270
const [editIsAllowSubnames, setEditIsAllowSubnames] = useState<boolean>(false);
7371

74-
// Dialogs
75-
const [renewDialogOpen, setRenewDialogOpen] = useState(false);
76-
7772
// Sync permissions
7873
useEffect(() => {
7974
if (namePermissions) {
@@ -228,7 +223,6 @@ export function UpdateNameDialog({ name, open, setOpen }: UpdateNameDialogProps)
228223

229224
const disableEdit = isNameRecordLoading || isSendingTransaction || isExpired;
230225
const disableSave = updates.length === 0 || isWrongCombination || isLoading || isExpired;
231-
const disableRenew = isExpired;
232226

233227
return (
234228
<>
@@ -308,19 +302,6 @@ export function UpdateNameDialog({ name, open, setOpen }: UpdateNameDialogProps)
308302
</Card>
309303
</>
310304
) : null}
311-
{namePermissions?.allowTimeExtension && (
312-
<Card type={CardType.Outlined}>
313-
<CardBody
314-
title="Renew"
315-
subtitle={`Renew ${isNameSubname ? 'Subname' : 'Name'}.`}
316-
/>
317-
<Button
318-
text="Renew"
319-
onClick={() => setRenewDialogOpen(true)}
320-
disabled={disableRenew} //TODO: add grace period
321-
/>
322-
</Card>
323-
)}
324305
{updateNameError ? (
325306
<div className="text-red-400">{updateNameError.message}</div>
326307
) : null}
@@ -333,9 +314,6 @@ export function UpdateNameDialog({ name, open, setOpen }: UpdateNameDialogProps)
333314
</DialogBody>
334315
</DialogContent>
335316
</Dialog>
336-
{renewDialogOpen && (
337-
<RenewNameDialog open={renewDialogOpen} setOpen={setRenewDialogOpen} name={name} />
338-
)}
339317
</>
340318
);
341319
}

dapp/src/hooks/queryKey.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ export const queryKey = {
3535
name,
3636
address,
3737
],
38+
39+
// Core Config
40+
coreConfig: () => [queryKey.all, 'core-config'],
3841
};

dapp/src/hooks/useCoreConfig.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { useQuery } from '@tanstack/react-query';
5+
6+
import { useIotaNamesClient } from '@/contexts';
7+
8+
import { queryKey } from './queryKey';
9+
10+
export function useCoreConfig() {
11+
const { iotaNamesClient } = useIotaNamesClient();
12+
13+
return useQuery({
14+
queryKey: [...queryKey.coreConfig()],
15+
queryFn: async () => {
16+
return await iotaNamesClient.getCoreConfig();
17+
},
18+
staleTime: 60 * 60 * 1000,
19+
});
20+
}

dapp/src/lib/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
export * from './errorMessages.constants';
55
export * from './token';
66
export * from './routes.constants';
7+
export * from './time';

dapp/src/lib/constants/time.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export const YEAR_MS = 365 * 24 * 60 * 60 * 1000;

0 commit comments

Comments
 (0)