diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index fba3445..572c972 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useMemo, } from "react"; -import { useAuth, UseAuthProps } from "../hooks/useAuth"; +import { useAuth, UseAuthProps } from "../hooks/useInitializeAuth"; import { useResources } from "../hooks/useResources"; import { useAppInfo } from "../hooks/useAppInfo"; import { useIdentifiers } from "../hooks/useIdentifiers"; @@ -15,10 +15,8 @@ import { IndexManager } from "../components/IndexManager/IndexManager"; import { useIndexes } from "../hooks/useIndexes"; import { useProgressStore } from "../state/video"; import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer"; -import { Location, NavigateFunction } from "react-router-dom"; import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog"; import { useMultiplePublishStore } from "../state/multiplePublish"; -import { useGlobalPlayerStore } from "../state/pip"; // ✅ Define Global Context Type interface GlobalContextType { @@ -28,7 +26,7 @@ interface GlobalContextType { identifierOperations: ReturnType; persistentOperations: ReturnType; indexOperations: ReturnType; - enableGlobalVideoFeature: boolean + enableGlobalVideoFeature: boolean; } // ✅ Define Config Type for Hook Options @@ -39,7 +37,7 @@ interface GlobalProviderProps { auth?: UseAuthProps; appName: string; publicSalt: string; - enableGlobalVideoFeature?: boolean + enableGlobalVideoFeature?: boolean; }; toastStyle?: CSSProperties; @@ -48,9 +46,6 @@ interface GlobalProviderProps { // ✅ Create Context with Proper Type export const GlobalContext = createContext(null); - - - // 🔹 Global Provider (Handles Multiple Hooks) export const GlobalProvider = ({ children, @@ -59,7 +54,7 @@ export const GlobalProvider = ({ }: GlobalProviderProps) => { // ✅ Call hooks and pass in options dynamically const auth = useAuth(config?.auth || {}); - const isPublishing = useMultiplePublishStore((s)=> s.isPublishing); + const isPublishing = useMultiplePublishStore((s) => s.isPublishing); const appInfo = useAppInfo(config.appName, config?.publicSalt); const lists = useResources(); const identifierOperations = useIdentifiers( @@ -80,9 +75,16 @@ export const GlobalProvider = ({ identifierOperations, persistentOperations, indexOperations, - enableGlobalVideoFeature: config?.enableGlobalVideoFeature || false + enableGlobalVideoFeature: config?.enableGlobalVideoFeature || false, }), - [auth, lists, appInfo, identifierOperations, persistentOperations, config?.enableGlobalVideoFeature] + [ + auth, + lists, + appInfo, + identifierOperations, + persistentOperations, + config?.enableGlobalVideoFeature, + ] ); const { clearOldProgress } = useProgressStore(); @@ -91,18 +93,10 @@ export const GlobalProvider = ({ }, []); return ( - - {config?.enableGlobalVideoFeature && ( - - )} - - - - - {isPublishing && ( - - )} + {config?.enableGlobalVideoFeature && } + + {isPublishing && } - ); }; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 44ffbed..a8285a5 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,157 +1,83 @@ -import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useMemo } from "react"; import { useAuthStore } from "../state/auth"; +import { userAccountInfo } from "./useInitializeAuth"; -// ✅ Define Types -/** - * Configuration for balance retrieval behavior. - */ -export type BalanceSetting = - | { - /** If `true`, the balance will be fetched only once when the app loads. */ - onlyOnMount: true; - /** `interval` cannot be set when `onlyOnMount` is `true`. */ - interval?: never; - } - | { - /** If `false` or omitted, balance will be updated periodically. */ - onlyOnMount?: false; - /** The time interval (in milliseconds) for balance updates. */ - interval?: number; - }; - -interface userAccountInfo { - address: string; - publicKey: string -} -export interface UseAuthProps { - balanceSetting?: BalanceSetting; - /** User will be prompted for authentication on start-up */ - authenticateOnMount?: boolean; - userAccountInfo?: userAccountInfo | null -} - -export const useAuth = ({ balanceSetting, authenticateOnMount = true, userAccountInfo = null }: UseAuthProps) => { +export const useAuth = () => { const address = useAuthStore((s) => s.address); -const publicKey = useAuthStore((s) => s.publicKey); -const name = useAuthStore((s) => s.name); -const avatarUrl = useAuthStore((s) => s.avatarUrl); -const balance = useAuthStore((s) => s.balance); + const publicKey = useAuthStore((s) => s.publicKey); + const name = useAuthStore((s) => s.name); + const avatarUrl = useAuthStore((s) => s.avatarUrl); -const isLoadingUser = useAuthStore((s) => s.isLoadingUser); -const isLoadingInitialBalance = useAuthStore((s) => s.isLoadingInitialBalance); -const errorLoadingUser = useAuthStore((s) => s.errorLoadingUser); + const isLoadingUser = useAuthStore((s) => s.isLoadingUser); + const errorLoadingUser = useAuthStore((s) => s.errorLoadingUser); + const setErrorLoadingUser = useAuthStore((s) => s.setErrorLoadingUser); + const setIsLoadingUser = useAuthStore((s) => s.setIsLoadingUser); + const setUser = useAuthStore((s) => s.setUser); + const setName = useAuthStore((s) => s.setName); + const authenticateUser = useCallback( + async (userAccountInfo?: userAccountInfo) => { + try { + setErrorLoadingUser(null); + setIsLoadingUser(true); -const setErrorLoadingUser = useAuthStore((s) => s.setErrorLoadingUser); -const setIsLoadingUser = useAuthStore((s) => s.setIsLoadingUser); -const setUser = useAuthStore((s) => s.setUser); -const setBalance = useAuthStore((s) => s.setBalance); + const account = + userAccountInfo || + (await qortalRequest({ + action: "GET_USER_ACCOUNT", + })); - - const balanceSetIntervalRef = useRef>(null); - - const authenticateUser = useCallback(async (userAccountInfo?: userAccountInfo) => { - try { - setErrorLoadingUser(null); - setIsLoadingUser(true); - - const account = userAccountInfo || await qortalRequest({ - action: "GET_USER_ACCOUNT", - }); - - if (account?.address) { - const nameData = await qortalRequest({ - action: "GET_PRIMARY_NAME", - address: account.address, - }); - setUser({ ...account, name: nameData || "" }); + if (account?.address) { + const nameData = await qortalRequest({ + action: "GET_PRIMARY_NAME", + address: account.address, + }); + setUser({ ...account, name: nameData || "" }); + } + } catch (error) { + setErrorLoadingUser( + error instanceof Error ? error.message : "Unable to authenticate" + ); + } finally { + setIsLoadingUser(false); } - } catch (error) { - setErrorLoadingUser( - error instanceof Error ? error.message : "Unable to authenticate" - ); - } finally { - setIsLoadingUser(false); - } - }, [setErrorLoadingUser, setIsLoadingUser, setUser]); + }, + [setErrorLoadingUser, setIsLoadingUser, setUser] + ); - const getBalance = useCallback(async (address: string): Promise => { - try { - const response = await qortalRequest({ - action: "GET_BALANCE", - address, - }); - const userBalance = Number(response) || 0 - setBalance(userBalance); - return userBalance - } catch (error) { - setBalance(0); - return 0 - } - }, [setBalance]); + const switchName = useCallback( + async (name: string) => { + if (!name) throw new Error("No name provided"); + const response = await fetch(`/names/${name}`); + if (!response?.ok) throw new Error("Error fetching name details"); + const nameInfo = await response.json(); + const currentAddress = useAuthStore.getState().address; - const balanceSetInterval = useCallback((address: string, interval: number) => { - try { - if (balanceSetIntervalRef.current) { - clearInterval(balanceSetIntervalRef.current); - } - - let isCalling = false; - balanceSetIntervalRef.current = setInterval(async () => { - if (isCalling) return; - isCalling = true; - await getBalance(address); - isCalling = false; - }, interval); - } catch (error) { - console.error(error); - } - }, [getBalance]); - - useEffect(() => { - if (authenticateOnMount) { - authenticateUser(); - } - if(userAccountInfo?.address && userAccountInfo?.publicKey){ - authenticateUser(userAccountInfo); - } - }, [authenticateOnMount, authenticateUser, userAccountInfo?.address, userAccountInfo?.publicKey]); - - useEffect(() => { - if (address && (balanceSetting?.onlyOnMount || (balanceSetting?.interval && !isNaN(balanceSetting?.interval)))) { - getBalance(address); - } - if (address && balanceSetting?.interval !== undefined && !isNaN(balanceSetting.interval)) { - balanceSetInterval(address, balanceSetting.interval); - } - }, [balanceSetting?.onlyOnMount, balanceSetting?.interval, address, getBalance, balanceSetInterval]); - - const manualGetBalance = useCallback(async () : Promise => { - if(!address) throw new Error('Not authenticated') - const res = await getBalance(address) - return res - }, [address]) - - return useMemo(() => ({ - address, - publicKey, - name, - avatarUrl, - balance, - isLoadingUser, - isLoadingInitialBalance, - errorMessageLoadingUser: errorLoadingUser, - authenticateUser, - getBalance: manualGetBalance, - }), [ - address, - publicKey, - name, - avatarUrl, - balance, - isLoadingUser, - isLoadingInitialBalance, - errorLoadingUser, - authenticateUser, - manualGetBalance, - ]); + if (nameInfo?.owner !== currentAddress) + throw new Error(`This account does not own the name ${name}`); + setName(name); + }, + [setName] + ); + return useMemo( + () => ({ + address, + publicKey, + name, + avatarUrl, + isLoadingUser, + errorMessageLoadingUser: errorLoadingUser, + authenticateUser, + switchName, + }), + [ + address, + publicKey, + name, + avatarUrl, + isLoadingUser, + errorLoadingUser, + authenticateUser, + switchName, + ] + ); }; diff --git a/src/hooks/useBalance.tsx b/src/hooks/useBalance.tsx new file mode 100644 index 0000000..1e4a90e --- /dev/null +++ b/src/hooks/useBalance.tsx @@ -0,0 +1,48 @@ +import { useCallback, useMemo } from "react"; +import { useAuthStore } from "../state/auth"; + +export const useQortBalance = () => { + const address = useAuthStore((s) => s.address); + const setBalance = useAuthStore((s) => s.setBalance); + const isLoadingInitialBalance = useAuthStore( + (s) => s.isLoadingInitialBalance + ); + const setIsLoadingBalance = useAuthStore((s) => s.setIsLoadingBalance); + + const qortBalance = useAuthStore((s) => s.balance); + + const getBalance = useCallback( + async (address: string): Promise => { + try { + setIsLoadingBalance(true); + const response = await qortalRequest({ + action: "GET_BALANCE", + address, + }); + const userBalance = Number(response) || 0; + setBalance(userBalance); + return userBalance; + } catch (error) { + setBalance(0); + return 0; + } finally { + setIsLoadingBalance(false); + } + }, + [setBalance] + ); + + const manualGetBalance = useCallback(async (): Promise => { + if (!address) throw new Error("Not authenticated"); + const res = await getBalance(address); + return res; + }, [address]); + return useMemo( + () => ({ + value: qortBalance, + getBalance: manualGetBalance, + isLoading: isLoadingInitialBalance, + }), + [qortBalance, manualGetBalance, isLoadingInitialBalance] + ); +}; diff --git a/src/hooks/useInitializeAuth.tsx b/src/hooks/useInitializeAuth.tsx new file mode 100644 index 0000000..54c0ae5 --- /dev/null +++ b/src/hooks/useInitializeAuth.tsx @@ -0,0 +1,185 @@ +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { useAuthStore } from "../state/auth"; + +// ✅ Define Types +/** + * Configuration for balance retrieval behavior. + */ +export type BalanceSetting = + | { + /** If `true`, the balance will be fetched only once when the app loads. */ + onlyOnMount: true; + /** `interval` cannot be set when `onlyOnMount` is `true`. */ + interval?: never; + } + | { + /** If `false` or omitted, balance will be updated periodically. */ + onlyOnMount?: false; + /** The time interval (in milliseconds) for balance updates. */ + interval?: number; + }; + +export interface userAccountInfo { + address: string; + publicKey: string; +} +export interface UseAuthProps { + balanceSetting?: BalanceSetting; + /** User will be prompted for authentication on start-up */ + authenticateOnMount?: boolean; + userAccountInfo?: userAccountInfo | null; +} + +export const useAuth = ({ + balanceSetting, + authenticateOnMount = true, + userAccountInfo = null, +}: UseAuthProps) => { + const address = useAuthStore((s) => s.address); + const publicKey = useAuthStore((s) => s.publicKey); + const name = useAuthStore((s) => s.name); + const avatarUrl = useAuthStore((s) => s.avatarUrl); + + const isLoadingUser = useAuthStore((s) => s.isLoadingUser); + const errorLoadingUser = useAuthStore((s) => s.errorLoadingUser); + const setIsLoadingBalance = useAuthStore((s) => s.setIsLoadingBalance); + + const setErrorLoadingUser = useAuthStore((s) => s.setErrorLoadingUser); + const setIsLoadingUser = useAuthStore((s) => s.setIsLoadingUser); + const setUser = useAuthStore((s) => s.setUser); + const setBalance = useAuthStore((s) => s.setBalance); + + const balanceSetIntervalRef = useRef>( + null + ); + + const authenticateUser = useCallback( + async (userAccountInfo?: userAccountInfo) => { + try { + setErrorLoadingUser(null); + setIsLoadingUser(true); + + const account = + userAccountInfo || + (await qortalRequest({ + action: "GET_USER_ACCOUNT", + })); + + if (account?.address) { + const nameData = await qortalRequest({ + action: "GET_PRIMARY_NAME", + address: account.address, + }); + setUser({ ...account, name: nameData || "" }); + } + } catch (error) { + setErrorLoadingUser( + error instanceof Error ? error.message : "Unable to authenticate" + ); + } finally { + setIsLoadingUser(false); + } + }, + [setErrorLoadingUser, setIsLoadingUser, setUser] + ); + + const getBalance = useCallback( + async (address: string): Promise => { + try { + setIsLoadingBalance(true); + const response = await qortalRequest({ + action: "GET_BALANCE", + address, + }); + const userBalance = Number(response) || 0; + setBalance(userBalance); + return userBalance; + } catch (error) { + setBalance(0); + return 0; + } finally { + setIsLoadingBalance(false); + } + }, + [setBalance] + ); + + const balanceSetInterval = useCallback( + (address: string, interval: number) => { + try { + if (balanceSetIntervalRef.current) { + clearInterval(balanceSetIntervalRef.current); + } + + let isCalling = false; + balanceSetIntervalRef.current = setInterval(async () => { + if (isCalling) return; + isCalling = true; + await getBalance(address); + isCalling = false; + }, interval); + } catch (error) { + console.error(error); + } + }, + [getBalance] + ); + + useEffect(() => { + if (authenticateOnMount) { + authenticateUser(); + } + if (userAccountInfo?.address && userAccountInfo?.publicKey) { + authenticateUser(userAccountInfo); + } + }, [ + authenticateOnMount, + authenticateUser, + userAccountInfo?.address, + userAccountInfo?.publicKey, + ]); + + useEffect(() => { + if ( + address && + (balanceSetting?.onlyOnMount || + (balanceSetting?.interval && !isNaN(balanceSetting?.interval))) + ) { + getBalance(address); + } + if ( + address && + balanceSetting?.interval !== undefined && + !isNaN(balanceSetting.interval) + ) { + balanceSetInterval(address, balanceSetting.interval); + } + }, [ + balanceSetting?.onlyOnMount, + balanceSetting?.interval, + address, + getBalance, + balanceSetInterval, + ]); + + return useMemo( + () => ({ + address, + publicKey, + name, + avatarUrl, + isLoadingUser, + errorMessageLoadingUser: errorLoadingUser, + authenticateUser, + }), + [ + address, + publicKey, + name, + avatarUrl, + isLoadingUser, + errorLoadingUser, + authenticateUser, + ] + ); +}; diff --git a/src/hooks/usePublish.tsx b/src/hooks/usePublish.tsx index f7907f4..3e5a636 100644 --- a/src/hooks/usePublish.tsx +++ b/src/hooks/usePublish.tsx @@ -7,7 +7,6 @@ import { ReturnType } from "../components/ResourceList/ResourceListDisplay"; import { useCacheStore } from "../state/cache"; import { useMultiplePublishStore, usePublishStatusStore } from "../state/multiplePublish"; import { ResourceToPublish } from "../types/qortalRequests/types"; -import { MultiplePublishError } from "../components/MultiPublish/MultiPublishDialog"; interface StoredPublish { qortalMetadata: QortalMetadata; diff --git a/src/index.ts b/src/index.ts index 0d640ae..3d028a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,8 @@ export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHo export { VideoPlayerParent as VideoPlayer } from './components/VideoPlayer/VideoPlayerParent'; export { useListReturn } from './hooks/useListData'; export { useAllResourceStatus } from './hooks/useAllResourceStatus'; +export { useQortBalance } from './hooks/useBalance'; +export { useAuth } from './hooks/useAuth'; import './index.css' export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events'; export { formatBytes, formatDuration } from './utils/numbers'; diff --git a/src/state/auth.ts b/src/state/auth.ts index cf293ca..e3babc7 100644 --- a/src/state/auth.ts +++ b/src/state/auth.ts @@ -23,6 +23,7 @@ interface AuthState { setIsLoadingUser: (loading: boolean) => void; setIsLoadingBalance: (loading: boolean) => void; setErrorLoadingUser: (error: string | null) => void; + setName: (name: string | null) => void; } // ✅ Typed Zustand Store @@ -43,4 +44,11 @@ export const useAuthStore = create((set) => ({ setIsLoadingUser: (loading) => set({ isLoadingUser: loading }), setIsLoadingBalance: (loading) => set({ isLoadingInitialBalance: loading }), setErrorLoadingUser: (error) => set({ errorLoadingUser: error }), + setName: (name) => + set({ + name, + avatarUrl: !name + ? null + : `/arbitrary/THUMBNAIL/${encodeURIComponent(name)}/qortal_avatar?async=true`, + }), })); diff --git a/src/utils/qortal.ts b/src/utils/qortal.ts index 7802d1b..f175d2f 100644 --- a/src/utils/qortal.ts +++ b/src/utils/qortal.ts @@ -1,4 +1,6 @@ -export const createAvatarLink = (qortalName: string)=> { +export const createAvatarLink = (qortalName: string): string => { + if (!qortalName?.trim()) return ''; + return `/arbitrary/THUMBNAIL/${encodeURIComponent(qortalName)}/qortal_avatar?async=true` }