Skip to content

Commit 0707392

Browse files
marc2332evavirsedamsarcev
authored
chore(dapp): Clean up AvailabilityCheck (#456)
* feat(dapp): Use the base price as minimum bid * clean up * do not show cards if there is an error * chore(dapp): Clean up `AvailabilityCheck` * fix: use different regex * fixes and improvements * tests --------- Co-authored-by: evavirseda <evirseda@boxfish.studio> Co-authored-by: msarcev <mario.sarcevic@iota.org>
1 parent 1b9fe90 commit 0707392

File tree

6 files changed

+272
-140
lines changed

6 files changed

+272
-140
lines changed

dapp/src/auctions/lib/utils/auctionStatus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type UserAuctionStatus =
1414
| 'claimed'
1515
| 'unknown';
1616

17-
function isAuctionActive(auctionMetadata: AuctionMetadata | null): boolean {
17+
export function isAuctionActive(auctionMetadata: AuctionMetadata | null): boolean {
1818
if (!auctionMetadata) return false;
1919

2020
const now = Date.now();

dapp/src/components/availability-check/AvailabilityCheck.tsx

Lines changed: 189 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,62 @@
44
'use client';
55

66
import { Close, Search } from '@iota/apps-ui-icons';
7-
import { Button, ButtonType, ButtonUnstyled, Input, InputType } from '@iota/apps-ui-kit';
7+
import {
8+
Button,
9+
ButtonType,
10+
ButtonUnstyled,
11+
Input,
12+
InputType,
13+
LoadingIndicator,
14+
} from '@iota/apps-ui-kit';
815
import { ConnectButton, useCurrentWallet } from '@iota/dapp-kit';
16+
import { validateIotaName } from '@iota/iota-names-sdk';
917
import { useCallback, useMemo, useState } from 'react';
1018

1119
import { AuctionBidDialog } from '@/auctions/components/dialogs/AuctionBidDialog';
1220
import { useGetAuctionMetadata } from '@/auctions/hooks/useGetAuctionMetadata';
13-
import { useNameRecord, usePriceList } from '@/hooks';
14-
import { formatNanosToIota } from '@/lib/utils';
15-
import { denormalizeName } from '@/lib/utils/format/formatNames';
21+
import { isAuctionActive } from '@/auctions/lib/utils';
22+
import { NameRecordData, useNameRecord, usePriceList } from '@/hooks';
23+
import { denormalizeName, normalizeName } from '@/lib/utils/format/formatNames';
24+
import { formatNanosToIota } from '@/lib/utils/format/formatNanosToIota';
1625

1726
import { PurchaseNameDialog } from '../dialogs/PurchaseNameDialog';
1827
import { NamePurchaseCard } from '../NamePurchaseCard';
1928

20-
function getValidationError(
21-
name: string,
22-
minLength: number = 3,
23-
maxLength: number = 64,
24-
): string | null {
25-
const IOTA_NAME_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
26-
if (!name) return null;
27-
28-
if (name.includes('.')) {
29-
return 'No subnames allowed';
30-
}
31-
if (!IOTA_NAME_REGEX.test(name)) {
32-
return 'Invalid characters. Only a-z, 0-9, and hyphens (not at the beginning or end) are allowed';
33-
}
34-
if (name.length < minLength || name.length > maxLength) {
35-
return `Name must be ${minLength}-${maxLength} characters long`;
36-
}
37-
return null;
38-
}
39-
4029
interface AvailabilityCheckProps {
4130
autoFocusInput?: boolean;
4231
onCompleted?: () => void;
4332
}
33+
4434
export function AvailabilityCheck({ autoFocusInput, onCompleted }: AvailabilityCheckProps) {
45-
const { isConnected } = useCurrentWallet();
4635
const [searchValue, setSearchValue] = useState<string>('');
4736
const [name, setName] = useState<string>('');
48-
const [isPurchaseDialogOpen, setPurchaseDialogOpen] = useState(false);
49-
const [isAuctionBidDialogOpen, setAuctionDialogOpen] = useState(false);
50-
51-
const { data: nameRecordData, error } = useNameRecord(name);
52-
const { data: priceList } = usePriceList();
5337

54-
const { data: auctionMetadata, isLoading: isAuctionMetadataLoading } =
55-
useGetAuctionMetadata(name);
56-
57-
const isAvailable = nameRecordData?.type === 'available';
58-
const isUnavailable = nameRecordData?.type === 'unavailable';
59-
const isAuctionInProgress =
60-
isUnavailable &&
61-
auctionMetadata?.endTimestamp &&
62-
auctionMetadata.endTimestamp.getTime() > Date.now();
63-
64-
// User can bid in existing auctions or if there is no auction and the name is not taken
65-
const canBid = isAuctionInProgress || isAvailable;
38+
const {
39+
data: auctionMetadata,
40+
error: auctionError,
41+
isLoading: isLoadingAuctionMetadat,
42+
} = useGetAuctionMetadata(name);
43+
const {
44+
data: nameRecordData,
45+
error: nameError,
46+
isLoading: isLoadingNameRecord,
47+
} = useNameRecord(name);
48+
const { data: priceList, error: priceError, isLoading: isLoadingPriceLst } = usePriceList();
6649

6750
const validationError = useMemo(
68-
() => getValidationError(searchValue, priceList?.minLength, priceList?.maxLength),
51+
() =>
52+
searchValue
53+
? validateIotaName(
54+
`${searchValue}.iota`,
55+
priceList?.minLength,
56+
priceList?.maxLength,
57+
false,
58+
)
59+
: null,
6960
[searchValue, priceList],
7061
);
7162

72-
const errorMessage = error?.message ?? validationError ?? '';
73-
const enableSearch = Boolean(searchValue) && !errorMessage;
74-
7563
const handleSearch = useCallback(() => {
7664
if (searchValue) setName(`${searchValue}.iota`);
7765
}, [searchValue]);
@@ -83,31 +71,21 @@ export function AvailabilityCheck({ autoFocusInput, onCompleted }: AvailabilityC
8371
}
8472
}
8573

86-
function handleBid() {
87-
setAuctionDialogOpen(false);
88-
setSearchValue('');
89-
setName('');
90-
onCompleted?.();
91-
}
92-
93-
function handlePurchase() {
94-
setPurchaseDialogOpen(false);
74+
function handleBidOrPurchase() {
9575
setSearchValue('');
9676
setName('');
9777
onCompleted?.();
9878
}
9979

100-
const statusMessage =
101-
isUnavailable && !isAuctionInProgress
102-
? 'Name is already taken.'
103-
: isAuctionInProgress
104-
? 'In auction'
105-
: undefined;
106-
const purchasePrice = isAvailable ? nameRecordData.price : undefined;
107-
const bidPrice = auctionMetadata?.minBidNanos || purchasePrice;
108-
const cleanName = denormalizeName(name);
80+
const errorMessage =
81+
auctionError?.message || nameError?.message || priceError?.message || validationError || '';
82+
const isLoading = isLoadingAuctionMetadat || isLoadingNameRecord || isLoadingPriceLst;
10983

110-
const isAuctionLoading = name && (!nameRecordData || isAuctionMetadataLoading);
84+
const normalizedName = normalizeName(name);
85+
const enableSearch = Boolean(searchValue) && !errorMessage;
86+
const isAuctionInProgress = auctionMetadata ? isAuctionActive(auctionMetadata) : false;
87+
const isUnavailable = nameRecordData?.type === 'unavailable';
88+
const isNameTaken = isUnavailable && !isAuctionInProgress;
11189

11290
const inputTrailingElement = (
11391
<div className="flex flex-row gap-xs">
@@ -131,14 +109,6 @@ export function AvailabilityCheck({ autoFocusInput, onCompleted }: AvailabilityC
131109

132110
return (
133111
<div className="flex flex-col items-center w-full space-y-4">
134-
{isPurchaseDialogOpen && isAvailable && (
135-
<PurchaseNameDialog
136-
name={name}
137-
open={isPurchaseDialogOpen}
138-
setOpen={setPurchaseDialogOpen}
139-
onPurchase={handlePurchase}
140-
/>
141-
)}
142112
<div className="flex flex-col gap-2xl w-full max-w-[744px]">
143113
<div className="flex gap-x-sm items-baseline justify-center w-full">
144114
<Input
@@ -155,70 +125,155 @@ export function AvailabilityCheck({ autoFocusInput, onCompleted }: AvailabilityC
155125
trailingElement={inputTrailingElement}
156126
/>
157127
</div>
158-
{nameRecordData && !errorMessage && (
159-
<div className="flex flex-col items-center space-y-4 w-full">
160-
{!isAuctionInProgress && (
161-
<NamePurchaseCard
162-
name={cleanName}
163-
isAvailable={!!(!isUnavailable || isAuctionInProgress)}
164-
price={
165-
purchasePrice
166-
? formatNanosToIota(purchasePrice, {
167-
showIotaSymbol: false,
168-
})
169-
: undefined
170-
}
171-
priceSupportingText={isAvailable ? 'Price' : undefined}
172-
statusMessage={statusMessage}
173-
>
174-
{isUnavailable ? null : isConnected ? (
175-
<Button
176-
type={ButtonType.Secondary}
177-
text="Buy"
178-
onClick={() => setPurchaseDialogOpen(true)}
179-
/>
180-
) : (
181-
<ConnectButton connectText="Connect" />
182-
)}
183-
</NamePurchaseCard>
184-
)}
185-
186-
{isAuctionLoading ? (
187-
<p>Loading...</p>
188-
) : canBid ? (
189-
<NamePurchaseCard
190-
name={cleanName}
191-
isAvailable={!!(!isUnavailable || isAuctionInProgress)}
192-
price={
193-
bidPrice
194-
? formatNanosToIota(bidPrice, { showIotaSymbol: false })
195-
: undefined
196-
}
197-
priceSupportingText="Minimum bid"
198-
statusMessage={statusMessage}
199-
>
200-
{isConnected ? (
201-
<Button
202-
type={ButtonType.Primary}
203-
text="Bid"
204-
onClick={() => setAuctionDialogOpen(true)}
205-
/>
206-
) : (
207-
<ConnectButton connectText="Connect" />
208-
)}
209-
</NamePurchaseCard>
210-
) : null}
211-
</div>
128+
<div className="flex flex-col items-center space-y-4 w-full">
129+
{isLoading ? (
130+
<LoadingIndicator />
131+
) : isNameTaken ? (
132+
<NamePurchaseCard
133+
name={normalizedName}
134+
isAvailable={false}
135+
statusMessage="Name is already taken."
136+
></NamePurchaseCard>
137+
) : (
138+
nameRecordData && (
139+
<>
140+
<PurchaseName
141+
name={name}
142+
nameRecordData={nameRecordData}
143+
onCompleted={handleBidOrPurchase}
144+
/>
145+
146+
<BidName
147+
name={name}
148+
nameRecordData={nameRecordData}
149+
onCompleted={handleBidOrPurchase}
150+
/>
151+
</>
152+
)
153+
)}
154+
</div>
155+
</div>
156+
</div>
157+
);
158+
}
159+
160+
function BidName({
161+
name,
162+
nameRecordData,
163+
onCompleted,
164+
}: {
165+
name: string;
166+
nameRecordData: NameRecordData;
167+
onCompleted: () => void;
168+
}) {
169+
const { isConnected } = useCurrentWallet();
170+
const [isAuctionBidDialogOpen, setAuctionDialogOpen] = useState(false);
171+
const { data: auctionMetadata } = useGetAuctionMetadata(name);
172+
173+
const isAvailable = nameRecordData?.type === 'available';
174+
const isUnavailable = nameRecordData?.type === 'unavailable';
175+
const isAuctionInProgress = auctionMetadata ? isAuctionActive(auctionMetadata) : false;
176+
const isAllowedToBid = isAvailable || (isUnavailable && isAuctionInProgress) || false;
177+
178+
function handleBid() {
179+
setAuctionDialogOpen(false);
180+
onCompleted();
181+
}
182+
183+
const purchasePrice = nameRecordData?.type === 'available' ? nameRecordData.price : undefined;
184+
// If there is no auction yet, then we use the purchase price as minimum
185+
const bidPrice = auctionMetadata?.minBidNanos || purchasePrice;
186+
const formattedBidPrice = bidPrice
187+
? formatNanosToIota(bidPrice, { showIotaSymbol: false })
188+
: undefined;
189+
const normalizedName = normalizeName(name);
190+
191+
return (
192+
<>
193+
<NamePurchaseCard
194+
name={normalizedName}
195+
isAvailable={isAllowedToBid}
196+
price={formattedBidPrice}
197+
priceSupportingText="Minimum bid"
198+
statusMessage={isAuctionInProgress ? 'In auction' : ''}
199+
>
200+
{isConnected ? (
201+
<Button
202+
type={ButtonType.Primary}
203+
text="Bid"
204+
onClick={() => setAuctionDialogOpen(true)}
205+
/>
206+
) : (
207+
<ConnectButton connectText="Connect" />
212208
)}
209+
</NamePurchaseCard>
213210

214-
{isAuctionBidDialogOpen && (
215-
<AuctionBidDialog
216-
name={name}
217-
closeDialog={() => setAuctionDialogOpen(false)}
218-
onCompleted={handleBid}
211+
{isAuctionBidDialogOpen && (
212+
<AuctionBidDialog
213+
name={name}
214+
closeDialog={() => setAuctionDialogOpen(false)}
215+
onCompleted={handleBid}
216+
/>
217+
)}
218+
</>
219+
);
220+
}
221+
222+
function PurchaseName({
223+
name,
224+
nameRecordData,
225+
onCompleted,
226+
}: {
227+
name: string;
228+
nameRecordData: NameRecordData;
229+
onCompleted: () => void;
230+
}) {
231+
const { isConnected } = useCurrentWallet();
232+
const [isPurchaseDialogOpen, setPurchaseDialogOpen] = useState(false);
233+
234+
const isAvailable = nameRecordData?.type === 'available';
235+
const isUnavailable = nameRecordData?.type === 'unavailable';
236+
237+
function handlePurchase() {
238+
setPurchaseDialogOpen(false);
239+
onCompleted();
240+
}
241+
242+
const purchasePrice = nameRecordData?.type === 'available' ? nameRecordData.price : undefined;
243+
const formattedPurchasePrice = purchasePrice
244+
? formatNanosToIota(purchasePrice, {
245+
showIotaSymbol: false,
246+
})
247+
: undefined;
248+
const normalizedName = normalizeName(name);
249+
250+
return (
251+
<>
252+
<NamePurchaseCard
253+
name={normalizedName}
254+
isAvailable={isAvailable}
255+
price={formattedPurchasePrice}
256+
priceSupportingText={isAvailable ? 'Price' : undefined}
257+
>
258+
{isUnavailable ? null : isConnected ? (
259+
<Button
260+
type={ButtonType.Secondary}
261+
text="Buy"
262+
onClick={() => setPurchaseDialogOpen(true)}
219263
/>
264+
) : (
265+
<ConnectButton connectText="Connect" />
220266
)}
221-
</div>
222-
</div>
267+
</NamePurchaseCard>
268+
269+
{isPurchaseDialogOpen && (
270+
<PurchaseNameDialog
271+
name={name}
272+
open={isPurchaseDialogOpen}
273+
setOpen={setPurchaseDialogOpen}
274+
onPurchase={handlePurchase}
275+
/>
276+
)}
277+
</>
223278
);
224279
}

0 commit comments

Comments
 (0)