diff --git a/packages/api/src/services/WalletService.ts b/packages/api/src/services/WalletService.ts index f5188c879e..a9c04c5d47 100644 --- a/packages/api/src/services/WalletService.ts +++ b/packages/api/src/services/WalletService.ts @@ -132,7 +132,7 @@ export default class Wallet extends Service { } async logIn(args: { - fingerprint: string; + fingerprint: number; type?: 'normal' | 'skip' | 'restore_backup'; // skip is used to skip import }) { const { fingerprint, type = 'normal' } = args; diff --git a/packages/core/src/components/Auth/AuthProvider.tsx b/packages/core/src/components/Auth/AuthProvider.tsx new file mode 100644 index 0000000000..7c3c4b02ec --- /dev/null +++ b/packages/core/src/components/Auth/AuthProvider.tsx @@ -0,0 +1,96 @@ +import { useClearCache, useLogInMutation, useGetLoggedInFingerprintQuery } from '@chia-network/api-react'; +import React, { useMemo, useCallback, useRef, useState, useEffect, createContext, type ReactNode } from 'react'; + +export const AuthContext = createContext< + | { + logIn: (fingerprint: number) => Promise; + logOut: () => Promise; + fingerprint?: number; + isLoading: boolean; + } + | undefined +>(undefined); + +export type AuthProviderProps = { + children: ReactNode; +}; + +export default function AuthProvider(props: AuthProviderProps) { + const { children } = props; + const [isLoading, setIsLoading] = useState(false); + const [fingerprint, setFingerprint] = useState(); + const { data: currentFingerprint } = useGetLoggedInFingerprintQuery(); + const [logIn] = useLogInMutation(); + const clearCache = useClearCache(); + + const isLoadingRef = useRef(isLoading); + isLoadingRef.current = isLoading; + + const processNewFingerprint = useCallback( + async (newFingerprint: number) => { + if (!isLoadingRef.current) { + try { + setIsLoading(true); + await clearCache(); + setFingerprint(newFingerprint); + } finally { + setIsLoading(false); + } + } + }, + [clearCache] + ); + + // automatically log in if we have a fingerprint already and logIn is not in progress + useEffect(() => { + if (!!currentFingerprint && currentFingerprint !== fingerprint) { + processNewFingerprint(currentFingerprint); + } + }, [currentFingerprint, fingerprint, processNewFingerprint]); + + // immutable + const handleLogOut = useCallback(async () => { + // do nothing until backend change API, + // user is still logged in and syncing with previously selected fingerprint + }, []); + + // immutable + const handleLogIn = useCallback( + async (logInFingerprint: number) => { + try { + if (isLoadingRef.current) { + return; + } + + setIsLoading(true); + + handleLogOut(); + + await logIn({ + fingerprint: logInFingerprint, + type: 'skip', + }).unwrap(); + + // all data are from previous fingerprint, so we need to clear cache, + // invalidateTags just do refetch and showing old data until new data are fetched + await clearCache(); + setFingerprint(logInFingerprint); + } finally { + setIsLoading(false); + } + }, + [handleLogOut, logIn, clearCache] + ); + + const context = useMemo( + () => ({ + logIn: handleLogIn, + logOut: handleLogOut, + fingerprint, + isLoading, + }), + [handleLogIn, handleLogOut, fingerprint, isLoading] + ); + + return {children}; +} diff --git a/packages/core/src/components/Auth/index.ts b/packages/core/src/components/Auth/index.ts new file mode 100644 index 0000000000..75e8d25ec8 --- /dev/null +++ b/packages/core/src/components/Auth/index.ts @@ -0,0 +1 @@ +export { default as AuthProvider } from './AuthProvider'; diff --git a/packages/core/src/components/LayoutDashboard/LayoutDashboard.tsx b/packages/core/src/components/LayoutDashboard/LayoutDashboard.tsx index fca7698569..83b5582b80 100644 --- a/packages/core/src/components/LayoutDashboard/LayoutDashboard.tsx +++ b/packages/core/src/components/LayoutDashboard/LayoutDashboard.tsx @@ -18,6 +18,7 @@ import React, { type ReactNode, useState, Suspense, useCallback } from 'react'; import { useNavigate, Outlet } from 'react-router-dom'; import styled from 'styled-components'; +import useAuth from '../../hooks/useAuth'; import useGetLatestVersionFromWebsite from '../../hooks/useGetLatestVersionFromWebsite'; import useOpenDialog from '../../hooks/useOpenDialog'; import EmojiAndColorPicker from '../../screens/SelectKey/EmojiAndColorPicker'; @@ -87,6 +88,7 @@ export default function LayoutDashboard(props: LayoutDashboardProps) { const { children, sidebar, settings, outlet = false, actions } = props; const navigate = useNavigate(); + const { logOut } = useAuth(); const [editWalletName, setEditWalletName] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const { data: fingerprint, isLoading: isLoadingFingerprint } = useGetLoggedInFingerprintQuery(); @@ -131,8 +133,7 @@ export default function LayoutDashboard(props: LayoutDashboardProps) { }, [openDialog, appVersion]); async function handleLogout() { - localStorage.setItem('visibilityFilters', JSON.stringify(['visible'])); - localStorage.setItem('typeFilter', JSON.stringify([])); + await logOut(); navigate('/'); } diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 070380f445..a62fa26345 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -4,6 +4,7 @@ export { default as AdvancedOptions } from './AdvancedOptions'; export { default as AlertDialog } from './AlertDialog'; export { default as Amount } from './Amount'; export { default as AspectRatio } from './AspectRatio'; +export { AuthProvider } from './Auth'; export { default as Autocomplete } from './Autocomplete'; export { default as Back } from './Back'; export { default as Button } from './Button'; diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts index 8a5524f662..0141730558 100644 --- a/packages/core/src/hooks/index.ts +++ b/packages/core/src/hooks/index.ts @@ -1,4 +1,5 @@ export { default as useAppVersion } from './useAppVersion'; +export { default as useAuth } from './useAuth'; export { default as useDarkMode } from './useDarkMode'; export { default as useHiddenList } from './useHiddenList'; export { default as useCurrencyCode } from './useCurrencyCode'; diff --git a/packages/core/src/hooks/useAuth.ts b/packages/core/src/hooks/useAuth.ts new file mode 100644 index 0000000000..3d16dcf3c3 --- /dev/null +++ b/packages/core/src/hooks/useAuth.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react'; + +import { AuthContext } from '../components/Auth/AuthProvider'; + +export default function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +} diff --git a/packages/core/src/screens/SelectKey/SelectKey.tsx b/packages/core/src/screens/SelectKey/SelectKey.tsx index 49c6bfaadc..4b660273f9 100644 --- a/packages/core/src/screens/SelectKey/SelectKey.tsx +++ b/packages/core/src/screens/SelectKey/SelectKey.tsx @@ -4,8 +4,6 @@ import { useGetKeyringStatusQuery, useDeleteAllKeysMutation, useGetKeysQuery, - useClearCache, - useLogInMutation, type Serializable, } from '@chia-network/api-react'; import { ChiaBlack, Coins } from '@chia-network/icons'; @@ -26,6 +24,7 @@ import Loading from '../../components/Loading'; import MenuItem from '../../components/MenuItem/MenuItem'; import More from '../../components/More'; import TooltipIcon from '../../components/TooltipIcon'; +import useAuth from '../../hooks/useAuth'; import useKeyringMigrationPrompt from '../../hooks/useKeyringMigrationPrompt'; import useOpenDialog from '../../hooks/useOpenDialog'; import useShowError from '../../hooks/useShowError'; @@ -43,15 +42,15 @@ export default function SelectKey() { const openDialog = useOpenDialog(); const navigate = useNavigate(); const [deleteAllKeys] = useDeleteAllKeysMutation(); - const [logIn] = useLogInMutation(); - const { data: publicKeyFingerprints, isLoading: isLoadingPublicKeys, error, refetch } = useGetKeysQuery(); + + const { isLoading: isLoggingIn, logIn } = useAuth(); + const [selectedKey, setSelectedKey] = useState(null); + const { data: publicKeyFingerprints, isLoading: isLoadingPublicKeys, error, refetch } = useGetKeysQuery({}); const { data: keyringState, isLoading: isLoadingKeyringStatus } = useGetKeyringStatusQuery(); const hasFingerprints = !!publicKeyFingerprints?.length; - const [selectedFingerprint, setSelectedFingerprint] = useState(); const [skippedMigration] = useSkipMigration(); const [promptForKeyringMigration] = useKeyringMigrationPrompt(); const showError = useShowError(); - const clearCache = useClearCache(); const [sortedWallets, setSortedWallets] = usePrefs('sortedWallets', []); const keyItemsSortable = React.useRef(null); @@ -101,28 +100,16 @@ export default function SelectKey() { } }, [publicKeyFingerprints, fingerprintSettings, setFingerprintSettings, allColors]); - async function handleSelect(fingerprint: number) { - if (selectedFingerprint) { - return; - } - + async function handleSelect(logInFingerprint: number) { try { - setSelectedFingerprint(fingerprint); - - // we need to clear cache before logging in, because we need to clean notifications - await logIn({ - fingerprint, - type: 'skip', - }).unwrap(); - - // because some queries may be run during login , we need to clear them again - await clearCache(); + setSelectedKey(logInFingerprint); + await logIn(logInFingerprint); navigate('/dashboard/wallets'); } catch (err) { - showError(err); + showError(err as Error); } finally { - setSelectedFingerprint(undefined); + setSelectedKey(null); } } @@ -170,7 +157,7 @@ export default function SelectKey() { function sortedFingerprints(fingerprints: string[]) { const sorted = sortedWallets - .map((fingerprint: string) => fingerprints.find((f: any) => fingerprint === String(f.fingerprint))) + .map((value: string) => fingerprints.find((f: any) => value === String(f.fingerprint))) .filter((x: any) => !!x); /* if we added a new wallet and order was not saved yet case */ fingerprints.forEach((f: any) => { if (sorted.map((f2: any) => f2.fingerprint).indexOf(f.fingerprint) === -1) { @@ -239,10 +226,10 @@ export default function SelectKey() { return ( - + {isLoadingPublicKeys ? ( - Loading list of the keys + Loading keys ) : error ? ( } > - Unable to load the list of the keys + Unable to load keys   {error.message} @@ -321,8 +308,8 @@ export default function SelectKey() { index={index} keyData={keyData} onSelect={handleSelect} - loading={keyData.fingerprint === selectedFingerprint} - disabled={!!selectedFingerprint && keyData.fingerprint !== selectedFingerprint} + loading={isLoggingIn && keyData.fingerprint === selectedKey} + disabled={isLoggingIn} /> ))} diff --git a/packages/gui/src/components/app/AppProviders.tsx b/packages/gui/src/components/app/AppProviders.tsx index 6ca2d02b32..0e2926778c 100644 --- a/packages/gui/src/components/app/AppProviders.tsx +++ b/packages/gui/src/components/app/AppProviders.tsx @@ -10,6 +10,7 @@ import { dark, light, ErrorBoundary, + AuthProvider, } from '@chia-network/core'; import { nativeTheme } from '@electron/remote'; import { Trans } from '@lingui/macro'; @@ -97,24 +98,26 @@ export default function App(props: AppProps) { - - - - - }> - - - - {outlet ? : children} - - - - - - - - - + + + + + + }> + + + + {outlet ? : children} + + + + + + + + + + diff --git a/packages/gui/src/hooks/useBlockchainNotifications.tsx b/packages/gui/src/hooks/useBlockchainNotifications.tsx index e5aab43636..25f18227bb 100644 --- a/packages/gui/src/hooks/useBlockchainNotifications.tsx +++ b/packages/gui/src/hooks/useBlockchainNotifications.tsx @@ -4,7 +4,7 @@ import { useDeleteNotificationsMutation, useLazyGetTimestampForHeightQuery, } from '@chia-network/api-react'; -import { ConfirmDialog, useOpenDialog } from '@chia-network/core'; +import { ConfirmDialog, useOpenDialog, useAuth } from '@chia-network/core'; import { useWalletState } from '@chia-network/wallets'; import { Trans } from '@lingui/macro'; import debug from 'debug'; @@ -33,6 +33,7 @@ export default function useBlockchainNotifications() { const [getTimestampForHeight] = useLazyGetTimestampForHeightQuery(); + const { isLoading: isLoggingIn } = useAuth(); const { state, isLoading: isLoadingWalletState } = useWalletState(); const [notifications, setNotifications] = useStateAbort([]); @@ -56,14 +57,16 @@ export default function useBlockchainNotifications() { async ( blockchainNotificationsList: BlockchainNotification[], isWalletSynced: boolean, + loggingIn: boolean, abortSignal: AbortSignal ) => { try { setPreparingError(undefined, abortSignal); setIsPreparingNotifications(true, abortSignal); - // without wallet sync we can't get timestamp - if (!blockchainNotificationsList?.length || !isWalletSynced) { + // without wallet sync we can't get timestamp, during loggingIn we have wrong + // list of notifications from previous fingerprint and we need to wait for clear cache + if (!blockchainNotificationsList?.length || !isWalletSynced || loggingIn) { setNotifications([], abortSignal); return; } @@ -142,9 +145,9 @@ export default function useBlockchainNotifications() { abortControllerRef.current.abort(); abortControllerRef.current = new AbortController(); - prepareNotifications(blockchainNotifications, isSynced, abortControllerRef.current.signal); + prepareNotifications(blockchainNotifications, isSynced, isLoggingIn, abortControllerRef.current.signal); } - }, [blockchainNotifications, prepareNotifications, isSynced]); + }, [blockchainNotifications, prepareNotifications, isSynced, isLoggingIn]); const handleDeleteNotification = useCallback( async (id: string) => {