Skip to content

Commit

Permalink
feat(*): replace isReconnecting with unified isReady state (#335)
Browse files Browse the repository at this point in the history
* feat(*): add isReady state to track manager initialization

Add `managerStatus` to track wallet manager initialization state and expose
`isReady` getter in React, Vue, and Solid implementations. This allows
consumers to know when the wallet manager has completed initialization
and is ready for use.

- Add `managerStatus` to `State` type and `defaultState`
- Add `status` and `isReady` getters to `WalletManager` class
- Remove `isReconnecting` from React implementation (see PR #330)
- Add `isReady` to all framework implementations
- Add tests for `isReady` behavior

* docs(react): update isReady state documentation

Replace `isReconnecting` with `isReady` in React quick start guide to reflect
the new unified approach to wallet manager initialization state across all
framework implementations. Update example code and description to be more
descriptive and user-friendly.
  • Loading branch information
drichar authored Jan 17, 2025
1 parent 4e138ba commit fe252c9
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 69 deletions.
8 changes: 4 additions & 4 deletions docs/guides/react-quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ Now, in any component, you can use the `useWallet` hook to access the wallet man
import { useWallet } from '@txnlab/use-wallet-react'

function WalletMenu() {
const { wallets, activeWallet, activeAccount, isReconnecting } = useWallet()
const { wallets, activeWallet, activeAccount, isReady } = useWallet()

if (isReconnecting) {
return <div>Reconnecting to wallet...</div>
if (!isReady) {
return <div>Initializing wallet manager...</div>
}

return (
Expand Down Expand Up @@ -80,7 +80,7 @@ function WalletMenu() {
}
```
The `isReconnecting` state is a boolean that helps manage wallet reconnection flows. It starts as `true` during both SSR and initial client-side mounting. During the first mount, the `WalletProvider` automatically attempts to restore any previously connected wallet sessions. Once this reconnection process completes - whether successful or not - `isReconnecting` switches to `false`.
The `isReady` state indicates whether the wallet manager has completed initialization. It starts as `false` during both SSR and initial client-side mounting. During the first mount, the `WalletProvider` automatically attempts to restore any previously connected wallet sessions. Once this process completes, `isReady` switches to `true`.
## Signing Transactions
Expand Down
38 changes: 37 additions & 1 deletion packages/use-wallet-react/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,8 @@ describe('useWallet', () => {
activeWalletAccounts,
activeWalletAddresses,
activeAccount,
activeAddress
activeAddress,
isReady
} = useWallet()

return (
Expand All @@ -374,6 +375,7 @@ describe('useWallet', () => {
<li key={wallet.id}>{wallet.metadata.name}</li>
))}
</ul>
<div data-testid="is-ready">Is Ready: {JSON.stringify(isReady)}</div>
<div data-testid="active-network">Active Network: {JSON.stringify(activeNetwork)}</div>
<div data-testid="active-wallet">Active Wallet: {JSON.stringify(activeWallet)}</div>
<div data-testid="active-wallet-accounts">
Expand All @@ -400,6 +402,7 @@ describe('useWallet', () => {
expect(listItems[index]).toHaveTextContent(wallet.metadata.name)
})

expect(getByTestId('is-ready')).toHaveTextContent('false')
expect(getByTestId('active-network')).toHaveTextContent(JSON.stringify(NetworkId.TESTNET))
expect(getByTestId('active-wallet')).toHaveTextContent(JSON.stringify(null))
expect(getByTestId('active-wallet-accounts')).toHaveTextContent(JSON.stringify(null))
Expand All @@ -411,6 +414,7 @@ describe('useWallet', () => {
act(() => {
mockStore.setState((state) => ({
...state,
managerStatus: 'ready',
wallets: {
[WalletId.DEFLY]: {
accounts: [
Expand All @@ -429,6 +433,7 @@ describe('useWallet', () => {
}))
})

expect(getByTestId('is-ready')).toHaveTextContent('true')
expect(getByTestId('active-network')).toHaveTextContent(JSON.stringify(NetworkId.TESTNET))
expect(getByTestId('active-wallet')).toHaveTextContent(JSON.stringify(WalletId.DEFLY))
expect(getByTestId('active-wallet-accounts')).toHaveTextContent(
Expand Down Expand Up @@ -474,4 +479,35 @@ describe('useWallet', () => {
new algosdk.Algodv2(token, baseServer, port, headers)
)
})

it('initializes with isReady false and updates after resumeSessions', async () => {
const { result } = renderHook(() => useWallet(), { wrapper })

// Initially should not be ready
expect(result.current.isReady).toBe(false)

// Wait for resumeSessions to complete
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})

// Should be ready after resumeSessions
expect(result.current.isReady).toBe(true)
})

it('updates isReady when manager status changes', () => {
const { result } = renderHook(() => useWallet(), { wrapper })

expect(result.current.isReady).toBe(false)

// Simulate manager status change
act(() => {
mockStore.setState((state) => ({
...state,
managerStatus: 'ready'
}))
})

expect(result.current.isReady).toBe(true)
})
})
23 changes: 7 additions & 16 deletions packages/use-wallet-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export * from '@txnlab/use-wallet'

interface IWalletContext {
manager: WalletManager
isReconnecting: boolean
algodClient: algosdk.Algodv2
setAlgodClient: React.Dispatch<React.SetStateAction<algosdk.Algodv2>>
}
Expand All @@ -34,7 +33,10 @@ export const useWallet = () => {
throw new Error('useWallet must be used within the WalletProvider')
}

const { manager, isReconnecting, algodClient, setAlgodClient } = context
const { manager, algodClient, setAlgodClient } = context

const managerStatus = useStore(manager.store, (state) => state.managerStatus)
const isReady = managerStatus === 'ready'

const activeNetwork = useStore(manager.store, (state) => state.activeNetwork)

Expand Down Expand Up @@ -109,7 +111,7 @@ export const useWallet = () => {

return {
wallets,
isReconnecting,
isReady,
algodClient,
activeNetwork,
activeWallet,
Expand All @@ -131,7 +133,6 @@ interface WalletProviderProps {

export const WalletProvider = ({ manager, children }: WalletProviderProps): JSX.Element => {
const [algodClient, setAlgodClient] = React.useState(manager.algodClient)
const [isReconnecting, setIsReconnecting] = React.useState(true)

React.useEffect(() => {
manager.algodClient = algodClient
Expand All @@ -140,24 +141,14 @@ export const WalletProvider = ({ manager, children }: WalletProviderProps): JSX.
const resumedRef = React.useRef(false)

React.useEffect(() => {
const resumeSessions = async () => {
try {
await manager.resumeSessions()
} catch (error) {
console.error('Error resuming sessions:', error)
} finally {
setIsReconnecting(false)
}
}

if (!resumedRef.current) {
resumeSessions()
manager.resumeSessions()
resumedRef.current = true
}
}, [manager])

return (
<WalletContext.Provider value={{ manager, isReconnecting, algodClient, setAlgodClient }}>
<WalletContext.Provider value={{ manager, algodClient, setAlgodClient }}>
{children}
</WalletContext.Provider>
)
Expand Down
62 changes: 59 additions & 3 deletions packages/use-wallet-solid/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
WalletManager,
WalletId,
type State,
type WalletAccount
type WalletAccount,
type ManagerStatus
} from '@txnlab/use-wallet'
import { For, Show, createSignal } from 'solid-js'
import { Wallet, WalletProvider, useWallet, useWalletManager } from '../index'
Expand Down Expand Up @@ -69,7 +70,8 @@ const TestComponent = () => {
isWalletConnected,
walletStore,
wallets,
algodClient
algodClient,
isReady
} = useWallet()

const [magicEmail, setMagicEmail] = createSignal('')
Expand All @@ -83,6 +85,7 @@ const TestComponent = () => {

return (
<div>
<div data-testid="is-ready">{JSON.stringify(isReady())}</div>
<div data-testid="active-account">{JSON.stringify(activeAccount())}</div>
<div data-testid="active-address">{JSON.stringify(activeAddress())}</div>
<div data-testid="active-network">{activeNetwork()}</div>
Expand Down Expand Up @@ -191,7 +194,8 @@ describe('useWallet', () => {
wallets: {},
activeWallet: null,
activeNetwork: NetworkId.TESTNET,
algodClient: new algosdk.Algodv2('', 'https://testnet-api.4160.nodely.dev/')
algodClient: new algosdk.Algodv2('', 'https://testnet-api.4160.nodely.dev/'),
managerStatus: 'initializing' as ManagerStatus
}

mockStore = new Store<State>(defaultState)
Expand Down Expand Up @@ -532,6 +536,58 @@ describe('useWallet', () => {

expect(screen.getByTestId('active-network')).toHaveTextContent(NetworkId.MAINNET)
})

it('initializes with isReady false and updates after resumeSessions', async () => {
render(() => (
<WalletProvider manager={mockWalletManager}>
<TestComponent />
</WalletProvider>
))

// Initially should not be ready
expect(screen.getByTestId('is-ready')).toHaveTextContent('false')

// Simulate manager status change
mockStore.setState((state) => ({
...state,
managerStatus: 'ready'
}))

// Should be ready after status change
await waitFor(() => {
expect(screen.getByTestId('is-ready')).toHaveTextContent('true')
})
})

it('updates isReady when manager status changes', async () => {
render(() => (
<WalletProvider manager={mockWalletManager}>
<TestComponent />
</WalletProvider>
))

expect(screen.getByTestId('is-ready')).toHaveTextContent('false')

// Simulate manager status change
mockStore.setState((state) => ({
...state,
managerStatus: 'ready'
}))

await waitFor(() => {
expect(screen.getByTestId('is-ready')).toHaveTextContent('true')
})

// Simulate manager status change back to initializing (though this shouldn't happen in practice)
mockStore.setState((state) => ({
...state,
managerStatus: 'initializing'
}))

await waitFor(() => {
expect(screen.getByTestId('is-ready')).toHaveTextContent('false')
})
})
})

describe('WalletProvider', () => {
Expand Down
14 changes: 5 additions & 9 deletions packages/use-wallet-solid/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,27 +57,22 @@ export interface Wallet {
export function useWallet() {
const manager = createMemo(() => useWalletManager())

const algodClient = useStore(manager().store, (state) => state.algodClient)
const managerStatus = useStore(manager().store, (state) => state.managerStatus)
const isReady = createMemo(() => managerStatus() === 'ready')

const algodClient = useStore(manager().store, (state) => state.algodClient)
const walletStore = useStore(manager().store, (state) => state.wallets)

const walletState = (walletId: WalletId): WalletState | null => walletStore()[walletId] || null

const activeWalletId = useStore(manager().store, (state) => state.activeWallet)

const activeWallet = () => manager().getWallet(activeWalletId() as WalletId) || null

const activeWalletState = () => walletState(activeWalletId() as WalletId)

const activeWalletAccounts = () => activeWalletState()?.accounts ?? null

const activeWalletAddresses = () =>
activeWalletAccounts()?.map((account) => account.address) ?? null

const activeAccount = () => activeWalletState()?.activeAccount ?? null

const activeAddress = () => activeAccount()?.address ?? null

const isWalletActive = (walletId: WalletId) => walletId === activeWalletId()
const isWalletConnected = (walletId: WalletId) =>
!!walletState(walletId)?.accounts.length || false
Expand Down Expand Up @@ -141,6 +136,7 @@ export function useWallet() {
setActiveNetwork,
signTransactions,
transactionSigner,
wallets: manager().wallets
wallets: manager().wallets,
isReady
}
}
61 changes: 59 additions & 2 deletions packages/use-wallet-vue/src/__tests__/useWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,12 @@ describe('useWallet', () => {
})

it('integrates correctly with Vue component', async () => {
const { wallets, activeWallet, activeAddress, activeNetwork } = useWallet()
const { wallets, activeWallet, activeAddress, activeNetwork, isReady } = useWallet()

const TestComponent = {
template: `
<div>
<div data-testid="is-ready">{{ isReady }}</div>
<ul>
<li v-for="wallet in wallets" :key="wallet.id" data-testid="wallet">
{{ wallet.metadata.name }}
Expand All @@ -334,7 +335,8 @@ describe('useWallet', () => {
wallets,
activeWallet,
activeAddress,
activeNetwork
activeNetwork,
isReady
}
}
}
Expand Down Expand Up @@ -388,4 +390,59 @@ describe('useWallet', () => {
expect(wrapper.get('[data-testid="activeWallet"]').text()).toBe(WalletId.DEFLY)
expect(wrapper.get('[data-testid="activeAddress"]').text()).toBe('address1')
})

it('initializes with isReady false and updates when manager status changes', async () => {
const { isReady } = useWallet()

// Initially should not be ready
expect(isReady.value).toBe(false)

mockStore.setState((state) => ({
...state,
managerStatus: 'ready'
}))

await nextTick()

expect(isReady.value).toBe(true)

// Change back to initializing (though this shouldn't happen in practice)
mockStore.setState((state) => ({
...state,
managerStatus: 'initializing'
}))

await nextTick()

expect(isReady.value).toBe(false)
})

it('integrates isReady with Vue component', async () => {
const TestComponent = {
template: `
<div>
<div data-testid="is-ready">{{ isReady }}</div>
</div>
`,
setup() {
const { isReady } = useWallet()
return { isReady }
}
}

const wrapper = mount(TestComponent)

// Initially not ready
expect(wrapper.get('[data-testid="is-ready"]').text()).toBe('false')

mockStore.setState((state) => ({
...state,
managerStatus: 'ready'
}))

await nextTick()

// Should show ready after status change
expect(wrapper.get('[data-testid="is-ready"]').text()).toBe('true')
})
})
Loading

0 comments on commit fe252c9

Please sign in to comment.