Skip to content

Commit

Permalink
[dapp-kit] add global connection status info to the useCurrentWallet …
Browse files Browse the repository at this point in the history
…hook along with a new useAutoConnectionStatus hook (MystenLabs#14284)

## Description 

As I was migrating SuiFrens over to dapp-kit, I noticed that there
really isn't a great way to get the global connection state of your
wallet in relation to the dApp. There are scenarios in SuiFrens where we
want to show a custom connect button with the built-in modal component
and render different UI elements based on the connection status, but we
currently have no access to this information. @Jordan-Mysten also ran
into a similar problem where he needed to determine whether or not an
auto-connection attempt was in progress.

To address this, I'm adding a react-query style `connectionStatus` with
derived booleans to the `useCurrentWallet` hook since that information
changes together. This is technically a breaking change hence the major
bump JFYI.


## Test Plan 
- Automated tests
- Manual testing
- Tested docs

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
  • Loading branch information
williamrobertson13 authored Oct 18, 2023
1 parent c98f9f6 commit fb0ce34
Show file tree
Hide file tree
Showing 22 changed files with 278 additions and 52 deletions.
12 changes: 12 additions & 0 deletions .changeset/rude-wasps-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@mysten/dapp-kit': minor
---

Add global connection status info and change the hook interface of `useCurrentWallet` to
return an object to encapsulate connection info together. To migrate:

Before:
const currentWallet = useCurrentWallet();

After:
const { currentWallet } = useCurrentWallet();
1 change: 1 addition & 0 deletions sdk/dapp-kit/src/components/WalletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function WalletProvider({
wallets: getRegisteredWallets(preferredWallets, requiredFeatures),
storageKey,
storage,
autoConnect,
}),
);

Expand Down
31 changes: 24 additions & 7 deletions sdk/dapp-kit/src/hooks/wallet/useAutoConnectWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,48 @@
import { useEffect } from 'react';

import { useConnectWallet } from './useConnectWallet.js';
import { useCurrentWallet } from './useCurrentWallet.js';
import { useWallets } from './useWallets.js';
import { useWalletStore } from './useWalletStore.js';

export function useAutoConnectWallet(autoConnectEnabled: boolean) {
const { mutate: connectWallet } = useConnectWallet();
const wallets = useWallets();
const setAutoConnectionStatus = useWalletStore((state) => state.setAutoConnectionStatus);
const lastConnectedWalletName = useWalletStore((state) => state.lastConnectedWalletName);
const lastConnectedAccountAddress = useWalletStore((state) => state.lastConnectedAccountAddress);
const wallets = useWallets();
const { isDisconnected } = useCurrentWallet();

useEffect(() => {
if (!autoConnectEnabled || !lastConnectedWalletName) return;
if (
!autoConnectEnabled ||
!lastConnectedWalletName ||
!lastConnectedAccountAddress ||
!isDisconnected
) {
return;
}

const wallet = wallets.find((wallet) => wallet.name === lastConnectedWalletName);
if (wallet) {
connectWallet({
wallet,
accountAddress: lastConnectedAccountAddress || undefined,
silent: true,
});
connectWallet(
{
wallet,
accountAddress: lastConnectedAccountAddress,
silent: true,
},
{
onSettled: () => setAutoConnectionStatus('settled'),
},
);
}
}, [
autoConnectEnabled,
connectWallet,
isDisconnected,
lastConnectedAccountAddress,
lastConnectedWalletName,
setAutoConnectionStatus,
wallets,
]);
}
12 changes: 12 additions & 0 deletions sdk/dapp-kit/src/hooks/wallet/useAutoConnectionStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { useWalletStore } from './useWalletStore.js';

/**
* Retrieves the status for the initial wallet auto-connection process.
*/
export function useAutoConnectionStatus() {
// TODO: Replace this with shareable mutation state once we require react-query v5
return useWalletStore((state) => state.autoConnectionStatus);
}
25 changes: 17 additions & 8 deletions sdk/dapp-kit/src/hooks/wallet/useConnectWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,27 @@ export function useConnectWallet({
...mutationOptions
}: UseConnectWalletMutationOptions = {}) {
const setWalletConnected = useWalletStore((state) => state.setWalletConnected);
const setConnectionStatus = useWalletStore((state) => state.setConnectionStatus);

return useMutation({
mutationKey: walletMutationKeys.connectWallet(mutationKey),
mutationFn: async ({ wallet, accountAddress, ...standardConnectInput }) => {
const connectResult = await wallet.features['standard:connect'].connect(standardConnectInput);
const connectedSuiAccounts = connectResult.accounts.filter((account) =>
account.chains.some((chain) => chain.split(':')[0] === 'sui'),
);
const selectedAccount = getSelectedAccount(connectedSuiAccounts, accountAddress);
mutationFn: async ({ wallet, accountAddress, ...connectArgs }) => {
try {
setConnectionStatus('connecting');

setWalletConnected(wallet, connectedSuiAccounts, selectedAccount);
return { accounts: connectedSuiAccounts };
const connectResult = await wallet.features['standard:connect'].connect(connectArgs);
const connectedSuiAccounts = connectResult.accounts.filter((account) =>
account.chains.some((chain) => chain.split(':')[0] === 'sui'),
);
const selectedAccount = getSelectedAccount(connectedSuiAccounts, accountAddress);

setWalletConnected(wallet, connectedSuiAccounts, selectedAccount);

return { accounts: connectedSuiAccounts };
} catch (error) {
setConnectionStatus('disconnected');
throw error;
}
},
...mutationOptions,
});
Expand Down
31 changes: 30 additions & 1 deletion sdk/dapp-kit/src/hooks/wallet/useCurrentWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,34 @@ import { useWalletStore } from './useWalletStore.js';
* Retrieves the wallet that is currently connected to the dApp, if one exists.
*/
export function useCurrentWallet() {
return useWalletStore((state) => state.currentWallet);
const currentWallet = useWalletStore((state) => state.currentWallet);
const connectionStatus = useWalletStore((state) => state.connectionStatus);

switch (connectionStatus) {
case 'connecting':
return {
connectionStatus,
currentWallet: null,
isDisconnected: false,
isConnecting: true,
isConnected: false,
} as const;
case 'disconnected':
return {
connectionStatus,
currentWallet: null,
isDisconnected: true,
isConnecting: false,
isConnected: false,
} as const;
case 'connected': {
return {
connectionStatus,
currentWallet: currentWallet!,
isDisconnected: false,
isConnecting: false,
isConnected: true,
} as const;
}
}
}
2 changes: 1 addition & 1 deletion sdk/dapp-kit/src/hooks/wallet/useDisconnectWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function useDisconnectWallet({
mutationKey,
...mutationOptions
}: UseDisconnectWalletMutationOptions = {}) {
const currentWallet = useCurrentWallet();
const { currentWallet } = useCurrentWallet();
const setWalletDisconnected = useWalletStore((state) => state.setWalletDisconnected);

return useMutation({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function useSignAndExecuteTransactionBlock({
executeFromWallet,
...mutationOptions
}: UseSignAndExecuteTransactionBlockMutationOptions = {}) {
const currentWallet = useCurrentWallet();
const { currentWallet } = useCurrentWallet();
const currentAccount = useCurrentAccount();
const client = useSuiClient();

Expand Down
2 changes: 1 addition & 1 deletion sdk/dapp-kit/src/hooks/wallet/useSignPersonalMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function useSignPersonalMessage({
mutationKey,
...mutationOptions
}: UseSignPersonalMessageMutationOptions = {}) {
const currentWallet = useCurrentWallet();
const { currentWallet } = useCurrentWallet();
const currentAccount = useCurrentAccount();

return useMutation({
Expand Down
2 changes: 1 addition & 1 deletion sdk/dapp-kit/src/hooks/wallet/useSignTransactionBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function useSignTransactionBlock({
mutationKey,
...mutationOptions
}: UseSignTransactionBlockMutationOptions = {}) {
const currentWallet = useCurrentWallet();
const { currentWallet } = useCurrentWallet();
const currentAccount = useCurrentAccount();

return useMutation({
Expand Down
2 changes: 1 addition & 1 deletion sdk/dapp-kit/src/hooks/wallet/useSwitchAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function useSwitchAccount({
mutationKey,
...mutationOptions
}: UseSwitchAccountMutationOptions = {}) {
const currentWallet = useCurrentWallet();
const { currentWallet } = useCurrentWallet();
const setAccountSwitched = useWalletStore((state) => state.setAccountSwitched);

return useMutation({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useWalletStore } from './useWalletStore.js';
* Internal hook for easily handling various changes in properties for a wallet.
*/
export function useWalletPropertiesChanged() {
const currentWallet = useCurrentWallet();
const { currentWallet } = useCurrentWallet();
const updateWalletAccounts = useWalletStore((state) => state.updateWalletAccounts);

useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions sdk/dapp-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './hooks/useSuiClientInfiniteQuery.js';
export * from './hooks/useSuiClientMutation.js';
export * from './hooks/useSuiClientQuery.js';
export * from './hooks/wallet/useAccounts.js';
export * from './hooks/wallet/useAutoConnectionStatus.js';
export * from './hooks/wallet/useConnectWallet.js';
export * from './hooks/wallet/useCurrentAccount.js';
export * from './hooks/wallet/useCurrentWallet.js';
Expand Down
32 changes: 30 additions & 2 deletions sdk/dapp-kit/src/walletStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import { createStore } from 'zustand';
import type { StateStorage } from 'zustand/middleware';
import { createJSONStorage, persist } from 'zustand/middleware';

type WalletConnectionStatus = 'disconnected' | 'connecting' | 'connected';

type WalletAutoConnectionStatus = 'disabled' | 'idle' | 'settled';

export type WalletActions = {
setAccountSwitched: (selectedAccount: WalletAccount) => void;
setConnectionStatus: (connectionStatus: WalletConnectionStatus) => void;
setAutoConnectionStatus: (autoConnectionStatus: WalletAutoConnectionStatus) => void;
setWalletConnected: (
wallet: WalletWithRequiredFeatures,
connectedAccounts: readonly WalletAccount[],
Expand All @@ -31,15 +37,23 @@ export type StoreState = {
currentAccount: WalletAccount | null;
lastConnectedAccountAddress: string | null;
lastConnectedWalletName: string | null;
connectionStatus: WalletConnectionStatus;
autoConnectionStatus: WalletAutoConnectionStatus;
} & WalletActions;

export type WalletConfiguration = {
type WalletConfiguration = {
wallets: WalletWithRequiredFeatures[];
autoConnect: boolean;
storage: StateStorage;
storageKey: string;
};

export function createWalletStore({ wallets, storage, storageKey }: WalletConfiguration) {
export function createWalletStore({
wallets,
storage,
storageKey,
autoConnect,
}: WalletConfiguration) {
return createStore<StoreState>()(
persist(
(set, get) => ({
Expand All @@ -50,13 +64,25 @@ export function createWalletStore({ wallets, storage, storageKey }: WalletConfig
lastConnectedAccountAddress: null,
lastConnectedWalletName: null,
connectionStatus: 'disconnected',
autoConnectionStatus: autoConnect ? 'idle' : 'disabled',
setConnectionStatus(connectionStatus) {
set(() => ({
connectionStatus,
}));
},
setAutoConnectionStatus(autoConnectionStatus) {
set(() => ({
autoConnectionStatus,
}));
},
setWalletConnected(wallet, connectedAccounts, selectedAccount) {
set(() => ({
accounts: connectedAccounts,
currentWallet: wallet,
currentAccount: selectedAccount,
lastConnectedWalletName: wallet.name,
lastConnectedAccountAddress: selectedAccount?.address,
connectionStatus: 'connected',
}));
},
setWalletDisconnected() {
Expand All @@ -66,6 +92,7 @@ export function createWalletStore({ wallets, storage, storageKey }: WalletConfig
currentAccount: null,
lastConnectedWalletName: null,
lastConnectedAccountAddress: null,
connectionStatus: 'disconnected',
}));
},
setAccountSwitched(selectedAccount) {
Expand All @@ -86,6 +113,7 @@ export function createWalletStore({ wallets, storage, storageKey }: WalletConfig
currentAccount: null,
lastConnectedWalletName: null,
lastConnectedAccountAddress: null,
connectionStatus: 'disconnected',
}));
} else {
set(() => ({ wallets: updatedWallets }));
Expand Down
12 changes: 6 additions & 6 deletions sdk/dapp-kit/test/components/WalletProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('WalletProvider', () => {
{ wrapper },
);

expect(result.current.currentWallet).toBeFalsy();
expect(result.current.currentWallet.isConnected).toBeFalsy();
expect(result.current.currentAccount).toBeFalsy();
expect(result.current.wallets).toHaveLength(0);
});
Expand Down Expand Up @@ -131,7 +131,6 @@ describe('WalletProvider', () => {
() => ({
connectWallet: useConnectWallet(),
currentAccount: useCurrentAccount(),
currentWallet: useCurrentWallet(),
accounts: useAccounts(),
}),
{ wrapper },
Expand Down Expand Up @@ -186,8 +185,10 @@ describe('WalletProvider', () => {
{ wrapper },
);

await waitFor(() => expect(updatedResult.current.currentWallet).toBeTruthy());
expect(updatedResult.current.currentWallet!.name).toStrictEqual('Mock Wallet 1');
await waitFor(() => expect(updatedResult.current.currentWallet.isConnected).toBe(true));
expect(updatedResult.current.currentWallet.currentWallet!.name).toStrictEqual(
'Mock Wallet 1',
);

expect(updatedResult.current.currentAccount).toBeTruthy();
expect(updatedResult.current.currentAccount!.address).toStrictEqual(
Expand All @@ -209,7 +210,6 @@ describe('WalletProvider', () => {
() => ({
connectWallet: useConnectWallet(),
disconnectWallet: useDisconnectWallet(),
currentWallet: useCurrentWallet(),
currentAccount: useCurrentAccount(),
}),
{ wrapper },
Expand All @@ -229,7 +229,7 @@ describe('WalletProvider', () => {

// Render our component tree again and assert that we weren't able to auto-connect.
const { result: updatedResult } = renderHook(() => useCurrentWallet(), { wrapper });
expect(updatedResult.current).toBeFalsy();
expect(updatedResult.current.isConnected).toBeFalsy();

act(() => unregister());
});
Expand Down
Loading

0 comments on commit fb0ce34

Please sign in to comment.