Skip to content

Commit

Permalink
Add MetaMask Snaps feature detection (MetaMask#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeRx authored Aug 24, 2023
1 parent 7704dd4 commit b5146ce
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 27 deletions.
49 changes: 32 additions & 17 deletions packages/site/src/hooks/MetamaskContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import {
useReducer,
} from 'react';
import { Snap } from '../types';
import { isFlask, getSnap } from '../utils';
import { detectSnaps, getSnap, isFlask } from '../utils';

export type MetamaskState = {
snapsDetected: boolean;
isFlask: boolean;
installedSnap?: Snap;
error?: Error;
};

const initialState: MetamaskState = {
snapsDetected: false,
isFlask: false,
error: undefined,
};

type MetamaskDispatch = { type: MetamaskActions; payload: any };
Expand All @@ -33,8 +34,9 @@ export const MetaMaskContext = createContext<

export enum MetamaskActions {
SetInstalled = 'SetInstalled',
SetFlaskDetected = 'SetFlaskDetected',
SetSnapsDetected = 'SetSnapsDetected',
SetError = 'SetError',
SetIsFlask = 'SetIsFlask',
}

const reducer: Reducer<MetamaskState, MetamaskDispatch> = (state, action) => {
Expand All @@ -45,18 +47,21 @@ const reducer: Reducer<MetamaskState, MetamaskDispatch> = (state, action) => {
installedSnap: action.payload,
};

case MetamaskActions.SetFlaskDetected:
case MetamaskActions.SetSnapsDetected:
return {
...state,
snapsDetected: action.payload,
};
case MetamaskActions.SetIsFlask:
return {
...state,
isFlask: action.payload,
};

case MetamaskActions.SetError:
return {
...state,
error: action.payload,
};

default:
return state;
}
Expand All @@ -76,30 +81,40 @@ export const MetaMaskProvider = ({ children }: { children: ReactNode }) => {

const [state, dispatch] = useReducer(reducer, initialState);

// Find MetaMask Provider and search for Snaps
// Also checks if MetaMask version is Flask
useEffect(() => {
async function detectFlask() {
const isFlaskDetected = await isFlask();

const setSnapsCompatibility = async () => {
dispatch({
type: MetamaskActions.SetFlaskDetected,
payload: isFlaskDetected,
type: MetamaskActions.SetSnapsDetected,
payload: await detectSnaps(),
});
}
};

setSnapsCompatibility();
}, [window.ethereum]);

// Set installed snaps
useEffect(() => {
async function detectSnapInstalled() {
const installedSnap = await getSnap();
dispatch({
type: MetamaskActions.SetInstalled,
payload: installedSnap,
payload: await getSnap(),
});
}

detectFlask();
const checkIfFlask = async () => {
dispatch({
type: MetamaskActions.SetIsFlask,
payload: await isFlask(),
});
};

if (state.isFlask) {
if (state.snapsDetected) {
detectSnapInstalled();
checkIfFlask();
}
}, [state.isFlask, window.ethereum]);
}, [state.snapsDetected]);

useEffect(() => {
let timeoutId: number;
Expand Down
14 changes: 10 additions & 4 deletions packages/site/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MetamaskActions, MetaMaskContext } from '../hooks';
import {
connectSnap,
getSnap,
isLocalSnap,
sendHello,
shouldDisplayReconnectButton,
} from '../utils';
Expand All @@ -14,6 +15,7 @@ import {
SendHelloButton,
Card,
} from '../components';
import { defaultSnapOrigin } from '../config';

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -102,6 +104,10 @@ const ErrorMessage = styled.div`
const Index = () => {
const [state, dispatch] = useContext(MetaMaskContext);

const isMetaMaskReady = isLocalSnap(defaultSnapOrigin)
? state.isFlask
: state.snapsDetected;

const handleConnectClick = async () => {
try {
await connectSnap();
Expand Down Expand Up @@ -140,7 +146,7 @@ const Index = () => {
<b>An error happened:</b> {state.error.message}
</ErrorMessage>
)}
{!state.isFlask && (
{!isMetaMaskReady && (
<Card
content={{
title: 'Install',
Expand All @@ -160,11 +166,11 @@ const Index = () => {
button: (
<ConnectButton
onClick={handleConnectClick}
disabled={!state.isFlask}
disabled={!isMetaMaskReady}
/>
),
}}
disabled={!state.isFlask}
disabled={!isMetaMaskReady}
/>
)}
{shouldDisplayReconnectButton(state.installedSnap) && (
Expand Down Expand Up @@ -197,7 +203,7 @@ const Index = () => {
}}
disabled={!state.installedSnap}
fullWidth={
state.isFlask &&
isMetaMaskReady &&
Boolean(state.installedSnap) &&
!shouldDisplayReconnectButton(state.installedSnap)
}
Expand Down
6 changes: 5 additions & 1 deletion packages/site/src/types/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { MetaMaskInpageProvider } from '@metamask/providers';

declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
ethereum: MetaMaskInpageProvider & {
setProvider?: (provider: MetaMaskInpageProvider) => void;
detected?: MetaMaskInpageProvider[];
providers?: MetaMaskInpageProvider[];
};
}
}
52 changes: 51 additions & 1 deletion packages/site/src/utils/metamask.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
import { getSnaps } from './snap';

/**
* Tries to detect if one of the injected providers is MetaMask and checks if snaps is available in that MetaMask version.
*
* @returns True if the MetaMask version supports Snaps, false otherwise.
*/
export const detectSnaps = async () => {
if (window.ethereum?.detected) {
for (const provider of window.ethereum.detected) {
try {
// Detect snaps support
await getSnaps(provider);

// enforces MetaMask as provider
if (window.ethereum.setProvider) {
window.ethereum.setProvider(provider);
}

return true;
} catch {
// no-op
}
}
}

if (window.ethereum?.providers) {
for (const provider of window.ethereum.providers) {
try {
// Detect snaps support
await getSnaps(provider);

window.ethereum = provider;

return true;
} catch {
// no-op
}
}
}

try {
await getSnaps();

return true;
} catch {
return false;
}
};

/**
* Detect if the wallet injecting the ethereum object is Flask.
* Detect if the wallet injecting the ethereum object is MetaMask Flask.
*
* @returns True if the MetaMask version is Flask, false otherwise.
*/
Expand Down
10 changes: 6 additions & 4 deletions packages/site/src/utils/snap.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { MetaMaskInpageProvider } from '@metamask/providers';
import { defaultSnapOrigin } from '../config';
import { GetSnapsResponse, Snap } from '../types';

/**
* Get the installed snaps in MetaMask.
*
* @param provider - The MetaMask inpage provider.
* @returns The snaps installed in MetaMask.
*/
export const getSnaps = async (): Promise<GetSnapsResponse> => {
return (await window.ethereum.request({
export const getSnaps = async (
provider?: MetaMaskInpageProvider,
): Promise<GetSnapsResponse> =>
(await (provider ?? window.ethereum).request({
method: 'wallet_getSnaps',
})) as unknown as GetSnapsResponse;
};

/**
* Connect a snap to MetaMask.
*
Expand Down

0 comments on commit b5146ce

Please sign in to comment.