Skip to content

Commit

Permalink
Merge pull request #135 from lidofinance/feature/async-safe-detection
Browse files Browse the repository at this point in the history
Async Safe wallet detection
  • Loading branch information
alx-khramov committed Apr 12, 2024
2 parents 17f84b3 + 037e62e commit c63a62c
Show file tree
Hide file tree
Showing 21 changed files with 228 additions and 114 deletions.
11 changes: 11 additions & 0 deletions packages/connect-wallet-modal/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# @reef-knot/connect-wallet-modal

## 4.1.0

### Minor Changes

- Make it possible for wallet detectors to be async;
- Improve Safe wallet detection;

### Patch Changes

- @reef-knot/wallets-list@1.13.1

## 4.0.0

### Major Changes
Expand Down
8 changes: 4 additions & 4 deletions packages/connect-wallet-modal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reef-knot/connect-wallet-modal",
"version": "4.0.0",
"version": "4.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
Expand Down Expand Up @@ -41,13 +41,13 @@
"@ledgerhq/hw-app-eth": "^6.35.2",
"@ledgerhq/hw-transport-webhid": "^6.28.1",
"@lidofinance/lido-ui": "^3.18.0",
"@reef-knot/wallets-list": "^1.13.0",
"@reef-knot/wallets-list": "^1.13.1",
"@types/react": "18.2.45",
"@types/react-dom": "18.2.17"
},
"devDependencies": {
"@reef-knot/core-react": "^3.0.0",
"@reef-knot/types": "^1.6.0",
"@reef-knot/core-react": "^3.1.0",
"@reef-knot/types": "^1.7.0",
"@reef-knot/ui-react": "^1.1.0",
"@reef-knot/wallets-helpers": "^1.1.5",
"@reef-knot/web3-react": "^3.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useReefKnotModal } from '@reef-knot/core-react';
import { WalletAdapterData } from '@reef-knot/types';

import { WalletsModalProps } from '../../types';
import { WalletModalConnectTermsProps } from '../../../Terms';
Expand Down Expand Up @@ -32,58 +33,73 @@ export const ConnectWalletModal = ({
const [inputValue, setInputValue] = useState('');
const [isShownOtherWallets, setShowOtherWallets] = useState(false);

const walletsListFull = useMemo(() => {
return sortWalletsList({
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.currentTarget.value);
},
[],
);

const handleInputClear = useCallback(() => {
setInputValue('');
}, []);

const handleToggleWalletsList = useCallback(() => {
setShowOtherWallets((value) => !value);
}, []);

const isWalletsToggleButtonShown = useRef(false);

const [walletsList, setWalletsList] = useState<WalletAdapterData[]>([]);
const [walletsListFull, setWalletsListFull] = useState<WalletAdapterData[]>(
[],
);

const getWalletsListFull = useCallback(async () => {
// Asynchronously filling wallets list because there is an async wallet detection during wallets sorting.
// Actually, almost all wallets can be detected synchronously, so this process is expected to be fast,
// and a loading indicator is not required for now.
const _walletsListFull = await sortWalletsList({
walletDataList,
walletsShown,
walletsPinned,
});
}, [walletDataList, walletsShown, walletsPinned]);
setWalletsListFull(_walletsListFull);
}, [walletDataList, walletsPinned, walletsShown]);

const walletsList = useMemo(() => {
useEffect(() => {
void getWalletsListFull();
}, [getWalletsListFull]);

useEffect(() => {
let _walletsList = walletsListFull;
if (!isShownOtherWallets) {
return walletsListFull.slice(0, walletsDisplayInitialCount);
_walletsList = walletsListFull.slice(0, walletsDisplayInitialCount);
}

if (inputValue) {
return walletsListFull.filter((wallet) =>
_walletsList = walletsListFull.filter((wallet) =>
wallet.walletName.toLowerCase().includes(inputValue.toLowerCase()),
);
}

return walletsListFull;
setWalletsList(_walletsList);
isWalletsToggleButtonShown.current =
walletsListFull.length > walletsList.length || isShownOtherWallets;
}, [
inputValue,
walletsListFull,
isShownOtherWallets,
walletsDisplayInitialCount,
walletsList.length,
walletsListFull,
]);

const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.currentTarget.value);
},
[],
);

const handleInputClear = useCallback(() => {
setInputValue('');
}, []);

const handleToggleWalletsList = useCallback(() => {
setShowOtherWallets((value) => !value);
}, []);

const isWalletsListEmpty = walletsList.length === 0;
const isWalletsToggleButtonShown =
walletsListFull.length > walletsList.length || isShownOtherWallets;

return (
<ConnectWalletModalLayout
inputValue={inputValue}
isEmptyWalletsList={isWalletsListEmpty}
isEmptyWalletsList={walletsList.length === 0}
isShownOtherWallets={isShownOtherWallets}
isShownWalletsToggleButton={isWalletsToggleButtonShown}
isShownWalletsToggleButton={isWalletsToggleButtonShown.current}
onInputChange={handleInputChange}
onInputClear={handleInputClear}
onToggleWalletsList={handleToggleWalletsList}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,44 @@ type GetWalletsListArgs = {
walletsPinned: WalletsModalProps['walletsPinned'];
};

export function sortWalletsList({
type FilteredWalletData = {
pinned: WalletAdapterData[];
detected: WalletAdapterData[];
default: WalletAdapterData[];
};

export async function sortWalletsList({
walletDataList,
walletsShown,
walletsPinned,
}: GetWalletsListArgs) {
const filteredWalletData = walletsShown.reduce(
(walletsList, walletId) => {
const walletData = walletDataList.find((w) => w.walletId === walletId);

if (!walletData) return walletsList;

const { detector, autoConnectOnly } = walletData;

// Filtering wallets marked as hidden and auto connect only
if (autoConnectOnly) return walletsList;

if (walletsPinned.includes(walletId)) {
// Put the pinned wallets on the first place, above all another
walletsList.pinned.push(walletData);
} else if (detector?.()) {
// If condition is true (usually means that a wallet is detected),
// move it to the first place in the wallets list, so a user can see it right away
walletsList.detected.push(walletData);
} else {
walletsList.default.push(walletData);
}

return walletsList;
},
{
pinned: [] as WalletAdapterData[],
detected: [] as WalletAdapterData[],
default: [] as WalletAdapterData[],
},
);
const filteredWalletData: FilteredWalletData = {
pinned: [],
detected: [],
default: [],
};

for (const walletId of walletsShown) {
const walletData = walletDataList.find((w) => w.walletId === walletId);

if (!walletData) continue;

const { detector, autoConnectOnly } = walletData;

// Filtering wallets marked as auto connect only, they should be hidden in UI
if (autoConnectOnly) continue;

if (walletsPinned.includes(walletId)) {
// Put the pinned wallets on the first place, above all other wallets
filteredWalletData.pinned.push(walletData);
} else if (await detector?.()) {
// If condition is true (usually means that a wallet is detected),
// move it to the first place in the wallets list, so a user can see it right away
filteredWalletData.detected.push(walletData);
} else {
filteredWalletData.default.push(walletData);
}
}

const pinsSorted = [...filteredWalletData.pinned].sort(
(a, b) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC, useCallback } from 'react';
import { useConnect } from 'wagmi';
import { useDisconnect } from '@reef-knot/web3-react';
import { useDisconnect } from '@reef-knot/core-react';
import { ConnectButton } from '../components/ConnectButton';
import { capitalize, suggestApp } from '../helpers';
import { ConnectInjectedProps } from './types';
Expand All @@ -21,7 +21,6 @@ export const ConnectInjected: FC<ConnectInjectedProps> = (
connector,
...rest
} = props;
const walletIsDetected = !!detector?.();
const walletIdCapitalized = capitalize(walletId);
const metricsOnConnect =
metrics?.events?.connect?.handlers[`onConnect${walletIdCapitalized}`];
Expand All @@ -40,7 +39,7 @@ export const ConnectInjected: FC<ConnectInjectedProps> = (
onBeforeConnect?.();
metricsOnClick?.();

if (walletIsDetected) {
if (await detector?.()) {
disconnect?.();
await connectAsync({ connector });
} else if (downloadURLs) {
Expand All @@ -49,11 +48,11 @@ export const ConnectInjected: FC<ConnectInjectedProps> = (
}, [
connectAsync,
connector,
detector,
disconnect,
downloadURLs,
metricsOnClick,
onBeforeConnect,
walletIsDetected,
]);

return (
Expand Down
7 changes: 7 additions & 0 deletions packages/core-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @reef-knot/core-react

## 3.1.0

### Minor Changes

- Make it possible for wallet detectors to be async;
- Improve Safe wallet detection;

## 3.0.0

### Major Changes
Expand Down
6 changes: 3 additions & 3 deletions packages/core-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reef-knot/core-react",
"version": "3.0.0",
"version": "3.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
Expand Down Expand Up @@ -41,8 +41,8 @@
},
"devDependencies": {
"@reef-knot/ledger-connector": "^3.0.0",
"@reef-knot/wallets-list": "^1.12.0",
"@reef-knot/types": "^1.5.0",
"@reef-knot/wallets-list": "^1.13.1",
"@reef-knot/types": "^1.7.0",
"@reef-knot/ui-react": "^1.0.8",
"@types/ua-parser-js": "^0.7.36",
"eslint-config-custom": "*",
Expand Down
27 changes: 20 additions & 7 deletions packages/core-react/src/hooks/useAutoConnectCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ import { useReefKnotContext } from './useReefKnotContext';

export const useAutoConnectCheck = () => {
const { walletDataList } = useReefKnotContext();
const autoConnectOnlyAdapters = walletDataList.filter(
({ autoConnectOnly }) => autoConnectOnly,
);
const isAutoConnectionSuitable = autoConnectOnlyAdapters.some((adapter) =>
adapter.detector?.(),
);

const checkIfShouldAutoConnect = async () => {
const autoConnectOnlyAdapters = walletDataList.filter(
({ autoConnectOnly }) => autoConnectOnly,
);
for (const adapter of autoConnectOnlyAdapters) {
// Try to detect at least one wallet, marked as for auto connection only
if (await adapter.detector?.()) return true;
}
return false;
};

const getAutoConnectOnlyConnectors = () => {
const autoConnectOnlyAdapters = walletDataList.filter(
({ autoConnectOnly }) => autoConnectOnly,
);
return autoConnectOnlyAdapters.map((adapter) => adapter.connector);
};

return {
isAutoConnectionSuitable,
checkIfShouldAutoConnect,
getAutoConnectOnlyConnectors,
};
};
6 changes: 3 additions & 3 deletions packages/core-react/src/hooks/useConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import { useReefKnotModal } from './useReefKnotModal';

export const useConnect = () => {
const { openModalAsync } = useReefKnotModal();
const { isAutoConnectionSuitable } = useAutoConnectCheck();
const { checkIfShouldAutoConnect } = useAutoConnectCheck();
const { eagerConnect } = useEagerConnect();

const connect = useCallback(async () => {
if (isAutoConnectionSuitable) {
if (await checkIfShouldAutoConnect()) {
const result = await eagerConnect();
return { success: !!result };
} else {
return openModalAsync({ type: 'wallet' });
}
}, [eagerConnect, openModalAsync, isAutoConnectionSuitable]);
}, [checkIfShouldAutoConnect, eagerConnect, openModalAsync]);
return { connect };
};
18 changes: 14 additions & 4 deletions packages/core-react/src/hooks/useDisconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,24 @@ export const useForceDisconnect = () => {

export const useDisconnect = (): {
disconnect?: () => void;
checkIfDisconnectMakesSense: () => boolean;
} => {
const { isConnected } = useAccount();
const { isConnected, connector } = useAccount();
const { disconnect } = useDisconnectWagmi();

const { isAutoConnectionSuitable } = useAutoConnectCheck();
const available = isConnected && !isAutoConnectionSuitable;
const { getAutoConnectOnlyConnectors } = useAutoConnectCheck();
const checkIfDisconnectMakesSense = () => {
// It doesn't make sense to offer a user the ability to disconnect if the user is not connected yet,
// or if the user was connected automatically
const autoConnectOnlyConnectors = getAutoConnectOnlyConnectors();
return (
isConnected &&
autoConnectOnlyConnectors.some((c) => c.id === connector?.id)
);
};

return {
disconnect: available ? disconnect : undefined,
disconnect: checkIfDisconnectMakesSense() ? disconnect : undefined,
checkIfDisconnectMakesSense,
};
};
Loading

0 comments on commit c63a62c

Please sign in to comment.