diff --git a/src/refactor/masa-client-provider.tsx b/src/refactor/masa-client-provider.tsx index 4f19c47a..346521b1 100644 --- a/src/refactor/masa-client-provider.tsx +++ b/src/refactor/masa-client-provider.tsx @@ -1,7 +1,10 @@ import React, { ReactNode, createContext, useContext, useMemo } from 'react'; +import { useAsync } from 'react-use'; import { useMasaClient } from './masa-client/use-masa-client'; import { useIdentity } from './masa-feature/use-identity'; -import { useSession } from './masa-feature/use-session'; + +import { useWallet } from './wallet-client/wallet/use-wallet'; +import { useSession } from './masa-feature/session/use-session'; export interface MasaClientProviderValue { masa?: ReturnType['sdk']; @@ -12,17 +15,63 @@ export interface MasaClientProviderValue { export const MasaClientContext = createContext({} as MasaClientProviderValue); export const MasaClientProvider = ({ children }: { children: ReactNode }) => { - const { sdk: masa } = useMasaClient(); - const { session } = useSession(); + const { masa, masaAddress } = useMasaClient(); + const { + session, + isLoadingSession, + hasSession, + loginSession, + logoutSession, + sessionAddress, + } = useSession(); + const { isDisconnected } = useWallet(); + + // * useEffect to handle account switches and disconnect + useAsync(async () => { + if (isLoadingSession) return; + + if ( + session && + masaAddress && + masaAddress === session?.user.address && + hasSession + ) { + return; + } + + if (isDisconnected) { + await logoutSession(); + return; + } + + if ( + hasSession && + sessionAddress && + masaAddress && + sessionAddress !== masaAddress + ) { + await logoutSession(); + } + }, [ + isLoadingSession, + sessionAddress, + masaAddress, + isDisconnected, + logoutSession, + hasSession, + session, + ]); const masaClientProviderValue: MasaClientProviderValue = useMemo( () => ({ masa, - session, + sessionAddress, + loginSession, + logoutSession, } as MasaClientProviderValue), - [masa, session] + [masa, session, sessionAddress, loginSession, logoutSession] ); return ( diff --git a/src/refactor/masa-client/query-client.ts b/src/refactor/masa-client/query-client.ts index c307f933..27ef002f 100644 --- a/src/refactor/masa-client/query-client.ts +++ b/src/refactor/masa-client/query-client.ts @@ -17,7 +17,7 @@ export const createQueryClient = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { - cacheTime: 1000 * 60 * 60 * 24, // 24 hours + cacheTime: 0, // 1000 * 60 * 60 * 24, // 24 hours networkMode: 'offlineFirst', refetchOnWindowFocus: false, retry: 0, @@ -58,7 +58,8 @@ export const createQueryClient = () => { query.cacheTime !== 0 && // Note: adding a `persist` flag to a query key will instruct the // persister whether or not to persist the response of the query. - (query.queryKey[0] as { persist?: boolean }).persist !== false, + (query.queryKey[1] as { persist?: boolean } & Record) + .persist !== false, }, }); diff --git a/src/refactor/masa-client/use-masa-client.ts b/src/refactor/masa-client/use-masa-client.ts index 855eb262..9a0c8aa8 100644 --- a/src/refactor/masa-client/use-masa-client.ts +++ b/src/refactor/masa-client/use-masa-client.ts @@ -1,6 +1,7 @@ import type { Signer } from 'ethers'; import type { NetworkName } from '@masa-finance/masa-sdk'; import { useMemo } from 'react'; +import { useAsync } from 'react-use'; import { useConfig } from '../base-provider'; import { useWallet } from '../wallet-client/wallet/use-wallet'; import { useMasaSDK } from './use-masa-sdk'; @@ -8,11 +9,12 @@ import { useNetwork } from '../wallet-client/network'; export const useMasaClient = () => { const { masaConfig } = useConfig(); - const { signer, isDisconnected } = useWallet(); + const { signer, isDisconnected, address } = useWallet(); const { activeChain } = useNetwork(); const masa = useMasaSDK( { + address, signer: isDisconnected ? undefined : (signer as Signer | undefined), ...masaConfig, environmentName: masaConfig.environment, @@ -23,6 +25,7 @@ export const useMasaClient = () => { : (activeChain?.network as NetworkName | undefined), }, [ + address, signer, masaConfig, masaConfig.environment, @@ -31,13 +34,29 @@ export const useMasaClient = () => { ] ); - const masaClient = useMemo( - () => ({ + const { value: masaAddress } = useAsync(async () => { + if (masa) { + const masaAddr = await masa.config.signer.getAddress(); + return masaAddr as `0x${string}`; + } + + return undefined; + }, [masa]); + + const masaClient = useMemo(() => { + if (address !== masaAddress) { + return { + masaAddress, + sdk: undefined, + masa: undefined, + }; + } + return { + masaAddress, sdk: masa, masa, - }), - [masa] - ); + }; + }, [masa, masaAddress, address]); return masaClient; }; diff --git a/src/refactor/masa-client/use-masa-sdk.ts b/src/refactor/masa-client/use-masa-sdk.ts index c4d429e4..a5f77b3e 100644 --- a/src/refactor/masa-client/use-masa-sdk.ts +++ b/src/refactor/masa-client/use-masa-sdk.ts @@ -36,10 +36,12 @@ export const useMasaSDK = ( verbose, apiUrl, contractAddressOverrides, - }: UseMasaSdkArgs, + address, + }: UseMasaSdkArgs & { address: `0x${string}` | undefined }, deps: Array ): Masa | undefined => { const masa = useMemo(() => { + if (!address) return undefined; if (!signer) { if (verbose) console.log('DEBUG: no signer, returning undefined for masa object'); diff --git a/src/refactor/masa-feature/session/use-session.ts b/src/refactor/masa-feature/session/use-session.ts new file mode 100644 index 00000000..ebc0efe6 --- /dev/null +++ b/src/refactor/masa-feature/session/use-session.ts @@ -0,0 +1,322 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useAsyncFn } from 'react-use'; +import type { ISession } from '@masa-finance/masa-sdk'; +import { useMemo } from 'react'; +import { useWallet } from '../../wallet-client/wallet/use-wallet'; +import { useMasaClient } from '../../masa-client/use-masa-client'; +import { QcContext } from '../../masa-provider'; +// import { useAccountChangeListen } from '../wallet-client/wallet/use-account-change-listen'; + +// * NOTE: react-query does not allow us to pass undefined as a function return, +// * NOTE: so we need to convert an undefined result in every query to null +// * TODO: split up the queries, pass context via function variable to avoid dependency cycle +export const useSession = () => { + const { address, isDisconnected, previousAddress, isConnected } = useWallet(); + const { masa, masaAddress } = useMasaClient(); + const queryClient = useQueryClient({ context: QcContext }); + + // * callbacks + const [{ loading: isCheckingSession }, checkSessionAsync] = + useAsyncFn(async () => { + if (!masaAddress) return null; + if (!masa) return null; + if (isDisconnected) return null; + + const hasSesh = await masa?.session.checkLogin(); + if (hasSesh !== undefined || hasSesh !== null) return hasSesh; + + return null; + }, [masa, masaAddress, isDisconnected]); + + const [, loginSessionAsync] = useAsyncFn(async () => { + if (!masa) return null; + if (!masaAddress) return null; + if (masaAddress !== address) return null; + if (isDisconnected) return null; + const loginObj = await masa.session.login(); + + if (!loginObj) { + return null; + } + + return loginObj as unknown as ISession & { + userId: string; + address: string; + }; + }, [masa, address, masaAddress, isDisconnected]); + + const [, getSessionAsync] = useAsyncFn(async () => { + if (!masa) return null; + if (!masaAddress) return null; + + const seshFromGet = await masa?.session.getSession(); + + if (seshFromGet === undefined || seshFromGet === null) { + return null; + } + + return seshFromGet; + }, [masaAddress, masa]); + + const [, onSettledGetSession] = useAsyncFn( + async (data: ISession | null | undefined) => { + console.log('ON SETTLE GETSESSION', { + masaAddress, + retrAddr: data?.user.address, + }); + if (!data) return; + + if (data?.user.address !== masaAddress) { + await queryClient.invalidateQueries([ + 'session', + { masaAddress, persist: false }, + ]); + queryClient.setQueryData( + ['session', { masaAddress, persist: false }], + null + ); + + await queryClient.invalidateQueries([ + 'session-check', + { masaAddress, persist: false }, + ]); + queryClient.setQueryData( + ['session', { masaAddress, persist: false }], + false + ); + } + }, + [masaAddress, queryClient] + ); + // * queries + const { + data: session, + isFetching: isFetchingSession, + refetch: getSession, + } = useQuery({ + queryKey: ['session', { masaAddress, persist: false }], + enabled: false, + cacheTime: 0, + context: QcContext, + onSettled: onSettledGetSession, + queryFn: async () => getSessionAsync(), + }); + + // * logout callback + const [{ loading: isLoggingOut }, logoutSession] = useAsyncFn(async () => { + await masa?.session.logout(); + + await queryClient.invalidateQueries([ + 'session-check', + { masaAddress, persist: false }, + ]); + await queryClient.invalidateQueries(['session-login', { masaAddress }]); + await queryClient.invalidateQueries([ + 'session', + { masaAddress, persist: false }, + ]); + queryClient.setQueryData( + ['session-check', { masaAddress, persist: false }], + null + ); + queryClient.setQueryData(['session-login', { masaAddress }], null); + queryClient.setQueryData( + ['session', { masaAddress, persist: false }], + null + ); + }, [masa, queryClient, masaAddress]); + + // * session address + const sessionAddress = useMemo(() => { + if (!session) return undefined; + if (!session.user) return undefined; + return session.user.address as `0x${string}`; + }, [session]); + + // * callback to handle succesful login to sync session state + const [, onSuccessLogin] = useAsyncFn( + async (data: boolean) => { + switch (data) { + case true: { + if (isLoggingOut) break; + + if (session && masaAddress === session?.user.address) { + break; + } + + if ( + previousAddress === undefined || + previousAddress !== masaAddress + ) { + await queryClient.invalidateQueries([ + 'session', + { masaAddress, persist: false }, + ]); + await queryClient.fetchQuery([ + 'session', + { masaAddress, persist: false }, + ]); + break; + } + + const checkedLogin = await checkSessionAsync(); + + if (!checkedLogin) { + await logoutSession(); + break; + } + + // * we are in a valid session but our session data needs to be updated. + await queryClient.fetchQuery([ + 'session', + { masaAddress, persist: false }, + ]); + break; + } + + case false: { + await queryClient.invalidateQueries([ + 'session', + { masaAddress, persist: false }, + ]); + await queryClient.invalidateQueries([ + 'session-login', + { masaAddress }, + ]); + queryClient.setQueryData( + ['session', { masaAddress, persist: false }], + null + ); + queryClient.setQueryData(['session-login', { masaAddress }], null); + break; + } + case undefined: { + await queryClient.invalidateQueries([ + 'session', + { masaAddress, persist: false }, + ]); + await queryClient.invalidateQueries([ + 'session-login', + { masaAddress }, + ]); + queryClient.setQueryData( + ['session', { masaAddress, persist: false }], + null + ); + queryClient.setQueryData(['session-login', { masaAddress }], null); + break; + } + default: { + break; + } + } + }, + [ + masaAddress, + previousAddress, + checkSessionAsync, + queryClient, + session, + isLoggingOut, + logoutSession, + ] + ); + + const { + data: hasSession, + refetch: checkLogin, + isFetching: isCheckingLogin, + } = useQuery({ + queryKey: ['session-check', { masaAddress, persist: false }], + enabled: !!masa && !!masaAddress, + context: QcContext, + cacheTime: 0, + onSuccess: onSuccessLogin, + + queryFn: async () => { + if (!isConnected) return false; + if (!masa) return false; + + if (masaAddress !== address) { + await queryClient.invalidateQueries([ + 'session', + { masaAddress, persist: false }, + ]); + await queryClient.invalidateQueries([ + 'session-login', + { masaAddress, persist: false }, + ]); + + return false; + } + + const hasSesh = await checkSessionAsync(); + + if (hasSesh) { + return hasSesh; + } + + return false; + }, + }); + + const [, onSettledLogin] = useAsyncFn(async () => { + if (!masa) return; + if (!address) return; + if (!masaAddress) return; + if (masaAddress === sessionAddress) return; + await checkLogin(); + }, [masa, address, masaAddress, sessionAddress, checkLogin]); + + const { refetch: loginSession, isFetching: isLoggingIn } = useQuery({ + queryKey: ['session-login', { masaAddress }], + enabled: false, + context: QcContext, + refetchOnMount: false, + cacheTime: 0, + onSettled: onSettledLogin, + + queryFn: async () => { + if (isDisconnected) return null; + if (hasSession) return null; + + await loginSessionAsync(); + + return null; + }, + }); + + const isLoadingSession = useMemo( + () => + isLoggingIn || + isLoggingOut || + isFetchingSession || + isCheckingLogin || + isCheckingSession, + [ + isLoggingIn, + isLoggingOut, + isFetchingSession, + isCheckingLogin, + isCheckingSession, + ] + ); + + return { + session, + sessionAddress, + getSession, + isFetchingSession, + hasSession, + checkSessionAsync, + isCheckingSession, + isCheckingLogin, + loginSession, + logoutSession, + isLoggingIn, + isLoggingOut, + isLoggedIn: hasSession, + checkLogin, + isLoadingSession, + }; +}; diff --git a/src/refactor/masa-feature/use-session.ts b/src/refactor/masa-feature/use-session.ts deleted file mode 100644 index 8205ec8c..00000000 --- a/src/refactor/masa-feature/use-session.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useAsync, useAsyncFn } from 'react-use'; -import type { ISession } from '@masa-finance/masa-sdk'; -import { useWallet } from '../wallet-client/wallet/use-wallet'; -import { useMasaClient } from '../masa-client/use-masa-client'; -import { QcContext } from '../masa-provider'; - -// * NOTE: react-query does not allow us to pass undefined as a function return, -// * NOTE: so we need to convert an undefined result in every query to null -// * TODO: split up the queries, pass context via function variable to avoid dependency cycle -// * FIXME: we are getting the session 3 times on every change, we should only get it once -export const useSession = () => { - const { address, isDisconnected, previousAddress } = useWallet(); - const { masa } = useMasaClient(); - const queryClient = useQueryClient({ context: QcContext }); - - // * callbacks - const [{ loading: isCheckingSession }, checkSessionAsync] = - useAsyncFn(async () => { - if (!address) return null; - if (!masa) return null; - if (isDisconnected) return null; - - const hasSesh = await masa?.session.checkLogin(); - if (hasSesh !== undefined || hasSesh !== null) return hasSesh; - - return null; - }, [masa, address, isDisconnected]); - - const [, loginSessionAsync] = useAsyncFn(async () => { - if (!masa) return null; - if (!address) return null; - if (isDisconnected) return null; - const loginObj = await masa.session.login(); - - if (!loginObj) { - return null; - } - - return loginObj as unknown as ISession & { - userId: string; - address: string; - }; - }, [masa, address, isDisconnected]); - - const [, getSessionAsync] = useAsyncFn(async () => { - if (!masa) return null; - if (!address) return null; - - const seshFromGet = await masa?.session.getSession(); - - if (seshFromGet === undefined || seshFromGet === null) { - return null; - } - - return seshFromGet; - }, [address, masa]); - - // * queries - const { - data: session, - isLoading: isFetchingSession, - refetch: getSession, - isRefetching: isRefetchingSession, - } = useQuery({ - queryKey: ['session', address], - enabled: false, - context: QcContext, - onSuccess: async (data: ISession | null) => { - if (data) { - const resultCheck = await checkSessionAsync(); - if (!resultCheck) { - // await queryClient.invalidateQueries(['session-check', address]); - } - } - }, - queryFn: async () => getSessionAsync(), - }); - - const { - data: hasSession, - refetch: checkLogin, - isRefetching: isCheckingLogin, - } = useQuery({ - queryKey: ['session-check', address], - enabled: !!masa, - context: QcContext, - onSuccess: async (data: boolean) => { - switch (data) { - case true: { - const checkedLogin = await checkSessionAsync(); - - if (!checkedLogin) { - await queryClient.invalidateQueries(['session', address]); - await queryClient.invalidateQueries(['session-check', address]); - await queryClient.invalidateQueries(['session-login', address]); - return; - } - - if (session && address === session?.user.address) { - return; - } - - await queryClient.invalidateQueries(['session', address]); - await queryClient.fetchQuery(['session', address]); - break; - } - - case false: { - queryClient.setQueryData(['session-login', address], null); - queryClient.setQueryData(['session', address], null); - break; - } - case undefined: { - queryClient.setQueryData(['session-login', address], null); - queryClient.setQueryData(['session', address], null); - break; - } - default: { - break; - } - } - }, - - queryFn: async () => { - if (!address) return false; - if (!masa) return false; - - const hasSesh = await checkSessionAsync(); - - if (hasSesh) { - return hasSesh; - } - - return false; - }, - }); - - // * logout callback - const [{ loading: isLoggingOut }, logoutSession] = useAsyncFn(async () => { - if (hasSession) { - await masa?.session.logout(); - } - - await queryClient.invalidateQueries(['session-check', address]); - await queryClient.invalidateQueries(['session-login', address]); - await queryClient.invalidateQueries(['session', address]); - }, [masa, queryClient, address, hasSession]); - - const { refetch: loginSession, isRefetching: isLoggingIn } = useQuery({ - queryKey: ['session-login', address], - enabled: false, - context: QcContext, - refetchOnMount: false, - - onSettled: async () => { - await checkLogin(); - }, - - queryFn: async () => { - if (isDisconnected) return null; - const hasIt = await checkSessionAsync(); - - if (hasIt) return null; - - await loginSessionAsync(); - return null; - }, - }); - - useAsync(async () => { - if (isDisconnected) { - await logoutSession(); - } - - if (previousAddress !== address) { - await checkLogin(); - } - }, [address, previousAddress, checkLogin, isDisconnected, logoutSession]); - - return { - session, - getSession, - isFetchingSession, - isRefetchingSession, - hasSession, - checkSessionAsync, - isCheckingSession, - isCheckingLogin, - loginSession, - logoutSession, - isLoggingIn, - isLoggingOut, - isLoggedIn: hasSession, - checkLogin, - isLoadingSession: - isLoggingIn || - isLoggingOut || - isFetchingSession || - isCheckingLogin || - isCheckingSession || - isRefetchingSession, - }; -}; diff --git a/src/refactor/masanew.stories.tsx b/src/refactor/masanew.stories.tsx index 7096f22a..b2fb76e2 100644 --- a/src/refactor/masanew.stories.tsx +++ b/src/refactor/masanew.stories.tsx @@ -11,7 +11,8 @@ import MasaProvider, { QcContext } from './masa-provider'; import { useWallet } from './wallet-client/wallet/use-wallet'; import { useNetwork } from './wallet-client/network/use-network'; // import { useIdentity } from './masa-feature/use-identity'; -import { useSession } from './masa-feature/use-session'; +import { useSession } from './masa-feature/session/use-session'; +import { useMasaClient } from './masa-client/use-masa-client'; // * nextjs fix // * TODO: move this to index.ts file at some point @@ -229,13 +230,13 @@ const MasaInfo = () => { logoutSession, isLoggingIn, isLoggingOut, - + sessionAddress, hasSession, isLoadingSession, checkLogin, } = useSession(); const { isDisconnected } = useWallet(); - + const { masaAddress } = useMasaClient(); // console.log('STORY', { hasSession, session }); return ( <> @@ -243,6 +244,9 @@ const MasaInfo = () => {

Masa

    • +

      Masa

      +
    • masaAddress: {masaAddress}
    • +
    • sessionAddress: {sessionAddress}
    • {/*

      Identity

    • isLoadingIdentity: {String(isLoadingIdentity)}
    • Identity Address: {identity?.address}
    • @@ -290,7 +294,7 @@ const MasaInfo = () => { diff --git a/src/refactor/wallet-client/wallet/use-account-change-listen.ts b/src/refactor/wallet-client/wallet/use-account-change-listen.ts index 19ba3940..a3fdc4fd 100644 --- a/src/refactor/wallet-client/wallet/use-account-change-listen.ts +++ b/src/refactor/wallet-client/wallet/use-account-change-listen.ts @@ -5,7 +5,7 @@ export const useAccountChangeListen = ({ onAccountChange, onChainChange, }: Partial<{ - onAccountChange?: () => void; + onAccountChange?: (account: `0x${string}`) => void; onChainChange?: () => void; }>) => { const provider = useProvider(); @@ -21,7 +21,7 @@ export const useAccountChangeListen = ({ window.localStorage.removeItem('walletconnect'); } - onAccountChange?.(); + onAccountChange?.(account); } else if (chain) { // NOTE: this is a hack to fix the walletconnect issue if (typeof window !== 'undefined') { diff --git a/src/refactor/wallet-client/wallet/use-wallet.ts b/src/refactor/wallet-client/wallet/use-wallet.ts index dcc83acc..3397ffb1 100644 --- a/src/refactor/wallet-client/wallet/use-wallet.ts +++ b/src/refactor/wallet-client/wallet/use-wallet.ts @@ -22,6 +22,7 @@ const useWallet = () => { const [previousAddress, setPreviousAddress] = useState< `0x${string}` | undefined >(undefined); + // * NOTE: internal state to compare addresses const [compareAddress, setCompareAddress] = useState(address); const { data: signer, isLoading: isLoadingSigner } = useSigner(); const { disconnect, disconnectAsync } = useDisconnect();