Metamask-extension: No "Connect" request from iframe web3 app when changing to unconnected wallet

Created on 11 Aug 2020  路  4Comments  路  Source: MetaMask/metamask-extension

When a web3 app is inside an iframe, changing the active wallet doesn't trigger a new "Connect" request inside Metamask.

Sample connect request:
image

Simple reproduction:

  1. Go to https://uniswap.org/docs/v2/interface-integration/iframe-integration/
  2. Scroll to the Uniswap iframe
  3. If you're not already connected to it, click Connect inside the iframe and connect it to Metamask.
  4. Create a new wallet in Metamask (or switch to one that you haven't connected to Uniswap before).
  5. Change your active wallet inside Metamask to the new one.

No connect requests will be triggered. Uniswap will show you the last wallet that you approved a connection for, but not the one that you changed to.

You can only change between wallets that are _already connected_ to the web3 app. Then the addresses update correctly inside the iframe. There's no other way to connect the wallet than to open the iframe in a new tab and approving the connection request after switching.

S2-normal T00-bug

All 4 comments

Thanks for the report! This case was not considered - currently only the parent frame is considered by our popup UI as the "active dapp".

We might be able to fall back to looking at a child frame if the child frame is connected but the parent isn't 馃. I'll have to check and see whether the web extension API provides information about child frames in the active tab. Handling _nested_ connected dapps would require a more substantial design change though.

When you encountered this issue, was the dapp in the iframe the only connected dapp (as in the example you linked)? Or did you also have nested dapps in mind?

There's no other way to connect the wallet than to open the iframe in a new tab and approving the connection request after switching.

This part is concerning on its own. Even if we decide not to consider nested dapps in our UI, we should at least provide a way to manually connect new accounts to them.

This part is concerning on its own. Even if we decide not to consider nested dapps in our UI, we should at least provide a way to manually connect new accounts to them.

@Gudahtt I wrote another issue which could be a relevant fix for "same origin url" apps: https://github.com/MetaMask/metamask-extension/issues/9190. If apps are identified only by their origin domain and _not port_ (so myapp.host.org:8000 is identified as myapp.host.org), then apps inside iframes that are on the same domain but a different port will immediately be connected once the parent app is connected.

For now, I built a (imo elegant) solution. I think it's the best possible workaround while there isn't yet a native fix:

The user experience is smooth (only 2 extra clicks):

  1. User changes wallet
  2. If it's not connected to the iframe they get a new tab saying "connect your new metamask wallet".
  3. They open metamask and the "connect" prompt is already open. They click "connect", tab auto-closes, and they're back on the parent app with updated wallets.

Here's how it works:

  • I use postMessage logic to send the new address to the iframe on wallet change.
  • In the iframe's event listener, the "parent" ethereum address is compared to the one in the iframe app.
  • If the addresses are different, that means the new wallet is not yet connected to the iframe app, so the iframe sends an openConnect message event back to the parent.
  • The parent then opens a new tab to a "Connect" page that's hosted on the iframe's (sub)domain, displaying the message "Open metamask and connect your new wallet. This page queries for addresses on page load to open the "Connect" panel in Metamask.
  • As soon as the wallet is connected, the accountsChanged event is fired by Metamask. On that event, a closeMe event is sent by the "pop-up" to window.opener (the "parent" app), and the parent app closes the pop-up.

Here's the code:

Parent app events (Svelte) (the iframe html object is bound to let frame :

<script>
  // Sends new address to iframe
  let frame: any;
  const eth: any = (window as any).ethereum;
  const updateWeb3Api = async () => {
    frame.contentWindow.postMessage(
      { type: "ethAddress", address: $ethAddress },
      config.iframeUrl
    );
  };

  // This triggers every time there's a new address. With vanilla js you should listen for `eth_accountsChanged` metamask event here.
  $: if (loaded && $ethAddress) {
    updateWeb3Api();
  }

  // Listen for events from iframe (to open Connect page in new tab or close it)
  let registered: boolean = false;
  let eventer: any;
  let manageWindow: any;
  const eventHandler = (e: any) => {
    if (e.data === "goToManage") {
      manageWindow = window.open(config.iframeUrl + "/manage", "_blank");
    }
    if (e.data === "closeManage") {
      manageWindow.close();
    }
  };

  // Mount eventListener
  onMount(() => {
    if (!registered) {
      registered = true;
      // Listen to message from child window
      window.addEventListener("message", eventHandler);
    }
  });

  // Unmount eventListener
  onDestroy(() => {
    loaded = false;
    if (registered) {
      window.removeEventListener("message", eventHandler);
    }
    registered = false;
  });
</script>


<iframe
  on:load={() => (loaded = true)}
  id="iframe"
  src="{config.iframeUrl}/{urlExt}"
  bind:this={frame}
  {style} />

iframe app index.tsx event listener/pusher (listens for address change events sent by parent):

if ('ethereum' in window) {
  ;(window.ethereum as any).autoRefreshOnNetworkChange = false
  const isMetamask = window.ethereum.isMetaMask
  // Handle new Ethereum account from iframe
  if (!listenerAttached) {
    listenerAttached = true
    window.addEventListener('message', async event => {
      if (event.data.type === 'ethAddress' && event.origin.startsWith(process.env.REACT_APP_PARENT)) {
        const acc = await (window.ethereum as any).request({ method: 'eth_requestAccounts' })
        if (isMetamask && String(acc[0]).toLowerCase() !== String(event.data?.address).toLowerCase()) {
          window.parent.postMessage('goToManage', process.env.REACT_APP_PARENT)
        }
      }
    })
  }
}

iframe app "Connect" tab (sends "closeMe" event to the parent app on connect so it can close this tab) (note you also need to query for accounts so the Connect tab is open as soon as you click Metamask):

export default function Manage() {
  ;(window.ethereum as any).on('accountsChanged', () => {
    // @ts-ignore
    if (window.opener) {
      window.opener.postMessage('closeManage', process.env.REACT_APP_WALLET)
    }
  })

  return (
    <p>
            Open MetaMask and connect your new wallet, then close this window.
    </p>
  )
}

When you encountered this issue, was the dapp in the iframe the only connected dapp (as in the example you linked)? Or did you also have nested dapps in mind?

@Gudahtt In my use case I have a parent web3 app, and inside it an iframe with another web3 app. So the parent gets "connect" requests for Metamask for wallet changes, but not the iframe. So for my use case the fallback should be a check "if the parent is connected but not the iframe".

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MarkOSullivan94 picture MarkOSullivan94  路  3Comments

hellobart picture hellobart  路  3Comments

BMillman19 picture BMillman19  路  3Comments

kumavis picture kumavis  路  3Comments

danfinlay picture danfinlay  路  3Comments