import { createContext, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { useDropzone } from 'react-dropzone'; import { Box, Button, ButtonBase, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControlLabel, Tooltip, Typography, useTheme, } from '@mui/material'; import { JsonView, allExpanded, darkStyles } from 'react-json-view-lite'; import 'react-json-view-lite/dist/index.css'; import { decryptStoredWallet } from './utils/decryptWallet'; import { CountdownCircleTimer } from 'react-countdown-circle-timer'; import Logo1Dark from './assets/svgs/Logo1Dark.svg'; import RefreshIcon from '@mui/icons-material/Refresh'; import DownloadIcon from '@mui/icons-material/Download'; import ltcLogo from './assets/ltc.png'; import PersonSearchIcon from '@mui/icons-material/PersonSearch'; import qortLogo from './assets/qort.png'; import { Return } from './assets/Icons/Return.tsx'; import WarningIcon from '@mui/icons-material/Warning'; import './utils/seedPhrase/RandomSentenceGenerator'; import EngineeringIcon from '@mui/icons-material/Engineering'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; import PriorityHighIcon from '@mui/icons-material/PriorityHigh'; import { createAccount, saveFileToDisk, saveSeedPhraseToDisk, } from './utils/generateWallet/generateWallet'; import { crypto, walletVersion } from './constants/decryptWallet'; import PhraseWallet from './utils/generateWallet/phrase-wallet'; import { AddressBox, AppContainer, AuthenticatedContainer, AuthenticatedContainerInnerLeft, AuthenticatedContainerInnerRight, CustomButton, CustomButtonAccept, CustomLabel, TextItalic, TextP, TextSpan, } from './styles/App-styles.ts'; import { Spacer } from './common/Spacer'; import { Loader } from './components/Loader'; import { PasswordField, ErrorText } from './components'; import { Group, requestQueueMemberNames } from './components/Group/Group'; import { TaskManager } from './components/TaskManager/TaskManager.tsx'; import { useModal } from './common/useModal'; import { CustomizedSnackbars } from './components/Snackbar/Snackbar'; import SettingsIcon from '@mui/icons-material/Settings'; import LogoutIcon from '@mui/icons-material/Logout'; import HelpIcon from '@mui/icons-material/Help'; import { cleanUrl, getProtocol, getWallets, groupApi, groupApiSocket, storeWallets, } from './background'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, } from './utils/events'; import { requestQueueCommentCount, requestQueuePublishedAccouncements, } from './components/Chat/GroupAnnouncements'; import { requestQueueGroupJoinRequests } from './components/Group/GroupJoinRequests'; import { DrawerComponent } from './components/Drawer/Drawer'; import { AddressQRCode } from './components/AddressQRCode'; import { Settings } from './components/Group/Settings'; import { MainAvatar } from './components/MainAvatar'; import { useRetrieveDataLocalStorage } from './hooks/useRetrieveDataLocalStorage.tsx'; import { useQortalGetSaveSettings } from './hooks/useQortalGetSaveSettings.tsx'; import { canSaveSettingToQdnAtom, enabledDevModeAtom, groupAnnouncementsAtom, groupChatTimestampsAtom, groupsOwnerNamesAtom, groupsPropertiesAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, isRunningPublicNodeAtom, isUsingImportExportSettingsAtom, lastPaymentSeenTimestampAtom, mailsAtom, memberGroupsAtom, mutedGroupsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, timestampEnterDataAtom, txListAtom, } from './atoms/global'; import { NotAuthenticated } from './components/NotAuthenticated.tsx'; import { handleGetFileFromIndexedDB } from './utils/indexedDB'; import { Wallets } from './Wallets'; import { useFetchResources } from './common/useFetchResources'; import { Tutorials } from './components/Tutorials/Tutorials'; import { useHandleTutorials } from './hooks/useHandleTutorials.tsx'; import { useHandleUserInfo } from './hooks/useHandleUserInfo.tsx'; import { Minting } from './components/Minting/Minting'; import { isRunningGateway } from './qortalRequests'; import { QMailStatus } from './components/QMailStatus'; import { GlobalActions } from './components/GlobalActions/GlobalActions'; import { useBlockedAddresses } from './hooks/useBlockUsers.tsx'; import { WalletIcon } from './assets/Icons/WalletIcon'; import { UserLookup } from './components/UserLookup.tsx/UserLookup'; import { RegisterName } from './components/RegisterName'; import { BuyQortInformation } from './components/BuyQortInformation'; import { QortPayment } from './components/QortPayment'; import { GeneralNotifications } from './components/GeneralNotifications'; import { PdfViewer } from './common/PdfViewer'; import ThemeSelector from './components/Theme/ThemeSelector.tsx'; import { Trans, useTranslation } from 'react-i18next'; import LanguageSelector from './components/Language/LanguageSelector.tsx'; import { DownloadWallet } from './components/Auth/DownloadWallet.tsx'; import { CopyIcon } from './assets/Icons/CopyIcon.tsx'; import { SuccessIcon } from './assets/Icons/SuccessIcon.tsx'; import { useAtom, useSetAtom } from 'jotai'; import { useResetAtom } from 'jotai/utils'; type extStates = | 'authenticated' | 'buy-order-submitted' | 'create-wallet' | 'download-wallet' | 'group' | 'not-authenticated' | 'send-qort' | 'transfer-success-regular' | 'transfer-success-request' | 'wallet-dropped' | 'wallets' | 'web-app-request-authentication' | 'web-app-request-buy-order' | 'web-app-request-connection' | 'web-app-request-payment'; interface MyContextInterface { isShow: boolean; onCancel: () => void; onOk: () => void; show: () => void; message: any; } const defaultValues: MyContextInterface = { isShow: false, onCancel: () => {}, onOk: () => {}, show: () => {}, message: { publishFee: '', message: '', }, }; export const allQueues = { requestQueueCommentCount: requestQueueCommentCount, requestQueuePublishedAccouncements: requestQueuePublishedAccouncements, requestQueueMemberNames: requestQueueMemberNames, requestQueueGroupJoinRequests: requestQueueGroupJoinRequests, }; const controlAllQueues = (action) => { Object.keys(allQueues).forEach((key) => { const val = allQueues[key]; try { if (typeof val[action] === 'function') { val[action](); } } catch (error) { console.error(error); } }); }; export const clearAllQueues = () => { Object.keys(allQueues).forEach((key) => { const val = allQueues[key]; try { val.clear(); } catch (error) { console.error(error); } }); }; export const pauseAllQueues = () => { controlAllQueues('pause'); window.sendMessage('pauseAllQueues', {}).catch((error) => { console.error( 'Failed to pause all queues:', error.message || 'An error occurred' ); }); }; export const resumeAllQueues = () => { controlAllQueues('resume'); window.sendMessage('resumeAllQueues', {}).catch((error) => { console.error( 'Failed to resume all queues:', error.message || 'An error occurred' ); }); }; const defaultValuesGlobal = { openTutorialModal: null, setOpenTutorialModal: () => {}, }; export const MyContext = createContext(defaultValues); export let globalApiKey: string | null = null; export const getBaseApiReact = (customApi?: string) => { if (customApi) { return customApi; } if (globalApiKey?.url) { return globalApiKey?.url; } else { return groupApi; } }; export const getArbitraryEndpointReact = () => { if (globalApiKey) { return `/arbitrary/resources/searchsimple`; } else { return `/arbitrary/resources/searchsimple`; } }; export const getBaseApiReactSocket = (customApi?: string) => { if (customApi) { return customApi; } if (globalApiKey?.url) { return `${ getProtocol(globalApiKey?.url) === 'http' ? 'ws://' : 'wss://' }${cleanUrl(globalApiKey?.url)}`; } else { return groupApiSocket; } }; export const isMainWindow = true; function App() { const [extState, setExtstate] = useState('not-authenticated'); const [desktopViewMode, setDesktopViewMode] = useState('home'); const [backupjson, setBackupjson] = useState(null); const [rawWallet, setRawWallet] = useState(null); const [ltcBalanceLoading, setLtcBalanceLoading] = useState(false); const [qortBalanceLoading, setQortBalanceLoading] = useState(false); const [decryptedWallet, setdecryptedWallet] = useState(null); const [requestConnection, setRequestConnection] = useState(null); const [requestBuyOrder, setRequestBuyOrder] = useState(null); const [authenticatedMode, setAuthenticatedMode] = useState('qort'); const [requestAuthentication, setRequestAuthentication] = useState(null); const [userInfo, setUserInfo] = useState(null); const [balance, setBalance] = useState(null); const [ltcBalance, setLtcBalance] = useState(null); const [paymentTo, setPaymentTo] = useState(''); const [paymentAmount, setPaymentAmount] = useState(0); const [paymentPassword, setPaymentPassword] = useState(''); const [sendPaymentError, setSendPaymentError] = useState(''); const [sendPaymentSuccess, setSendPaymentSuccess] = useState(''); const [countdown, setCountdown] = useState(null); const [walletToBeDownloaded, setWalletToBeDownloaded] = useState(null); const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] = useState(''); const [isMain, setIsMain] = useState(true); const isMainRef = useRef(false); const [authenticatePassword, setAuthenticatePassword] = useState(''); const [sendqortState, setSendqortState] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isLoadingSendCoin, setIsLoadingSendCoin] = useState(false); const { t } = useTranslation(['auth', 'core', 'group']); const theme = useTheme(); const [ walletToBeDownloadedPasswordConfirm, setWalletToBeDownloadedPasswordConfirm, ] = useState(''); const [walletToBeDownloadedError, setWalletToBeDownloadedError] = useState(''); const [walletToBeDecryptedError, setWalletToBeDecryptedError] = useState(''); const [isFocused, setIsFocused] = useState(true); const [hasSettingsChanged, setHasSettingsChanged] = useAtom( hasSettingsChangedAtom ); const balanceSetIntervalRef = useRef(null); const downloadResource = useFetchResources(); const holdRefExtState = useRef('not-authenticated'); const isFocusedRef = useRef(true); const { showTutorial, openTutorialModal, shownTutorialsInitiated, setOpenTutorialModal, hasSeenGettingStarted, } = useHandleTutorials(); const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, onOk: onOkUnsavedChanges, show: showUnsavedChanges, message: messageUnsavedChanges, } = useModal(); const { isShow: isShowInfo, onCancel: onCancelInfo, onOk: onOkInfo, show: showInfo, message: messageInfo, } = useModal(); const { onCancel: onCancelQortalRequest, onOk: onOkQortalRequest, show: showQortalRequest, isShow: isShowQortalRequest, message: messageQortalRequest, } = useModal(); const { onCancel: onCancelQortalRequestExtension, onOk: onOkQortalRequestExtension, show: showQortalRequestExtension, isShow: isShowQortalRequestExtension, message: messageQortalRequestExtension, } = useModal(); const setIsRunningPublicNode = useSetAtom(isRunningPublicNodeAtom); const [infoSnack, setInfoSnack] = useState(null); const [openSnack, setOpenSnack] = useState(false); const [hasLocalNode, setHasLocalNode] = useState(false); const [isOpenDrawerProfile, setIsOpenDrawerProfile] = useState(false); const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false); const [apiKey, setApiKey] = useState(''); const [isOpenSendQort, setIsOpenSendQort] = useState(false); const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false); const { isUserBlocked, addToBlockList, removeBlockFromList, getAllBlockedUsers, } = useBlockedAddresses(); const [currentNode, setCurrentNode] = useState({ url: 'http://127.0.0.1:12391', }); const [useLocalNode, setUseLocalNode] = useState(false); const [confirmRequestRead, setConfirmRequestRead] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [showSeed, setShowSeed] = useState(false); const [creationStep, setCreationStep] = useState(1); const getIndividualUserInfo = useHandleUserInfo(); const qortalRequestCheckbox1Ref = useRef(null); useRetrieveDataLocalStorage(userInfo?.address); useQortalGetSaveSettings(userInfo?.name, extState === 'authenticated'); const setIsEnabledDevMode = useSetAtom(enabledDevModeAtom); const setIsDisabledEditorEnter = useSetAtom(isDisabledEditorEnterAtom); const [isOpenMinting, setIsOpenMinting] = useState(false); const generatorRef = useRef(null); const exportSeedphrase = () => { const seedPhrase = generatorRef.current.parsedString; saveSeedPhraseToDisk(seedPhrase); }; const passwordRef = useRef(null); useEffect(() => { if (extState === 'wallet-dropped' && passwordRef.current) { passwordRef.current.focus(); } }, [extState]); useEffect(() => { const isDevModeFromStorage = localStorage.getItem('isEnabledDevMode'); if (isDevModeFromStorage) { setIsEnabledDevMode(JSON.parse(isDevModeFromStorage)); } }, []); useEffect(() => { isRunningGateway() .then((res) => { setIsRunningPublicNode(res); }) .catch((error) => { console.error(error); }); }, [extState]); useEffect(() => { if (!shownTutorialsInitiated) return; if (extState === 'not-authenticated') { showTutorial('create-account'); } else if (extState === 'create-wallet' && walletToBeDownloaded) { showTutorial('important-information'); } else if (extState === 'authenticated') { showTutorial('getting-started'); } }, [extState, walletToBeDownloaded, shownTutorialsInitiated]); //resets for recoil const resetAtomSortablePinnedAppsAtom = useResetAtom(sortablePinnedAppsAtom); const resetAtomIsUsingImportExportSettingsAtom = useResetAtom( isUsingImportExportSettingsAtom ); const resetAtomCanSaveSettingToQdnAtom = useResetAtom( canSaveSettingToQdnAtom ); const resetAtomSettingsQDNLastUpdatedAtom = useResetAtom( settingsQDNLastUpdatedAtom ); const resetAtomSettingsLocalLastUpdatedAtom = useResetAtom( settingsLocalLastUpdatedAtom ); const resetAtomOldPinnedAppsAtom = useResetAtom(oldPinnedAppsAtom); const resetAtomQMailLastEnteredTimestampAtom = useResetAtom( qMailLastEnteredTimestampAtom ); const resetAtomMailsAtom = useResetAtom(mailsAtom); const resetGroupPropertiesAtom = useResetAtom(groupsPropertiesAtom); const resetLastPaymentSeenTimestampAtom = useResetAtom( lastPaymentSeenTimestampAtom ); const resetGroupsOwnerNamesAtom = useResetAtom(groupsOwnerNamesAtom); const resetGroupAnnouncementsAtom = useResetAtom(groupAnnouncementsAtom); const resetMutedGroupsAtom = useResetAtom(mutedGroupsAtom); const resetGroupChatTimestampsAtom = useResetAtom(groupChatTimestampsAtom); const resetTimestampEnterAtom = useResetAtom(timestampEnterDataAtom); const resettxListAtomAtom = useResetAtom(txListAtom); const resetmemberGroupsAtomAtom = useResetAtom(memberGroupsAtom); const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); resetAtomCanSaveSettingToQdnAtom(); resetAtomSettingsQDNLastUpdatedAtom(); resetAtomSettingsLocalLastUpdatedAtom(); resetAtomOldPinnedAppsAtom(); resetAtomIsUsingImportExportSettingsAtom(); resetAtomQMailLastEnteredTimestampAtom(); resetAtomMailsAtom(); resetGroupPropertiesAtom(); resetLastPaymentSeenTimestampAtom(); resetGroupsOwnerNamesAtom(); resetGroupAnnouncementsAtom(); resetMutedGroupsAtom(); resetGroupChatTimestampsAtom(); resetTimestampEnterAtom(); resettxListAtomAtom(); resetmemberGroupsAtomAtom(); }; const contextValue = useMemo( () => ({ isShow, onCancel, onOk, show, userInfo, message, showInfo, openSnackGlobal: openSnack, setOpenSnackGlobal: setOpenSnack, infoSnackCustom: infoSnack, setInfoSnackCustom: setInfoSnack, downloadResource, getIndividualUserInfo, isUserBlocked, addToBlockList, removeBlockFromList, getAllBlockedUsers, showTutorial, openTutorialModal, setOpenTutorialModal, hasSeenGettingStarted, }), [ isShow, onCancel, onOk, show, userInfo, message, showInfo, openSnack, setOpenSnack, infoSnack, setInfoSnack, downloadResource, getIndividualUserInfo, isUserBlocked, addToBlockList, removeBlockFromList, getAllBlockedUsers, showTutorial, openTutorialModal, setOpenTutorialModal, hasSeenGettingStarted, ] ); const handleSetGlobalApikey = (key) => { globalApiKey = key; }; useEffect(() => { try { setIsLoading(true); window .sendMessage('getApiKey') .then((response) => { if (response) { handleSetGlobalApikey(response); setApiKey(response); } }) .catch((error) => { console.error( 'Failed to get API key:', error?.message || 'An error occurred' ); }) .finally(() => { window .sendMessage('getWalletInfo') .then((response) => { if (response && response?.walletInfo) { setRawWallet(response?.walletInfo); if ( holdRefExtState.current === 'web-app-request-payment' || holdRefExtState.current === 'web-app-request-connection' || holdRefExtState.current === 'web-app-request-buy-order' ) return; if (response?.hasKeyPair) { setExtstate('authenticated'); } else { setExtstate('wallet-dropped'); } } }) .catch((error) => { console.error('Failed to get wallet info:', error); }); }); } catch (error) { console.log(error); } finally { setIsLoading(false); } }, []); useEffect(() => { if (extState) { holdRefExtState.current = extState; } }, [extState]); useEffect(() => { try { const val = localStorage.getItem('settings-disable-editor-enter'); if (val) { const parsedVal = JSON.parse(val); if (parsedVal === false || parsedVal === true) { setIsDisabledEditorEnter(parsedVal); } } } catch (error) { console.log(error); } }, []); useEffect(() => { isFocusedRef.current = isFocused; }, [isFocused]); const address = useMemo(() => { if (!rawWallet?.address0) return ''; return rawWallet.address0; }, [rawWallet]); const { getRootProps, getInputProps } = useDropzone({ accept: { 'application/json': ['.json'], // Only accept JSON files }, maxFiles: 1, onDrop: async (acceptedFiles) => { const file: any = acceptedFiles[0]; const fileContents = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onabort = () => reject('File reading was aborted'); reader.onerror = () => reject('File reading has failed'); reader.onload = () => { // Resolve the promise with the reader result when reading completes resolve(reader.result); }; // Read the file as text reader.readAsText(file); }); let error: any = null; let pf: any; try { if (typeof fileContents !== 'string') return; pf = JSON.parse(fileContents); } catch (e) { console.log(error); } try { const requiredFields = [ 'address0', 'salt', 'iv', 'version', 'encryptedSeed', 'mac', 'kdfThreads', ]; for (const field of requiredFields) { if (!(field in pf)) throw new Error( t('auth:message.error.field_not_found_json', { field: field, postProcess: 'capitalizeFirstChar', }) ); } setRawWallet(pf); setExtstate('wallet-dropped'); setdecryptedWallet(null); } catch (e) { console.log(e); } }, }); const saveWalletFunc = async (password: string) => { let wallet = structuredClone(rawWallet); const res = await decryptStoredWallet(password, wallet); const wallet2 = new PhraseWallet(res, wallet?.version || walletVersion); wallet = await wallet2.generateSaveWalletData( password, crypto.kdfThreads, () => {} ); setWalletToBeDownloaded({ wallet, qortAddress: rawWallet.address0, }); return { wallet, qortAddress: rawWallet.address0, }; }; 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 .sendMessage('balance') .then((response) => { if (!response?.error && !isNaN(+response)) { setBalance(response); } setQortBalanceLoading(false); }) .catch((error) => { console.error('Failed to get balance:', error); setQortBalanceLoading(false); }) .finally(() => { balanceSetInterval(); }); }; const getLtcBalanceFunc = () => { setLtcBalanceLoading(true); window .sendMessage('ltcBalance') .then((response) => { if (!response?.error && !isNaN(+response)) { setLtcBalance(response); } setLtcBalanceLoading(false); }) .catch((error) => { console.error('Failed to get LTC balance:', error); setLtcBalanceLoading(false); }); }; const clearAllStates = () => { setRequestConnection(null); setRequestAuthentication(null); }; const qortalRequestPermissonFromExtension = async (message, event) => { if (message.action === 'QORTAL_REQUEST_PERMISSION') { try { if (message?.payload?.checkbox1) { qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1?.value || false; } setConfirmRequestRead(false); await showQortalRequestExtension(message?.payload); if (qortalRequestCheckbox1Ref.current) { event.source.postMessage( { action: 'QORTAL_REQUEST_PERMISSION_RESPONSE', requestId: message?.requestId, result: { accepted: true, checkbox1: qortalRequestCheckbox1Ref.current, }, }, event.origin ); return; } event.source.postMessage( { action: 'QORTAL_REQUEST_PERMISSION_RESPONSE', requestId: message?.requestId, result: { accepted: true, }, }, event.origin ); } catch (error) { event.source.postMessage( { action: 'QORTAL_REQUEST_PERMISSION_RESPONSE', requestId: message?.requestId, result: { accepted: false, }, }, event.origin ); } } }; useEffect(() => { // Handler function for incoming messages const messageHandler = (event) => { if (event.origin !== window.location.origin) { return; } const message = event.data; if (message?.action === 'CHECK_FOCUS') { event.source.postMessage( { action: 'CHECK_FOCUS_RESPONSE', isFocused: isFocusedRef.current }, event.origin ); } else if (message.action === 'NOTIFICATION_OPEN_DIRECT') { executeEvent('openDirectMessage', { from: message.payload.from, }); } else if (message.action === 'NOTIFICATION_OPEN_GROUP') { executeEvent('openGroupMessage', { from: message.payload.from, }); } else if (message.action === 'NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP') { executeEvent('openGroupAnnouncement', { from: message.payload.from, }); } else if (message.action === 'NOTIFICATION_OPEN_THREAD_NEW_POST') { executeEvent('openThreadNewPost', { data: message.payload.data, }); } else if ( message.action === 'QORTAL_REQUEST_PERMISSION' && message?.isFromExtension ) { qortalRequestPermissonFromExtension(message, event); } else if (message?.action === 'getFileFromIndexedDB') { handleGetFileFromIndexedDB(event); } }; // Attach the event listener window.addEventListener('message', messageHandler); // Clean up the event listener on component unmount return () => { window.removeEventListener('message', messageHandler); }; }, []); //param = isDecline const confirmPayment = (isDecline: boolean) => { // REMOVED FOR MOBILE APP }; const confirmBuyOrder = (isDecline: boolean) => { // REMOVED FOR MOBILE APP }; const responseToConnectionRequest = ( isOkay: boolean, hostname: string, interactionId: string ) => { // REMOVED FOR MOBILE APP }; const getUserInfo = useCallback(async (useTimer?: boolean) => { try { if (useTimer) { await new Promise((res) => { setTimeout(() => { res(null); }, 10000); }); } window .sendMessage('userInfo') .then((response) => { if (response && !response.error) { setUserInfo(response); } }) .catch((error) => { console.error('Failed to get user info:', error); }); getBalanceFunc(); } catch (error) { console.log(error); } }, []); useEffect(() => { if (!address) return; getUserInfo(); }, [address]); useEffect(() => { return () => { console.log('exit'); }; }, []); useEffect(() => { if ( authenticatedMode === 'ltc' && !ltcBalanceLoading && ltcBalance === null ) { getLtcBalanceFunc(); } }, [authenticatedMode]); const saveFileToDiskFunc = async () => { try { await saveFileToDisk( walletToBeDownloaded.wallet, walletToBeDownloaded.qortAddress ); } catch (error: any) { setWalletToBeDownloadedError(error?.message); } }; const saveWalletToLocalStorage = async (newWallet) => { try { getWallets() .then((res) => { if (res && Array.isArray(res)) { const wallets = [...res, newWallet]; storeWallets(wallets); } else { storeWallets([newWallet]); } setIsLoading(false); }) .catch((error) => { console.error(error); setIsLoading(false); }); } catch (error) { console.error(error); } }; const createAccountFunc = async () => { try { if (!walletToBeDownloadedPassword) { setWalletToBeDownloadedError( t('core:message.generic.password_enter', { postProcess: 'capitalizeFirstChar', }) ); return; } if (!walletToBeDownloadedPasswordConfirm) { setWalletToBeDownloadedError( t('core:message.generic.password_confirm', { postProcess: 'capitalizeFirstChar', }) ); return; } if ( walletToBeDownloadedPasswordConfirm !== walletToBeDownloadedPassword ) { setWalletToBeDownloadedError( t('core:message.error.password_not_matching', { postProcess: 'capitalizeFirstChar', }) ); return; } setIsLoading(true); await new Promise((res) => { setTimeout(() => { res(); }, 250); }); const res = await createAccount(generatorRef.current.parsedString); const wallet = await res.generateSaveWalletData( walletToBeDownloadedPassword, crypto.kdfThreads, () => {} ); window .sendMessage('decryptWallet', { password: walletToBeDownloadedPassword, wallet, }) .then((response) => { if (response && !response.error) { setRawWallet(wallet); saveWalletToLocalStorage(wallet); setWalletToBeDownloaded({ wallet, qortAddress: wallet.address0, }); window .sendMessage('userInfo') .then((response2) => { setIsLoading(false); if (response2 && !response2.error) { setUserInfo(response2); } }) .catch((error) => { setIsLoading(false); console.error('Failed to get user info:', error); }); getBalanceFunc(); } else if (response?.error) { setIsLoading(false); setWalletToBeDecryptedError(response.error); } }) .catch((error) => { setIsLoading(false); console.error('Failed to decrypt wallet:', error); }); } catch (error: any) { setWalletToBeDownloadedError(error?.message); setIsLoading(false); } }; const logoutFunc = useCallback(async () => { try { if (extState === 'authenticated') { await showUnsavedChanges({ message: t('core:message.question.logout', { postProcess: 'capitalizeFirstChar', }), }); } window .sendMessage('logout', {}) .then((response) => { if (response) { executeEvent('logout-event', {}); resetAllStates(); } }) .catch((error) => { console.error( 'Failed to log out:', error.message || 'An error occurred' ); }); } catch (error) { console.log(error); } }, [hasSettingsChanged, extState]); const returnToMain = () => { setPaymentTo(''); setPaymentAmount(0); setPaymentPassword(''); setSendPaymentError(''); setSendPaymentSuccess(''); setCountdown(null); setWalletToBeDownloaded(null); setWalletToBeDownloadedPassword(''); setShowSeed(false); setCreationStep(1); setExtstate('authenticated'); setIsOpenSendQort(false); setIsOpenSendQortSuccess(false); }; const resetAllStates = () => { setExtstate('not-authenticated'); setAuthenticatedMode('qort'); setBackupjson(null); setRawWallet(null); setdecryptedWallet(null); setRequestConnection(null); setRequestBuyOrder(null); setRequestAuthentication(null); setUserInfo(null); setBalance(null); setLtcBalance(null); setPaymentTo(''); setPaymentAmount(0); setPaymentPassword(''); setSendPaymentError(''); setSendPaymentSuccess(''); setCountdown(null); setWalletToBeDownloaded(null); setWalletToBeDownloadedPassword(''); setShowSeed(false); setCreationStep(1); setWalletToBeDownloadedPasswordConfirm(''); setWalletToBeDownloadedError(''); setSendqortState(null); setHasLocalNode(false); resetAllRecoil(); if (balanceSetIntervalRef?.current) { clearInterval(balanceSetIntervalRef?.current); } }; function roundUpToDecimals(number, decimals = 8) { const factor = Math.pow(10, decimals); // Create a factor based on the number of decimals return Math.ceil(+number * factor) / factor; } const authenticateWallet = async () => { try { setIsLoading(true); setWalletToBeDecryptedError(''); await new Promise((res) => { setTimeout(() => { res(); }, 250); }); window .sendMessage( 'decryptWallet', { password: authenticatePassword, wallet: rawWallet, }, 120000 ) .then((response) => { if (response && !response.error) { setAuthenticatePassword(''); setExtstate('authenticated'); setWalletToBeDecryptedError(''); window .sendMessage('userInfo') .then((response) => { setIsLoading(false); if (response && !response.error) { setUserInfo(response); } }) .catch((error) => { setIsLoading(false); console.error('Failed to get user info:', error); }); getBalanceFunc(); window .sendMessage('getWalletInfo') .then((response) => { if (response && response.walletInfo) { setRawWallet(response.walletInfo); } }) .catch((error) => { console.error('Failed to get wallet info:', error); }); } else if (response?.error) { setIsLoading(false); setWalletToBeDecryptedError(response.error); } }) .catch((error) => { setIsLoading(false); console.error('Failed to decrypt wallet:', error); }); } catch (error) { setWalletToBeDecryptedError( t('core:message.error.password_wrong', { postProcess: 'capitalizeFirstChar', }) ); } }; useEffect(() => { if (!isMainWindow) return; // Handler for when the window gains focus const handleFocus = () => { setIsFocused(true); }; // Handler for when the window loses focus const handleBlur = () => { setIsFocused(false); }; // Attach the event listeners window.addEventListener('focus', handleFocus); window.addEventListener('blur', handleBlur); // Optionally, listen for visibility changes const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { setIsFocused(true); } else { setIsFocused(false); } }; document.addEventListener('visibilitychange', handleVisibilityChange); // Cleanup the event listeners on component unmount return () => { window.removeEventListener('focus', handleFocus); window.removeEventListener('blur', handleBlur); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); const openGlobalSnackBarFunc = (e) => { const message = e.detail?.message; const type = e.detail?.type; setOpenSnack(true); setInfoSnack({ type, message, }); }; useEffect(() => { subscribeToEvent('openGlobalSnackBar', openGlobalSnackBarFunc); return () => { unsubscribeFromEvent('openGlobalSnackBar', openGlobalSnackBarFunc); }; }, []); const openPaymentInternal = (e) => { const directAddress = e.detail?.address; const name = e.detail?.name; setIsOpenSendQort(true); setPaymentTo(name || directAddress); }; useEffect(() => { subscribeToEvent('openPaymentInternal', openPaymentInternal); return () => { unsubscribeFromEvent('openPaymentInternal', openPaymentInternal); }; }, []); const renderProfileLeft = () => { return ( {authenticatedMode === 'qort' && ( {t('core:wallet.litecoin', { postProcess: 'capitalizeAll' })} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.default, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > { setAuthenticatedMode('ltc'); }} src={ltcLogo} style={{ cursor: 'pointer', height: 'auto', width: '20px', }} /> )} {authenticatedMode === 'ltc' && ( {t('core:wallet.qortal', { postProcess: 'capitalizeAll' })} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.default, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > { setAuthenticatedMode('qort'); }} src={qortLogo} style={{ cursor: 'pointer', width: '20px', height: 'auto', }} /> )} {authenticatedMode === 'ltc' ? ( <> { if (rawWallet?.ltcAddress) { navigator.clipboard .writeText(rawWallet.ltcAddress) .catch((err) => { console.error('Failed to copy LTC address:', err); }); } }} > {rawWallet?.ltcAddress?.slice(0, 6)}... {rawWallet?.ltcAddress?.slice(-4)}{' '} {ltcBalanceLoading && ( )} {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( {ltcBalance} LTC )} ) : ( <> {userInfo?.name} { if (rawWallet?.address0) { navigator.clipboard .writeText(rawWallet.address0) .catch((err) => { console.error('Failed to copy address:', err); }); } }} > {rawWallet?.address0?.slice(0, 6)}... {rawWallet?.address0?.slice(-4)}{' '} {qortBalanceLoading && ( )} {!qortBalanceLoading && balance >= 0 && ( {balance?.toFixed(2)} QORT )} {userInfo && !userInfo?.name && ( { executeEvent('openRegisterName', {}); }} > {t('core:action.register_name', { postProcess: 'capitalizeAll', })} )} { setIsOpenSendQort(true); setIsOpenDrawerProfile(false); }} > {t('core:action.transfer_qort', { postProcess: 'capitalizeFirstChar', })} )} { executeEvent('addTab', { data: { service: 'APP', name: 'q-trade' }, }); executeEvent('open-apps-mode', {}); }} > {t('core:action.get_qort', { postProcess: 'capitalizeFirstChar' })} ); }; const renderProfile = () => { return ( {desktopViewMode !== 'apps' && desktopViewMode !== 'dev' && desktopViewMode !== 'chat' && <>{renderProfileLeft()}} { logoutFunc(); setIsOpenDrawerProfile(false); }} > {t('core:action.logout')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > { setIsSettingsOpen(true); }} > {t('core:settings')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > { setIsOpenDrawerLookup(true); }} > {t('core:user_lookup')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > { executeEvent('openWalletsApp', {}); }} > {t('core:wallet.wallet_other')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > {desktopViewMode !== 'home' && ( <> {t('auth:account.your')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > { setIsOpenDrawerProfile(true); }} > )} {extState === 'authenticated' && ( )} {extState === 'authenticated' && isMainWindow && ( <> )} { try { const res = await isRunningGateway(); if (res) throw new Error( t('core:message.generic.no_minting_details', { postProcess: 'capitalizeFirstChar', }) ); setIsOpenMinting(true); } catch (error) { setOpenSnack(true); setInfoSnack({ type: 'error', message: error?.message, }); } }} > {t('core:minting_status')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > {(desktopViewMode === 'apps' || desktopViewMode === 'home') && ( { if (desktopViewMode === 'apps') { showTutorial('qapps', true); } else { showTutorial('getting-started', true); } }} > {t('core:tutorial')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > )} { setExtstate('download-wallet'); setIsOpenDrawerProfile(false); }} > {t('core:action.backup_wallet')} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.paper, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > ); }; return ( {extState === 'not-authenticated' && ( )} {extState === 'authenticated' && isMainWindow && ( {renderProfile()} )} {isOpenSendQort && isMainWindow && ( { setIsOpenSendQort(false); setIsOpenSendQortSuccess(true); }} defaultPaymentTo={paymentTo} /> )} {isShowQortalRequest && !isMainWindow && ( <> {messageQortalRequest?.text1} {messageQortalRequest?.text2 && ( <> {messageQortalRequest?.text2} )} {messageQortalRequest?.text3 && ( <> {messageQortalRequest?.text3} )} {messageQortalRequest?.text4 && ( {messageQortalRequest?.text4} )} {messageQortalRequest?.html && (
)} {messageQortalRequest?.highlightedText} {messageQortalRequest?.fee && ( <> {t('core:message.generic.fee_qort', { message: messageQortalRequest?.fee, postProcess: 'capitalizeFirstChar', })} )} {messageQortalRequest?.checkbox1 && ( { qortalRequestCheckbox1Ref.current = e.target.checked; }} edge="start" tabIndex={-1} disableRipple defaultChecked={messageQortalRequest?.checkbox1?.value} sx={{ '&.Mui-checked': { color: theme.palette.text.secondary, // Customize the color when checked }, '& .MuiSvgIcon-root': { color: theme.palette.text.secondary, }, }} /> {messageQortalRequest?.checkbox1?.label} )} onOkQortalRequest('accepted')} > {t('core:action.accept', { postProcess: 'capitalizeFirstChar', })} onCancelQortalRequest()} > {t('core:action.decline', { postProcess: 'capitalizeFirstChar', })} {sendPaymentError} )} {extState === 'web-app-request-buy-order' && !isMainWindow && ( <> , italic: , span: , }} values={{ hostname: requestBuyOrder?.hostname, count: requestBuyOrder?.crosschainAtInfo?.length || 0, }} tOptions={{ postProcess: ['capitalizeFirst'] }} > The Application
{{ hostname }}
is requesting {{ count }} buy order
{requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { return latest + +cur?.qortAmount; }, 0)}{' '} QORT {t('core:for', { postProcess: 'capitalizeAll' })} {roundUpToDecimals( requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { return latest + +cur?.expectedForeignAmount; }, 0) )} {` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`} confirmBuyOrder(false)} > {t('core:action.accept', { postProcess: 'capitalizeFirstChar', })} confirmBuyOrder(true)} > {t('core:action.decline', { postProcess: 'capitalizeFirstChar', })} {sendPaymentError} )} {extState === 'web-app-request-payment' && !isMainWindow && ( <> , italic: , span: , }} values={{ hostname: requestBuyOrder?.hostname, count: requestBuyOrder?.crosschainAtInfo?.length || 0, }} tOptions={{ postProcess: ['capitalizeFirst'] }} > The Application
{{ hostname }}
is requesting {{ count }} a payment
{sendqortState?.description} {sendqortState?.amount} QORT confirmPayment(false)} > {t('core:action.accept', { postProcess: 'capitalizeFirstChar', })} confirmPayment(true)} > {t('core:action.decline', { postProcess: 'capitalizeFirstChar', })} {sendPaymentError} )} {extState === 'web-app-request-connection' && !isMainWindow && ( <>
The Application

{' '} {requestConnection?.hostname}

is requestion a connection
responseToConnectionRequest( true, requestConnection?.hostname, requestConnection.interactionId ) } > {t('core:action.accept', { postProcess: 'capitalizeFirstChar', })} responseToConnectionRequest( false, requestConnection?.hostname, requestConnection.interactionId ) } > {t('core:action.decline', { postProcess: 'capitalizeFirstChar', })} )} {extState === 'web-app-request-authentication' && !isMainWindow && ( <>
The Application

{' '} {requestConnection?.hostname}

requests authentication
{t('auth:action.authenticate', { postProcess: 'capitalizeFirstChar', })} { setExtstate('create-wallet'); }} > {t('auth:action.create_account', { postProcess: 'capitalizeFirstChar', })} )} {extState === 'wallets' && ( <> { setRawWallet(null); setExtstate('not-authenticated'); logoutFunc(); }} /> )} {rawWallet && extState === 'wallet-dropped' && ( <> { setRawWallet(null); setExtstate('wallets'); logoutFunc(); }} />
{rawWallet?.name ? rawWallet?.name : rawWallet?.address0} {t('auth:action.authenticate', { postProcess: 'capitalizeFirstChar', })} <> {t('auth:wallet.password', { postProcess: 'capitalizeFirstChar', })} setAuthenticatePassword(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { authenticateWallet(); } }} ref={passwordRef} /> {useLocalNode ? ( <> {t('auth:node.using', { postProcess: 'capitalizeFirstChar', })} : {currentNode?.url} ) : ( <> {t('auth:node.using_public', { postProcess: 'capitalizeFirstChar', })} )} {t('auth:action.authenticate', { postProcess: 'capitalizeFirstChar', })} {walletToBeDecryptedError} )} {extState === 'download-wallet' && ( )} {extState === 'create-wallet' && ( <> {!walletToBeDownloaded && ( <> { if (creationStep === 2) { setCreationStep(1); return; } setExtstate('not-authenticated'); setShowSeed(false); setCreationStep(1); setWalletToBeDownloadedPasswordConfirm(''); setWalletToBeDownloadedPassword(''); }} />
{t('auth:action.setup_qortal_account', { postProcess: 'capitalizeFirstChar', })} setShowSeed(true)} style={{ fontSize: '14px', color: 'steelblue', cursor: 'pointer', }} /> ), }} tOptions={{ postProcess: ['capitalizeFirst'] }} > A SEEDPHRASE has been randomly generated in the background. {t('auth:tips.view_seedphrase', { postProcess: 'capitalizeFirstChar', })} ), }} tOptions={{ postProcess: ['capitalizeFirst'] }} > Create your Qortal account by clicking NEXT{' '} below. { setCreationStep(2); }} > {t('core:page.next', { postProcess: 'capitalizeFirstChar', })}
{t('auth:seed_your', { postProcess: 'capitalizeFirstChar', })} {generatorRef.current?.parsedString} {t('auth:action.export_seedphrase', { postProcess: 'capitalizeFirstChar', })}
{t('auth:wallet.password', { postProcess: 'capitalizeFirstChar', })} setWalletToBeDownloadedPassword(e.target.value) } /> {t('auth:wallet.password_confirmation', { postProcess: 'capitalizeFirstChar', })} setWalletToBeDownloadedPasswordConfirm(e.target.value) } /> {t('auth:message.generic.no_minimum_length', { postProcess: 'capitalizeFirstChar', })} {t('auth:action.create_account', { postProcess: 'capitalizeFirstChar', })} {walletToBeDownloadedError} )} {walletToBeDownloaded && ( <> {t('auth:message.generic.congrats_setup', { postProcess: 'capitalizeFirstChar', })} {t('auth:tips.safe_place', { postProcess: 'capitalizeFirstChar', })} { await saveFileToDiskFunc(); returnToMain(); await showInfo({ message: t('auth:tips.wallet_secure', { postProcess: 'capitalizeFirstChar', }), }); }} > {t('core:action.backup_account', { postProcess: 'capitalizeFirstChar', })} )} )} {isOpenSendQortSuccess && ( {t('core:message.success.transfer', { postProcess: 'capitalizeFirstChar', })} { returnToMain(); }} > {t('core:action.continue', { postProcess: 'capitalizeFirstChar', })} )} {extState === 'transfer-success-request' && ( <> {t('core:message.success.transfer', { postProcess: 'capitalizeFirstChar', })} { window.close(); }} > {t('core:action.continue', { postProcess: 'capitalizeFirstChar', })} )} {extState === 'buy-order-submitted' && ( <> {t('core:message.success.order_submitted', { postProcess: 'capitalizeFirstChar', })} { window.close(); }} > {t('core:action.close', { postProcess: 'capitalizeFirstChar' })} )} {countdown && ( { window.close(); }} size={75} strokeWidth={8} > {({ remainingTime }) => {remainingTime}} )} {isLoading && } {isShow && ( {message.paymentFee ? 'Payment' : 'Publish'} {message.message} {message?.paymentFee && ( {t('core:fee.payment', { postProcess: 'capitalizeFirstChar', })} : {message.paymentFee} )} {message?.publishFee && ( {t('core:fee.publish', { postProcess: 'capitalizeFirstChar', })} : {message.publishFee} )} )} {isShowInfo && ( {'Important Info'} {messageInfo.message} )} {isShowUnsavedChanges && ( {t('core:action.logout', { postProcess: 'capitalizeAll' })} {messageUnsavedChanges.message} )} {isShowQortalRequestExtension && isMainWindow && ( { onCancelQortalRequestExtension(); }} size={50} strokeWidth={5} > {({ remainingTime }) => {remainingTime}} {messageQortalRequestExtension?.text1} {messageQortalRequestExtension?.text2 && ( <> {messageQortalRequestExtension?.text2} )} {messageQortalRequestExtension?.text3 && ( <> {messageQortalRequestExtension?.text3} )} {messageQortalRequestExtension?.text4 && ( {messageQortalRequestExtension?.text4} )} {messageQortalRequestExtension?.html && ( <>
)} {messageQortalRequestExtension?.highlightedText} {messageQortalRequestExtension?.json && ( <> )} {messageQortalRequestExtension?.fee && ( <> {'Fee: '} {messageQortalRequestExtension?.fee} {' QORT'} )} {messageQortalRequestExtension?.appFee && ( <> {t('core:message.generic.fee_qort', { message: messageQortalRequestExtension?.appFee, postProcess: 'capitalizeFirstChar', })} )} {messageQortalRequestExtension?.foreignFee && ( <> {t('core:message.generic.foreign_fee', { message: messageQortalRequestExtension?.foreignFee, postProcess: 'capitalizeFirstChar', })} )} {messageQortalRequestExtension?.checkbox1 && ( { qortalRequestCheckbox1Ref.current = e.target.checked; }} edge="start" tabIndex={-1} disableRipple defaultChecked={ messageQortalRequestExtension?.checkbox1?.value } sx={{ '&.Mui-checked': { color: theme.palette.text.secondary, // Customize the color when checked }, '& .MuiSvgIcon-root': { color: theme.palette.text.secondary, }, }} /> {messageQortalRequestExtension?.checkbox1?.label} )} {messageQortalRequestExtension?.confirmCheckbox && ( setConfirmRequestRead(e.target.checked)} checked={confirmRequestRead} edge="start" tabIndex={-1} disableRipple sx={{ '&.Mui-checked': { color: theme.palette.text.secondary, }, '& .MuiSvgIcon-root': { color: theme.palette.text.secondary, }, }} /> } label={ {t('core:message.success.request_read', { postProcess: 'capitalizeFirstChar', })} } /> )} { if ( messageQortalRequestExtension?.confirmCheckbox && !confirmRequestRead ) return; onOkQortalRequestExtension('accepted'); }} > {t('core:action.accept', { postProcess: 'capitalizeFirstChar', })} onCancelQortalRequestExtension()} > {t('core:action.decline', { postProcess: 'capitalizeFirstChar', })} {sendPaymentError}
)} {isSettingsOpen && ( )} {renderProfileLeft()} {extState === 'create-wallet' && walletToBeDownloaded && ( { showTutorial('important-information', true); }} sx={{ bottom: '25px', position: 'fixed', right: '25px', }} > )} {isOpenMinting && ( )} ); } export default App;