Qortal-Hub/src/components/RegisterName.tsx
2025-05-18 21:42:19 +02:00

414 lines
11 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
ListItem,
ListItemIcon,
ListItemText,
List,
TextField,
Typography,
useTheme,
} from '@mui/material';
import { Label } from './Group/AddGroup';
import { Spacer } from '../common/Spacer';
import { getBaseApiReact } from '../App';
import { getFee } from '../background';
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events';
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
import CheckIcon from '@mui/icons-material/Check';
import ErrorIcon from '@mui/icons-material/Error';
import { useSetAtom } from 'jotai';
import { txListAtom } from '../atoms/global';
import { useTranslation } from 'react-i18next';
enum Availability {
NULL = 'null',
LOADING = 'loading',
AVAILABLE = 'available',
NOT_AVAILABLE = 'not-available',
}
export const RegisterName = ({
setOpenSnack,
setInfoSnack,
userInfo,
show,
balance,
}) => {
const setTxList = useSetAtom(txListAtom);
const [isOpen, setIsOpen] = useState(false);
const [registerNameValue, setRegisterNameValue] = useState('');
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false);
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
Availability.NULL
);
const [nameFee, setNameFee] = useState(null);
const theme = useTheme();
const { t } = useTranslation(['auth', 'core', 'group']);
const checkIfNameExisits = async (name) => {
if (!name?.trim()) {
setIsNameAvailable(Availability.NULL);
return;
}
setIsNameAvailable(Availability.LOADING);
try {
const res = await fetch(`${getBaseApiReact()}/names/` + name);
const data = await res.json();
if (data?.message === 'name unknown') {
setIsNameAvailable(Availability.AVAILABLE);
} else {
setIsNameAvailable(Availability.NOT_AVAILABLE);
}
} catch (error) {
console.error(error);
}
};
// Debounce logic
useEffect(() => {
const handler = setTimeout(() => {
checkIfNameExisits(registerNameValue);
}, 500);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
clearTimeout(handler);
};
}, [registerNameValue]);
const openRegisterNameFunc = useCallback(
(e) => {
setIsOpen(true);
},
[setIsOpen]
);
useEffect(() => {
subscribeToEvent('openRegisterName', openRegisterNameFunc);
return () => {
unsubscribeFromEvent('openRegisterName', openRegisterNameFunc);
};
}, [openRegisterNameFunc]);
useEffect(() => {
const nameRegistrationFee = async () => {
try {
const fee = await getFee('REGISTER_NAME');
setNameFee(fee?.fee);
} catch (error) {
console.error(error);
}
};
nameRegistrationFee();
}, []);
const registerName = async () => {
try {
if (!userInfo?.address)
throw new Error(
t('core:message.error.address_not_found', {
postProcess: 'capitalizeFirstChar',
})
);
if (!registerNameValue)
throw new Error(
t('core:action.enter_name', {
postProcess: 'capitalizeFirstChar',
})
);
const fee = await getFee('REGISTER_NAME');
await show({
message: t('core:message.question.register_name', {
postProcess: 'capitalizeFirstChar',
}),
publishFee: fee.fee + ' QORT',
});
setIsLoadingRegisterName(true);
new Promise((res, rej) => {
window
.sendMessage('registerName', {
name: registerNameValue,
})
.then((response) => {
if (!response?.error) {
res(response);
setIsLoadingRegisterName(false);
setInfoSnack({
type: 'success',
message: t('group:message.success.registered_name', {
postProcess: 'capitalizeFirstChar',
}),
});
setIsOpen(false);
setRegisterNameValue('');
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: 'register-name',
label: t('group:message.success.registered_name_label', {
postProcess: 'capitalizeFirstChar',
}),
labelDone: t(
'group:message.success.registered_name_success',
{
postProcess: 'capitalizeFirstChar',
}
),
done: false,
},
...prev.filter((item) => !item.done),
]);
return;
}
setInfoSnack({
type: 'error',
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
})
.catch((error) => {
setInfoSnack({
type: 'error',
message:
error.message ||
t('core:message.error.generic', {
postProcess: 'capitalizeFirstChar',
}),
});
setOpenSnack(true);
rej(error);
});
});
} catch (error) {
if (error?.message) {
setOpenSnack(true);
setInfoSnack({
type: 'error',
message: error?.message,
});
}
} finally {
setIsLoadingRegisterName(false);
}
};
return (
<Dialog
open={isOpen}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{'Register name'}</DialogTitle>
<DialogContent>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
gap: '10px',
height: '500px',
maxHeight: '90vh',
maxWidth: '90vw',
padding: '10px',
width: '400px',
}}
>
<Label>
{t('core:action.choose_name', {
postProcess: 'capitalizeFirstChar',
})}
</Label>
<TextField
autoComplete="off"
autoFocus
onChange={(e) => setRegisterNameValue(e.target.value)}
value={registerNameValue}
placeholder="Choose a name"
/>
{(!balance || (nameFee && balance && balance < nameFee)) && (
<>
<Spacer height="10px" />
<Box
sx={{
display: 'flex',
gap: '5px',
alignItems: 'center',
}}
>
<ErrorIcon
sx={{
color: theme.palette.text.primary,
}}
/>
<Typography>
{t('core:message.generic.name_registration', {
balance: balance ?? 0,
fee: { nameFee },
postProcess: 'capitalizeFirstChar',
})}
</Typography>
</Box>
<Spacer height="10px" />
</>
)}
<Spacer height="5px" />
{isNameAvailable === Availability.AVAILABLE && (
<Box
sx={{
display: 'flex',
gap: '5px',
alignItems: 'center',
}}
>
<CheckIcon
sx={{
color: theme.palette.text.primary,
}}
/>
<Typography>
{t('core:message.generic.name_available', {
name: registerNameValue,
postProcess: 'capitalizeFirstChar',
})}
</Typography>
</Box>
)}
{isNameAvailable === Availability.NOT_AVAILABLE && (
<Box
sx={{
display: 'flex',
gap: '5px',
alignItems: 'center',
}}
>
<ErrorIcon
sx={{
color: theme.palette.text.primary,
}}
/>
<Typography>
{t('core:message.generic.name_unavailable', {
name: registerNameValue,
postProcess: 'capitalizeFirstChar',
})}
</Typography>
</Box>
)}
{isNameAvailable === Availability.LOADING && (
<Box
sx={{
display: 'flex',
gap: '5px',
alignItems: 'center',
}}
>
<BarSpinner width="16px" color={theme.palette.text.primary} />
<Typography>
{t('core:message.generic.name_checking', {
postProcess: 'capitalizeFirstChar',
})}
</Typography>
</Box>
)}
<Spacer height="25px" />
<Typography
sx={{
textDecoration: 'underline',
}}
>
{t('core:message.generic.name_benefits', {
postProcess: 'capitalizeFirstChar',
})}
</Typography>
<List
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
aria-label="contacts"
>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon
sx={{
color: theme.palette.text.primary,
}}
/>
</ListItemIcon>
<ListItemText
primary={t('core:message.generic.publish_data', {
postProcess: 'capitalizeFirstChar',
})}
/>
</ListItem>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon
sx={{
color: theme.palette.text.primary,
}}
/>
</ListItemIcon>
<ListItemText
primary={t('core:message.generic.secure_ownership', {
postProcess: 'capitalizeFirstChar',
})}
/>
</ListItem>
</List>
</Box>
</DialogContent>
<DialogActions>
<Button
disabled={isLoadingRegisterName}
variant="contained"
onClick={() => {
setIsOpen(false);
setRegisterNameValue('');
}}
>
{t('core:action.close', { postProcess: 'capitalizeFirstChar' })}
</Button>
<Button
disabled={
!registerNameValue.trim() ||
isLoadingRegisterName ||
isNameAvailable !== Availability.AVAILABLE ||
!balance ||
(balance && nameFee && +balance < +nameFee)
}
variant="contained"
onClick={registerName}
autoFocus
>
{t('core:action.register_name', {
postProcess: 'capitalizeFirstChar',
})}
</Button>
</DialogActions>
</Dialog>
);
};