Skip to content

Commit

Permalink
feat(core-react): rework useAutoConnect
Browse files Browse the repository at this point in the history
Separate logic, handle `chainChanged` event
  • Loading branch information
alx-khramov committed Feb 8, 2024
1 parent 745d134 commit 7a6b67e
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 65 deletions.
18 changes: 10 additions & 8 deletions packages/core-react/src/context/acceptTermsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { createContext, useMemo, useState } from 'react';

export type AcceptTermsModal = {
isVisible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
onContinue: () => void;
setOnContinue: React.Dispatch<React.SetStateAction<() => void>>;
error?: Error;
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
};

export type AcceptTermsModalContextValue = {
acceptTermsModal: {
isVisible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
onContinue: () => void;
setOnContinue: React.Dispatch<React.SetStateAction<() => void>>;
error?: Error;
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
};
acceptTermsModal: AcceptTermsModal;
};

const isVisibleDefaultValue = false;
Expand Down
8 changes: 8 additions & 0 deletions packages/core-react/src/helpers/checkTermsAccepted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { LS_KEY_TERMS_ACCEPTANCE } from '../constants/localStorage';

export const checkTermsAccepted = () => {
if (typeof window !== 'undefined') {
return window.localStorage?.getItem(LS_KEY_TERMS_ACCEPTANCE) === 'true';
}
return false;
};
157 changes: 100 additions & 57 deletions packages/core-react/src/hooks/useAutoConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,98 @@ import { connect, disconnect } from 'wagmi/actions';
import type { ConnectResult, Connector } from '@wagmi/core';
import { useContext, useEffect, useRef } from 'react';
import { WalletAdapterData } from '@reef-knot/types';
import { AcceptTermsModalContext } from '../context/acceptTermsModal';
import { LS_KEY_TERMS_ACCEPTANCE } from '../constants/localStorage';
import {
AcceptTermsModal,
AcceptTermsModalContext,
} from '../context/acceptTermsModal';
import { getUnsupportedChainError } from '../helpers/getUnsupportedChainError';
import { checkTermsAccepted } from '../helpers/checkTermsAccepted';

const isTermsAccepted = () => {
if (typeof window !== 'undefined') {
return window.localStorage?.getItem(LS_KEY_TERMS_ACCEPTANCE) === 'true';
const connectAndHandleErrors = async (
connector: Connector,
supportedChains: Chain[],
acceptTermsModal: AcceptTermsModal,
): Promise<{
connectResult: ConnectResult | null;
connectError?: Error;
}> => {
let connectResult = null;
let connectError;

try {
connectResult = await connect({ connector });
} catch (e) {
connectResult = null; // ensure that connectResult is empty in case of an error
connectError = e as Error;
}

if (connectResult?.chain.unsupported) {
// No errors during connection, but the chain is unsupported.
// This case is considered as error for now, and we explicitly call disconnect() here.
// This logic comes from previously used web3-react connection logic, which wasn't reworked yet after web3-react removal.
// web3-react logic was: if a chain is unsupported – break the connection, throw an error
// wagmi logic is: if a chain is unsupported – connect anyway, without errors, set `chain.unsupported` flag to true.
// So, here we are trying to mimic the legacy logic, because we are not ready to rework it yet.
connectResult = null;
connectError = getUnsupportedChainError(supportedChains);
await disconnect();

// A user may change a chain in a wallet app, prepare for that event.
// There is a strong recommendation in the MetaMask documentation
// to reload the page upon chain changes, unless there is a good reason not to.
// This looks like a good general approach.
const provider = await connector.getProvider();
provider.once('chainChanged', () => globalThis.window?.location.reload());
}

if (connectError) {
acceptTermsModal.setError?.(connectError);
acceptTermsModal.setVisible?.(true);
} else {
acceptTermsModal.setVisible?.(false);
acceptTermsModal.setError?.(undefined);
}

return { connectResult, connectError };
};

const connectEagerly = async (
adapters: WalletAdapterData[],
acceptTermsModal: AcceptTermsModal,
supportedChains: Chain[],
): Promise<{
connectResult: ConnectResult | null;
connectError?: Error;
}> => {
const isTermsAccepted = checkTermsAccepted();
let connectResult = null;
let connectError;

const continueConnection = async (connector: Connector) => {
({ connectResult, connectError } = await connectAndHandleErrors(
connector,
supportedChains,
acceptTermsModal,
));
};

for (const adapter of adapters) {
if (adapter.detector?.()) {
// wallet is detected
if (!isTermsAccepted) {
// Terms of service were not accepted previously.
// So, for legal reasons, we must ask a user to accept the terms before connecting.
const onContinue = () => void continueConnection(adapter.connector);
acceptTermsModal.setOnContinue?.(() => onContinue);
acceptTermsModal.setVisible?.(true);
} else {
await continueConnection(adapter.connector);
}
break; // no need to iterate over all other adapters if at least one was detected
}
}
return false;

return { connectResult, connectError };
};

export const useAutoConnect = (
Expand All @@ -28,65 +111,25 @@ export const useAutoConnect = (
);

useEffect(() => {
// Don't auto-connect if already connected or if the auto-connect feature is disabled or if already tried to auto-connect.
if (isConnected || !autoConnectEnabled || isAutoConnectCalled.current)
return;

void (async () => {
// The current logic is to try auto-connect only once, even if an error happened and connection was not successful.
isAutoConnectCalled.current = true;
let connectResult: ConnectResult | null = null;
let connectError: Error | undefined;

const connectAndHandleErrors = async (connector: Connector) => {
connectError = undefined; // reset previous error if any
try {
connectResult = await connect({ connector });
} catch (e) {
connectResult = null; // ensure that connectResult is empty in case of an error
connectError = e as Error;
}

if (!connectError && connectResult?.chain.unsupported) {
// No errors during connection, but the chain is unsupported.
// This case is considered as error for now, and we explicitly call disconnect() here.
// This logic comes from previously used web3-react connection logic, which wasn't reworked yet after web3-react removal.
// web3-react logic was: if a chain is unsupported – break the connection, throw an error
// wagmi logic is: if a chain is unsupported – connect anyway, without errors, set `chain.unsupported` flag to true.
// So, here we are trying to mimic the legacy logic, because we are not ready to rework it yet.
connectError = getUnsupportedChainError(chains);
connectResult = null;
await disconnect();
}

if (connectError) {
acceptTermsModal.setError?.(connectError);
acceptTermsModal.setVisible?.(true);
} else {
acceptTermsModal.setVisible?.(false);
acceptTermsModal.setError?.(undefined);
}
};

// First, check wallets that are meant to be used only with auto-connection.
// Try to eagerly connect wallets that are meant to be used only with auto-connection.
// For example, wallets with dApp browsers, or using iframes to open dApps.
for (const adapter of autoConnectOnlyAdapters) {
if (adapter.detector?.()) {
// autoConnectOnly wallet is detected
if (!isTermsAccepted()) {
// Terms os service were not accepted previously.
// So, for legal reasons, we must ask a user to accept the terms before connecting.
const onContinue = () => {
void connectAndHandleErrors(adapter.connector);
};
acceptTermsModal.setOnContinue?.(() => onContinue);
acceptTermsModal.setVisible?.(true);
}
await connectAndHandleErrors(adapter.connector);
}
}
const { connectResult, connectError } = await connectEagerly(
autoConnectOnlyAdapters,
acceptTermsModal,
chains,
);

if (!connectResult && !connectError) {
// Finally, if connection didn't happen earlier and there were no errors,
// call the default wagmi autoConnect method, which attempts to connect to the last used connector.
// If still not connected and there were no errors and the terms of service are accepted,
// call the default wagmi autoConnect method, which attempts to connect to the last used connector.
if (!connectResult && !connectError && checkTermsAccepted()) {
await client.autoConnect();
}
})();
Expand Down

0 comments on commit 7a6b67e

Please sign in to comment.