Skip to content

Commit

Permalink
Fix MetaMask mobile on iOS (#4229)
Browse files Browse the repository at this point in the history
* Try Firefox hack for MetaMask mobile on iOS

* Try manually injecting mobile provider

* Dont import

* Add ReactNativePostMessageStream

* Add full MM mobile injection

* Simplify

* Remove web3 shim

* Update deps

* Update FF hack

* Add comment

* Show MM mobile links for web3 mobile install
  • Loading branch information
FrederikBolding authored Dec 8, 2021
1 parent 1e2bd04 commit 7e17317
Show file tree
Hide file tree
Showing 8 changed files with 402 additions and 74 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"@ethersproject/transactions": "5.4.0",
"@ethersproject/units": "5.4.0",
"@ethersproject/wallet": "5.4.0",
"@metamask/inpage-provider": "6.0.1",
"@metamask/inpage-provider": "8.0.3",
"@metamask/object-multiplex": "1.2.0",
"@metamask/post-message-stream": "4.0.0",
"@mycrypto/eth-scan": "3.4.4",
"@mycrypto/ui": "0.24.1",
"@mycrypto/unlock-scan": "1.2.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/WalletUnlock/Web3ProviderInstall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const AppLinkContainer = styled.div`
`;

function InstallTrunk() {
const providers = [WALLETS_CONFIG.TRUST, WALLETS_CONFIG.COINBASE];
const providers = [WALLETS_CONFIG.METAMASK, WALLETS_CONFIG.COINBASE];
return (
<Box variant="rowAlign" justifyContent="space-between" mt={SPACING.BASE} width="100%">
{providers.map((provider) => (
Expand Down
4 changes: 3 additions & 1 deletion src/config/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export const WALLETS_CONFIG: Record<WalletId, IWalletConfig> = {
description: 'ADD_WEB3DESC',
helpLink: getKBHelpArticle(MIGRATE_TO_METAMASK),
install: {
getItLink: 'https://metamask.io'
getItLink: 'https://metamask.io',
appStore: 'https://apps.apple.com/us/app/metamask/id1438144202',
googlePlay: 'https://play.google.com/store/apps/details?id=io.metamask'
},
flags: {
supportsNonce: false
Expand Down
109 changes: 109 additions & 0 deletions src/vendor/MetaMask/MobilePortStream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const { Duplex } = require('readable-stream');
const { inherits } = require('util');

const noop = () => undefined;

module.exports = MobilePortStream;

inherits(MobilePortStream, Duplex);

/**
* Creates a stream that's both readable and writable.
* The stream supports arbitrary objects.
*
* @class
* @param {Object} port Remote Port object
*/
function MobilePortStream(port) {
Duplex.call(this, {
objectMode: true
});
this._name = port.name;
this._targetWindow = window;
this._port = port;
this._origin = location.origin;
window.addEventListener('message', this._onMessage.bind(this), false);
}

/**
* Callback triggered when a message is received from
* the remote Port associated with this Stream.
*
* @private
* @param {Object} msg - Payload from the onMessage listener of Port
*/
MobilePortStream.prototype._onMessage = function (event) {
const msg = event.data;

// validate message
if (this._origin !== '*' && event.origin !== this._origin) {
return;
}
if (!msg || typeof msg !== 'object') {
return;
}
if (!msg.data || typeof msg.data !== 'object') {
return;
}
if (msg.target && msg.target !== this._name) {
return;
}
// Filter outgoing messages
if (msg.data.data && msg.data.data.toNative) {
return;
}

if (Buffer.isBuffer(msg)) {
delete msg._isBuffer;
const data = Buffer.from(msg);
this.push(data);
} else {
this.push(msg);
}
};

/**
* Callback triggered when the remote Port
* associated with this Stream disconnects.
*
* @private
*/
MobilePortStream.prototype._onDisconnect = function () {
this.destroy();
};

/**
* Explicitly sets read operations to a no-op
*/
MobilePortStream.prototype._read = noop;

/**
* Called internally when data should be written to
* this writable stream.
*
* @private
* @param {*} msg Arbitrary object to write
* @param {string} encoding Encoding to use when writing payload
* @param {Function} cb Called when writing is complete or an error occurs
*/
MobilePortStream.prototype._write = function (msg, _encoding, cb) {
try {
if (Buffer.isBuffer(msg)) {
const data = msg.toJSON();
data._isBuffer = true;
window.ReactNativeWebView.postMessage(
JSON.stringify({ ...data, origin: window.location.href })
);
} else {
if (msg.data) {
msg.data.toNative = true;
}
window.ReactNativeWebView.postMessage(
JSON.stringify({ ...msg, origin: window.location.href })
);
}
} catch (err) {
return cb(new Error('MobilePortStream - disconnected'));
}
return cb();
};
80 changes: 80 additions & 0 deletions src/vendor/MetaMask/ReactNativePostMessageStream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const { Duplex } = require('readable-stream');
const { inherits } = require('util');

const noop = () => undefined;

module.exports = PostMessageStream;

inherits(PostMessageStream, Duplex);

function PostMessageStream(opts) {
Duplex.call(this, {
objectMode: true
});

this._name = opts.name;
this._target = opts.target;
this._targetWindow = opts.targetWindow || window;
this._origin = opts.targetWindow ? '*' : location.origin;

// initialization flags
this._init = false;
this._haveSyn = false;

window.addEventListener('message', this._onMessage.bind(this), false);
// send syncorization message
this._write('SYN', null, noop);
this.cork();
}

// private
PostMessageStream.prototype._onMessage = function (event) {
const msg = event.data;

// validate message
if (this._origin !== '*' && event.origin !== this._origin) {
return;
}
if (event.source !== this._targetWindow && window === top) {
return;
}
if (!msg || typeof msg !== 'object') {
return;
}
if (msg.target !== this._name) {
return;
}
if (!msg.data) {
return;
}

if (this._init) {
// forward message
try {
this.push(msg.data);
} catch (err) {
this.emit('error', err);
}
} else if (msg.data === 'SYN') {
this._haveSyn = true;
this._write('ACK', null, noop);
} else if (msg.data === 'ACK') {
this._init = true;
if (!this._haveSyn) {
this._write('ACK', null, noop);
}
this.uncork();
}
};

// stream plumbing
PostMessageStream.prototype._read = noop;

PostMessageStream.prototype._write = function (data, _encoding, cb) {
const message = {
target: this._target,
data
};
this._targetWindow.postMessage(message, this._origin);
cb();
};
115 changes: 115 additions & 0 deletions src/vendor/inpage-metamask-mobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Based of: https://github.com/MetaMask/mobile-provider/blob/main/src/inpage/index.js

import { initializeProvider } from '@metamask/inpage-provider';
import ObjectMultiplex from '@metamask/object-multiplex';
import pump from 'pump';

import MobilePortStream from './MetaMask/MobilePortStream';
import ReactNativePostMessageStream from './MetaMask/ReactNativePostMessageStream';

const INPAGE = 'metamask-inpage';
const CONTENT_SCRIPT = 'metamask-contentscript';
const PROVIDER = 'metamask-provider';

export const injectMobile = () => {
// Setup stream for content script communication
const metamaskStream = new ReactNativePostMessageStream({
name: INPAGE,
target: CONTENT_SCRIPT
});

// Initialize provider object (window.ethereum)
initializeProvider({
connectionStream: metamaskStream,
shouldSendMetadata: false
});

setupProviderStreams();
};

// Functions

/**
* Setup function called from content script after the DOM is ready.
*/
function setupProviderStreams() {
// the transport-specific streams for communication between inpage and background
const pageStream = new ReactNativePostMessageStream({
name: CONTENT_SCRIPT,
target: INPAGE
});

const appStream = new MobilePortStream({
name: CONTENT_SCRIPT
});

// create and connect channel muxes
// so we can handle the channels individually
const pageMux = new ObjectMultiplex();
pageMux.setMaxListeners(25);
const appMux = new ObjectMultiplex();
appMux.setMaxListeners(25);

pump(pageMux, pageStream, pageMux, (err) =>
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err)
);
pump(appMux, appStream, appMux, (err) => {
logStreamDisconnectWarning('MetaMask Background Multiplex', err);
notifyProviderOfStreamFailure();
});

// forward communication across inpage-background for these channels only
forwardTrafficBetweenMuxes(PROVIDER, pageMux, appMux);
}

/**
* Set up two-way communication between muxes for a single, named channel.
*
* @param {string} channelName - The name of the channel.
* @param {ObjectMultiplex} muxA - The first mux.
* @param {ObjectMultiplex} muxB - The second mux.
*/
function forwardTrafficBetweenMuxes(channelName, muxA, muxB) {
const channelA = muxA.createStream(channelName);
const channelB = muxB.createStream(channelName);
pump(channelA, channelB, channelA, (err) =>
logStreamDisconnectWarning(`MetaMask muxed traffic for channel "${channelName}" failed.`, err)
);
}

/**
* Error handler for page to extension stream disconnections
*
* @param {string} remoteLabel - Remote stream name
* @param {Error} err - Stream connection error
*/
function logStreamDisconnectWarning(remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`;
if (err) {
warningMsg += `\n${err.stack}`;
}
console.warn(warningMsg);
console.error(err);
}

/**
* This function must ONLY be called in pump destruction/close callbacks.
* Notifies the inpage context that streams have failed, via window.postMessage.
* Relies on @metamask/object-multiplex and post-message-stream implementation details.
*/
function notifyProviderOfStreamFailure() {
window.postMessage(
{
target: INPAGE, // the post-message-stream "target"
data: {
// this object gets passed to object-multiplex
name: PROVIDER, // the object-multiplex channel name
data: {
jsonrpc: '2.0',
method: 'METAMASK_STREAM_FAILURE'
}
}
},
window.location.origin
);
}
26 changes: 17 additions & 9 deletions src/vendor/inpage-metamask.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { initProvider } from '@metamask/inpage-provider';
import LocalMessageDuplexStream from 'post-message-stream';
import { initializeProvider } from '@metamask/inpage-provider';
import { WindowPostMessageStream } from '@metamask/post-message-stream';

// Firefox Metamask Hack
import { injectMobile } from './inpage-metamask-mobile';

// Metamask injection hack
// Due to https://github.com/MetaMask/metamask-extension/issues/3133

(() => {
if (!window.ethereum && !window.web3 && navigator.userAgent.includes('Firefox')) {
if (window.ethereum || window.web3) {
return;
}
if (navigator.userAgent.includes('Firefox')) {
// setup background connection
const metamaskStream = new LocalMessageDuplexStream({
name: 'inpage',
target: 'contentscript'
const metamaskStream = new WindowPostMessageStream({
name: 'metamask-inpage',
target: 'metamask-contentscript'
});

// this will initialize the provider and set it as window.ethereum
initProvider({
connectionStream: metamaskStream
initializeProvider({
connectionStream: metamaskStream,
shouldShimWeb3: true
});
} else if (navigator.userAgent.includes('iPhone')) {
injectMobile();
}
})();
Loading

0 comments on commit 7e17317

Please sign in to comment.