From e02ac2d8693741a8ba8959970bb59e19b234cbec Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sat, 1 Mar 2025 22:38:19 +0200 Subject: [PATCH] register name dialog redesigned --- src/App.tsx | 161 +++-------- src/components/BuyQortInformation.tsx | 154 ++++++++++ src/components/Group/Group.tsx | 1 + src/components/Group/HomeDesktop.tsx | 43 ++- src/components/Group/ThingsToDoInitial.tsx | 47 +--- src/components/RegisterName.tsx | 312 +++++++++++++++++++++ src/main.tsx | 4 + 7 files changed, 570 insertions(+), 152 deletions(-) create mode 100644 src/components/BuyQortInformation.tsx create mode 100644 src/components/RegisterName.tsx diff --git a/src/App.tsx b/src/App.tsx index 7df8043..5eca049 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -149,6 +149,8 @@ import { useBlockedAddresses } from "./components/Group/useBlockUsers"; import { WalletIcon } from "./assets/Icons/WalletIcon"; import { DrawerUserLookup } from "./components/Drawer/DrawerUserLookup"; import { UserLookup } from "./components/UserLookup.tsx/UserLookup"; +import { RegisterName } from "./components/RegisterName"; +import { BuyQortInformation } from "./components/BuyQortInformation"; type extStates = | "not-authenticated" @@ -361,6 +363,7 @@ function App() { const [hasSettingsChanged, setHasSettingsChanged] = useRecoilState( hasSettingsChangedAtom ); + const balanceSetIntervalRef = useRef(null) const {downloadResource} = useFetchResources() const holdRefExtState = useRef("not-authenticated"); const isFocusedRef = useRef(true); @@ -396,10 +399,7 @@ function App() { message: messageQortalRequestExtension, } = useModal(); - const [openRegisterName, setOpenRegisterName] = useState(false); - const registerNamePopoverRef = useRef(null); - const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false); - const [registerNameValue, setRegisterNameValue] = useState(""); + const [infoSnack, setInfoSnack] = useState(null); const [openSnack, setOpenSnack] = useState(false); const [hasLocalNode, setHasLocalNode] = useState(false); @@ -702,6 +702,35 @@ function App() { }; }; + + const balanceSetInterval = ()=> { + try { + if(balanceSetIntervalRef?.current){ + clearInterval(balanceSetIntervalRef?.current); + } + + let isCalling = false; + balanceSetIntervalRef.current = setInterval(async () => { + if (isCalling) return; + isCalling = true; + window + .sendMessage("balance") + .then((response) => { + if (!response?.error && !isNaN(+response)) { + setBalance(response); + } + isCalling = false; + }) + .catch((error) => { + console.error("Failed to get balance:", error); + isCalling = false; + }); + }, 40000); + } catch (error) { + console.error(error) + } + } + const getBalanceFunc = () => { setQortBalanceLoading(true); window @@ -710,11 +739,14 @@ function App() { if (!response?.error && !isNaN(+response)) { setBalance(response); } + setQortBalanceLoading(false); }) .catch((error) => { console.error("Failed to get balance:", error); setQortBalanceLoading(false); + }).finally(()=> { + balanceSetInterval() }); }; const getLtcBalanceFunc = () => { @@ -1121,6 +1153,9 @@ function App() { setTxList([]); setMemberGroups([]); resetAllRecoil(); + if(balanceSetIntervalRef?.current){ + clearInterval(balanceSetIntervalRef?.current); + } }; function roundUpToDecimals(number, decimals = 8) { @@ -1275,72 +1310,7 @@ function App() { }; }, []); - const registerName = async () => { - try { - if (!userInfo?.address) throw new Error("Your address was not found"); - if(!registerNameValue) throw new Error('Enter a name') - const fee = await getFee("REGISTER_NAME"); - await show({ - message: "Would you like to register this name?", - 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: - "Successfully registered. It may take a couple of minutes for the changes to propagate", - }); - setOpenRegisterName(false); - setRegisterNameValue(""); - setOpenSnack(true); - setTxList((prev) => [ - { - ...response, - type: "register-name", - label: `Registered name: awaiting confirmation. This may take a couple minutes.`, - labelDone: `Registered name: success!`, - 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 || "An error occurred", - }); - setOpenSnack(true); - rej(error); - }); - }); - } catch (error) { - if (error?.message) { - setInfoSnack({ - type: "error", - message: error?.message, - }); - } - } finally { - setIsLoadingRegisterName(false); - } - }; + const renderProfileLeft = ()=> { @@ -1527,7 +1497,6 @@ function App() { {userInfo && !userInfo?.name && ( { - setOpenRegisterName(true); + executeEvent('openRegisterName', {}) }} > REGISTER NAME @@ -3616,52 +3585,6 @@ function App() { )} - { - setOpenRegisterName(false); - setRegisterNameValue(""); - }} - anchorOrigin={{ - vertical: "bottom", - horizontal: "center", - }} - transformOrigin={{ - vertical: "top", - horizontal: "center", - }} - style={{ marginTop: "8px" }} - > - - - setRegisterNameValue(e.target.value)} - value={registerNameValue} - placeholder="Choose a name" - /> - - - Register Name - - - {isSettingsOpen && ( )} @@ -3678,6 +3601,8 @@ function App() { {renderProfileLeft()} + + {extState === "create-wallet" && walletToBeDownloaded && ( { diff --git a/src/components/BuyQortInformation.tsx b/src/components/BuyQortInformation.tsx new file mode 100644 index 0000000..b6aed75 --- /dev/null +++ b/src/components/BuyQortInformation.tsx @@ -0,0 +1,154 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react' +import { + Avatar, + Box, + Button, + ButtonBase, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Input, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemIcon, + ListItemText, + List, + MenuItem, + Popover, + Select, + TextField, + Typography, + } from "@mui/material"; +import { Label } from './Group/AddGroup'; +import { Spacer } from '../common/Spacer'; +import { LoadingButton } from '@mui/lab'; +import { getBaseApiReact, MyContext } from '../App'; +import { getFee } from '../background'; +import qTradeLogo from "../assets/Icons/q-trade-logo.webp"; + +import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked'; +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../utils/events'; +import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner'; + + + +export const BuyQortInformation = ({balance}) => { + const [isOpen, setIsOpen] = useState(false) + + const openBuyQortInfoFunc = useCallback((e) => { + setIsOpen(true) + + }, [ setIsOpen]); + + useEffect(() => { + subscribeToEvent("openBuyQortInfo", openBuyQortInfoFunc); + + return () => { + unsubscribeFromEvent("openBuyQortInfo", openBuyQortInfoFunc); + }; + }, [openBuyQortInfoFunc]); + + return ( + + + {"Get QORT"} + + + + Get QORT using Qortal's crosschain trade portal + { + executeEvent("addTab", { + data: { service: "APP", name: "q-trade" }, + }); + executeEvent("open-apps-mode", {}); + setIsOpen(false) + }} + > + + + Trade QORT + + + + Benefits of having QORT + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index c4c7622..c2a42df 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -2528,6 +2528,7 @@ export const Group = ({ { + const [checked1, setChecked1] = React.useState(false); + const [checked2, setChecked2] = React.useState(false); + React.useEffect(() => { + if (balance && +balance >= 6) { + setChecked1(true); + } + }, [balance]); + + + React.useEffect(() => { + if (name) setChecked2(true); + }, [name]); + + + const isLoaded = React.useMemo(()=> { + if(userInfo !== null) return true + return false + }, [ userInfo]) + + const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { + if(isLoaded && checked1 && checked2) return true + return false + }, [checked1, isLoaded, checked2]) + + return ( */} - + )} + + + )} @@ -185,6 +216,9 @@ export const HomeDesktop = ({ {" "} + {!hasDoneNameAndBalanceAndIsLoaded && ( + + )} - + {hasDoneNameAndBalanceAndIsLoaded && ( + + + )} + + diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx index 0a02f36..367b3b1 100644 --- a/src/components/Group/ThingsToDoInitial.tsx +++ b/src/components/Group/ThingsToDoInitial.tsx @@ -12,27 +12,17 @@ import { Box, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { isMobile } from "../../App"; import { QMailMessages } from "./QMailMessages"; +import { executeEvent } from "../../utils/events"; export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => { const [checked1, setChecked1] = React.useState(false); const [checked2, setChecked2] = React.useState(false); - const [checked3, setChecked3] = React.useState(false); + // const [checked3, setChecked3] = React.useState(false); - // const getAddressInfo = async (address) => { - // const response = await fetch(getBaseApiReact() + "/addresses/" + address); - // const data = await response.json(); - // if (data.error && data.error === 124) { - // setChecked1(false); - // } else if (data.address) { - // setChecked1(true); - // } - // }; + // React.useEffect(() => { + // if (hasGroups) setChecked3(true); + // }, [hasGroups]); - // const checkInfo = async () => { - // try { - // getAddressInfo(myAddress); - // } catch (error) {} - // }; React.useEffect(() => { if (balance && +balance >= 6) { @@ -40,9 +30,6 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf } }, [balance]); - React.useEffect(() => { - if (hasGroups) setChecked3(true); - }, [hasGroups]); React.useEffect(() => { if (name) setChecked2(true); @@ -120,11 +107,14 @@ if(!isLoaded) return null disableRipple role={undefined} dense + onClick={()=> { + executeEvent("openBuyQortInfo", {}) + }} > - { + executeEvent('openRegisterName', {}) + }} sx={{ "& .MuiTypography-root": { - fontSize: "13px", + fontSize: "1rem", fontWeight: 400, }, }} primary={`Register a name`} /> @@ -202,16 +194,7 @@ if(!isLoaded) return null - - // - // - // } + {/* - + */} )} diff --git a/src/components/RegisterName.tsx b/src/components/RegisterName.tsx new file mode 100644 index 0000000..35af458 --- /dev/null +++ b/src/components/RegisterName.tsx @@ -0,0 +1,312 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react' +import { + Avatar, + Box, + Button, + ButtonBase, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Input, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemIcon, + ListItemText, + List, + MenuItem, + Popover, + Select, + TextField, + Typography, + } from "@mui/material"; +import { Label } from './Group/AddGroup'; +import { Spacer } from '../common/Spacer'; +import { LoadingButton } from '@mui/lab'; +import { getBaseApiReact, MyContext } 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'; + +enum Availability { + NULL = 'null', + LOADING = 'loading', + AVAILABLE = 'available', + NOT_AVAILABLE = 'not-available' +} +export const RegisterName = ({setOpenSnack, setInfoSnack, userInfo, show, setTxList, balance}) => { + const [isOpen, setIsOpen] = useState(false) + const [registerNameValue, setRegisterNameValue] = useState('') + const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false) + const [isNameAvailable, setIsNameAvailable] = useState(Availability.NULL) + const [nameFee, setNameFee] = useState(null) + + 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) + } finally { + } + } + // 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("Your address was not found"); + if(!registerNameValue) throw new Error('Enter a name') + + const fee = await getFee("REGISTER_NAME"); + await show({ + message: "Would you like to register this name?", + 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: + "Successfully registered. It may take a couple of minutes for the changes to propagate", + }); + setIsOpen(false); + setRegisterNameValue(""); + setOpenSnack(true); + setTxList((prev) => [ + { + ...response, + type: "register-name", + label: `Registered name: awaiting confirmation. This may take a couple minutes.`, + labelDone: `Registered name: success!`, + 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 || "An error occurred", + }); + setOpenSnack(true); + rej(error); + }); + }); + } catch (error) { + if (error?.message) { + setOpenSnack(true) + setInfoSnack({ + type: "error", + message: error?.message, + }); + } + } finally { + setIsLoadingRegisterName(false); + } + }; + + return ( + + + {"Register name"} + + + + + setRegisterNameValue(e.target.value)} + value={registerNameValue} + placeholder="Choose a name" + /> + {(!balance || (nameFee && balance && balance < nameFee))&& ( + <> + + + + Your balance is {balance ?? 0} QORT. A name registration requires a {nameFee} QORT fee + + + + + )} + + {isNameAvailable === Availability.AVAILABLE && ( + + + {registerNameValue} is available + + )} + {isNameAvailable === Availability.NOT_AVAILABLE && ( + + + {registerNameValue} is unavailable + + )} + {isNameAvailable === Availability.LOADING && ( + + + Checking if name already existis + + )} + + Benefits of a name + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/main.tsx b/src/main.tsx index 0531491..dcaf3ca 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -24,6 +24,10 @@ const theme = createTheme({ secondary: '#b0b0b0', // Light gray for secondary text disabled: '#808080', // Gray for disabled text }, + action: { + // disabledBackground: 'set color of background here', + disabled: 'rgb(255 255 255 / 70%)' + } }, typography: { fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif', // Font family