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 Copy from './assets/svgs/Copy.svg'; import ltcLogo from './assets/ltc.png'; import PersonSearchIcon from '@mui/icons-material/PersonSearch'; import qortLogo from './assets/qort.png'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { Return } from './assets/Icons/Return.tsx'; import WarningIcon from '@mui/icons-material/Warning'; import Success from './assets/svgs/Success.svg'; 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 { kdf } from './deps/kdf'; import { generateSaveWalletData } from './utils/generateWallet/storeWallet'; 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 './useRetrieveDataLocalStorage'; import { useQortalGetSaveSettings } from './useQortalGetSaveSettings'; import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'; import { canSaveSettingToQdnAtom, enabledDevModeAtom, fullScreenAtom, groupsPropertiesAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, isUsingImportExportSettingsAtom, lastPaymentSeenTimestampAtom, mailsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, } from './atoms/global'; import { useAppFullScreen } from './useAppFullscreen'; import { NotAuthenticated } from './ExtStates/NotAuthenticated'; import { handleGetFileFromIndexedDB } from './utils/indexedDB'; import { Wallets } from './Wallets'; import { useFetchResources } from './common/useFetchResources'; import { Tutorials } from './components/Tutorials/Tutorials'; import { useHandleTutorials } from './components/Tutorials/useHandleTutorials'; import { useHandleUserInfo } from './components/Group/useHandleUserInfo'; import { Minting } from './components/Minting/Minting'; import { isRunningGateway } from './qortalRequests'; import { QMailStatus } from './components/QMailStatus'; import { GlobalActions } from './components/GlobalActions/GlobalActions'; import { useBlockedAddresses } from './components/Group/useBlockUsers'; 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 { useTranslation } from 'react-i18next'; import LanguageSelector from './components/Language/LanguageSelector.tsx'; type extStates = | 'not-authenticated' | 'authenticated' | 'send-qort' | 'web-app-request-connection' | 'web-app-request-payment' | 'web-app-request-authentication' | 'download-wallet' | 'create-wallet' | 'transfer-success-regular' | 'transfer-success-request' | 'wallet-dropped' | 'web-app-request-buy-order' | 'buy-order-submitted' | 'wallets' | 'group'; interface MyContextInterface { txList: any[]; memberGroups: any[]; setTxList: (val) => void; setMemberGroups: (val) => void; isShow: boolean; onCancel: () => void; onOk: () => void; show: () => void; message: any; } const defaultValues: MyContextInterface = { txList: [], memberGroups: [], setTxList: () => {}, setMemberGroups: () => {}, 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 const GlobalContext = createContext(defaultValuesGlobal); 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']); const theme = useTheme(); const [ walletToBeDownloadedPasswordConfirm, setWalletToBeDownloadedPasswordConfirm, ] = useState(''); const [walletToBeDownloadedError, setWalletToBeDownloadedError] = useState(''); const [walletToBeDecryptedError, setWalletToBeDecryptedError] = useState(''); const [txList, setTxList] = useState([]); const [memberGroups, setMemberGroups] = useState([]); const [isFocused, setIsFocused] = useState(true); const [hasSettingsChanged, setHasSettingsChanged] = useRecoilState( 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 [isRunningPublicNode, setIsRunningPublicNode] = useState(false); 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 [rootHeight, setRootHeight] = useState('100%'); 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 [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom); const setIsDisabledEditorEnter = useSetRecoilState(isDisabledEditorEnterAtom); const [isOpenMinting, setIsOpenMinting] = useState(false); const { toggleFullScreen } = useAppFullScreen(setFullScreen); 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]); useEffect(() => { // Attach a global event listener for double-click const handleDoubleClick = () => { toggleFullScreen(); }; // Add the event listener to the root HTML document document.documentElement.addEventListener('dblclick', handleDoubleClick); // Clean up the event listener on unmount return () => { document.documentElement.removeEventListener( 'dblclick', handleDoubleClick ); }; }, [toggleFullScreen]); //resets for recoil const resetAtomSortablePinnedAppsAtom = useResetRecoilState( sortablePinnedAppsAtom ); const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState( isUsingImportExportSettingsAtom ); const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState( canSaveSettingToQdnAtom ); const resetAtomSettingsQDNLastUpdatedAtom = useResetRecoilState( settingsQDNLastUpdatedAtom ); const resetAtomSettingsLocalLastUpdatedAtom = useResetRecoilState( settingsLocalLastUpdatedAtom ); const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState( qMailLastEnteredTimestampAtom ); const resetAtomMailsAtom = useResetRecoilState(mailsAtom); const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom); const resetLastPaymentSeenTimestampAtom = useResetRecoilState( lastPaymentSeenTimestampAtom ); const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); resetAtomCanSaveSettingToQdnAtom(); resetAtomSettingsQDNLastUpdatedAtom(); resetAtomSettingsLocalLastUpdatedAtom(); resetAtomOldPinnedAppsAtom(); resetAtomIsUsingImportExportSettingsAtom(); resetAtomQMailLastEnteredTimestampAtom(); resetAtomMailsAtom(); resetGroupPropertiesAtom(); resetLastPaymentSeenTimestampAtom(); }; 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' ); // TODO translate }) .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(field + ' not found in JSON'); } setRawWallet(pf); // setExtstate("authenticated"); 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 confirmPasswordToDownload = async () => { try { setWalletToBeDownloadedError(''); if (!walletToBeDownloadedPassword) { setSendPaymentError('Please enter your password'); return; } setIsLoading(true); await new Promise((res) => { setTimeout(() => { res(); }, 250); }); const res = await saveWalletFunc(walletToBeDownloadedPassword); } catch (error: any) { setWalletToBeDownloadedError(error?.message); } finally { setIsLoading(false); } }; 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('Please enter a password'); return; } if (!walletToBeDownloadedPasswordConfirm) { setWalletToBeDownloadedError('Please confirm your password'); return; } if ( walletToBeDownloadedPasswordConfirm !== walletToBeDownloadedPassword ) { setWalletToBeDownloadedError('Password fields do not match!'); 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 = async () => { try { if (hasSettingsChanged) { await showUnsavedChanges({ message: 'Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.', }); // TODO translate } else if (extState === 'authenticated') { await showUnsavedChanges({ message: 'Are you sure you would like to logout?', }); } 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); } }; 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); setTxList([]); setMemberGroups([]); 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('Unable to authenticate. Wrong password'); } }; 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' && ( LITECOIN WALLET } 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', width: '20px', height: 'auto', }} /> )} {authenticatedMode === 'ltc' && ( QORTAL WALLET } 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' ? ( <> {rawWallet?.ltcAddress?.slice(0, 6)}... {rawWallet?.ltcAddress?.slice(-4)} {ltcBalanceLoading && ( )} {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( {ltcBalance} LTC )} ) : ( <> {userInfo?.name} {rawWallet?.address0?.slice(0, 6)}... {rawWallet?.address0?.slice(-4)} {qortBalanceLoading && ( )} {!qortBalanceLoading && balance >= 0 && ( {balance?.toFixed(2)} QORT )} {userInfo && !userInfo?.name && ( { executeEvent('openRegisterName', {}); }} > REGISTER NAME )} { setIsOpenSendQort(true); // setExtstate("send-qort"); setIsOpenDrawerProfile(false); }} > Transfer QORT )} { executeEvent('addTab', { data: { service: 'APP', name: 'q-trade' }, }); executeEvent('open-apps-mode', {}); }} > Get QORT at Q-Trade ); }; 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.default, }, }, 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.default, }, }, 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.default, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > { executeEvent('openWalletsApp', {}); }} > {t('core:wallet_other')} } 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, }, }, }} > {desktopViewMode !== 'home' && ( <> {t('auth:account.your')} } 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, }, }, }} > { setIsOpenDrawerProfile(true); }} > )} {extState === 'authenticated' && ( )} {extState === 'authenticated' && isMainWindow && ( )} { try { const res = await isRunningGateway(); if (res) throw new Error( 'Cannot view minting details on the gateway' ); 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.default, }, }, 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.default, }, }, 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.default, }, }, arrow: { sx: { color: theme.palette.text.primary, }, }, }} > ); }; return ( {extState === 'not-authenticated' && ( )} {/* {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 && ( <> {'Fee: '} {messageQortalRequest?.fee} {' QORT'} )} {messageQortalRequest?.checkbox1 && ( { qortalRequestCheckbox1Ref.current = e.target.checked; }} edge="start" tabIndex={-1} disableRipple defaultChecked={messageQortalRequest?.checkbox1?.value} sx={{ '&.Mui-checked': { color: 'white', // Customize the color when checked }, '& .MuiSvgIcon-root': { color: 'white', }, }} /> {messageQortalRequest?.checkbox1?.label} )} onOkQortalRequest('accepted')} > accept onCancelQortalRequest()} > decline {sendPaymentError} )} {extState === 'web-app-request-buy-order' && !isMainWindow && ( <> The Application

{' '} {requestBuyOrder?.hostname}

is requesting {requestBuyOrder?.crosschainAtInfo?.length}{' '} {`buy order${ requestBuyOrder?.crosschainAtInfo.length === 1 ? '' : 's' }`}
{requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { return latest + +cur?.qortAmount; }, 0)}{' '} QORT FOR {roundUpToDecimals( requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { return latest + +cur?.expectedForeignAmount; }, 0) )} {` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`} confirmBuyOrder(false)} > accept confirmBuyOrder(true)} > decline {sendPaymentError} )} {extState === 'web-app-request-payment' && !isMainWindow && ( <> The Application

{' '} {sendqortState?.hostname}

is requesting a payment
{sendqortState?.description} {sendqortState?.amount} QORT confirmPayment(false)} > accept confirmPayment(true)} > decline {sendPaymentError} )} {extState === 'web-app-request-connection' && !isMainWindow && ( <>
The Application

{' '} {requestConnection?.hostname}

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

{' '} {requestConnection?.hostname}

requests authentication
Authenticate { setExtstate('create-wallet'); }} > {t('auth:create_account', { postProcess: 'capitalize' })} )} {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:authenticate', { postProcess: 'capitalize' })} <> {t('auth:wallet.password', { postProcess: 'capitalize' })} setAuthenticatePassword(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { authenticateWallet(); } }} ref={passwordRef} /> {useLocalNode ? ( <> {t('auth:node.using', { postProcess: 'capitalize' })}:{' '} {currentNode?.url} ) : ( <> {t('auth:node.using_public', { postProcess: 'capitalize' })} )} {t('auth:authenticate', { postProcess: 'capitalize' })} {walletToBeDecryptedError} )} {extState === 'download-wallet' && ( <>
{t('auth:download_account', { postProcess: 'capitalize' })} {!walletToBeDownloaded && ( <> {t('auth:wallet.password_confirmation', { postProcess: 'capitalize', })} setWalletToBeDownloadedPassword(e.target.value) } /> {t('auth:password_confirmation', { postProcess: 'capitalize', })} {walletToBeDownloadedError} )} {walletToBeDownloaded && ( <> { await saveFileToDiskFunc(); await showInfo({ message: t('auth:keep_secure', { postProcess: 'capitalize', }), }); }} > {t('auth:download_account', { postProcess: 'capitalize', })} )} )} {extState === 'create-wallet' && ( <> {!walletToBeDownloaded && ( <> { if (creationStep === 2) { setCreationStep(1); return; } setExtstate('not-authenticated'); setShowSeed(false); setCreationStep(1); setWalletToBeDownloadedPasswordConfirm(''); setWalletToBeDownloadedPassword(''); }} />
Set up your Qortal account A ‘{' '} { setShowSeed(true); }} style={{ fontSize: '14px', color: 'steelblue', cursor: 'pointer', }} > SEEDPHRASE {' '} ’ has been randomly generated in the background. If you wish to VIEW THE SEEDPHRASE, click the word 'SEEDPHRASE' in this text. Seedphrases are used to generate the private key for your Qortal account. For security by default, seedphrases are NOT displayed unless specifically chosen. Create your Qortal account by clicking{' '} NEXT {' '} below. { setCreationStep(2); }} > Next
Your seedphrase {generatorRef.current?.parsedString} Export Seedphrase
Wallet Password setWalletToBeDownloadedPassword(e.target.value) } /> Confirm Wallet Password setWalletToBeDownloadedPasswordConfirm(e.target.value) } /> There is no minimum length requirement {t('auth:create_account', { postProcess: 'capitalize' })} {walletToBeDownloadedError} )} {walletToBeDownloaded && ( <> Congrats, you’re all set up! Save your account in a place where you will remember it! { await saveFileToDiskFunc(); returnToMain(); await showInfo({ message: `Keep your wallet file secure.`, }); }} > {t('core:action.backup_account', { postProcess: 'capitalize', })} )} )} {isOpenSendQortSuccess && ( {t('core:result.success.transfer', { postProcess: 'capitalize', })} { returnToMain(); }} > {t('core:action.continue', { postProcess: 'capitalize' })} )} {extState === 'transfer-success-request' && ( <> {t('core:result.success.transfer', { postProcess: 'capitalize', })} { window.close(); }} > {t('core:action.continue', { postProcess: 'capitalize' })} )} {extState === 'buy-order-submitted' && ( <> {t('core:result.success.order_submitted', { postProcess: 'capitalize', })} { window.close(); }} > {t('core:action.close', { postProcess: 'capitalize' })} // TODO translate )} {countdown && ( {/* */} { window.close(); }} size={75} strokeWidth={8} > {({ remainingTime }) => {remainingTime}} )} {isLoading && } {isShow && ( {message.paymentFee ? 'Payment' : 'Publish'} {message.message} {message?.paymentFee && ( {t('core:fee.payment', { postProcess: 'capitalize', })} : {message.paymentFee} )} {message?.publishFee && ( {t('core:fee.publish', { postProcess: 'capitalize', })} : {message.publishFee} )} )} {isShowInfo && ( {'Important Info'} {messageInfo.message} )} {isShowUnsavedChanges && ( {'LOGOUT'} {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 && ( <> {'App Fee: '} {messageQortalRequestExtension?.appFee} {' QORT'} )} {messageQortalRequestExtension?.foreignFee && ( <> {'Foreign Fee: '} {messageQortalRequestExtension?.foreignFee} )} {messageQortalRequestExtension?.checkbox1 && ( { qortalRequestCheckbox1Ref.current = e.target.checked; }} edge="start" tabIndex={-1} disableRipple defaultChecked={ messageQortalRequestExtension?.checkbox1?.value } sx={{ '&.Mui-checked': { color: 'white', // Customize the color when checked }, '& .MuiSvgIcon-root': { color: 'white', }, }} /> {messageQortalRequestExtension?.checkbox1?.label} )} {messageQortalRequestExtension?.confirmCheckbox && ( setConfirmRequestRead(e.target.checked)} checked={confirmRequestRead} edge="start" tabIndex={-1} disableRipple sx={{ '&.Mui-checked': { color: 'white', }, '& .MuiSvgIcon-root': { color: 'white', }, }} /> } label={ {t('core:result.success.request_read', { postProcess: 'capitalize', })} } /> )} { if ( messageQortalRequestExtension?.confirmCheckbox && !confirmRequestRead ) return; onOkQortalRequestExtension('accepted'); }} > {t('core:action.accept', { postProcess: 'capitalize', })} onCancelQortalRequestExtension()} > {t('core:action.decline', { postProcess: 'capitalize', })} {sendPaymentError}
)} {isSettingsOpen && ( )} {renderProfileLeft()} {extState === 'create-wallet' && walletToBeDownloaded && ( { showTutorial('important-information', true); }} sx={{ position: 'fixed', bottom: '25px', right: '25px', }} > )} {isOpenMinting && ( )} ); } export default App;