Skip to content

Commit

Permalink
InpageBridge and BackgroundBridge (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
bitpshr authored Aug 7, 2018
1 parent 56f5b86 commit 3f52dcd
Show file tree
Hide file tree
Showing 17 changed files with 545 additions and 818 deletions.
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ buck-out/
coverage

# app-specific
/android/app/src/main/assets/entry-web3.js
/android/app/src/main/assets/entry.js
/app/entry-web3.js
/app/entry.js
/android/app/src/main/assets/InpageBridge.js
/android/app/src/main/assets/InpageBridgeWeb3.js
/app/core/InpageBridgeWeb3.js
/shim.js
8 changes: 3 additions & 5 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/app/entry.js
/app/entry-web3.js
/android/app/src/main/assets/entry.js
/android/app/src/main/assets/entry-web3.js
/app/entry.js
/android/app/src/main/assets/InpageBridge.js
/android/app/src/main/assets/InpageBridgeWeb3.js
/app/core/InpageBridgeWeb3.js
/shim.js
/shim.js
__snapshots__
Expand Down
4 changes: 3 additions & 1 deletion app/components/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const engine = new Engine();
export default createBottomTabNavigator(
{
Home: {
screen: BrowserScreen,
screen: function Home() {
return <BrowserScreen engine={engine} />;
},
navigationOptions: () => ({
title: 'ÐApps',
tabBarIcon: ico => <Icon name="dapp" size={18} color={ico.tintColor} /> // eslint-disable-line react/display-name
Expand Down
20 changes: 15 additions & 5 deletions app/components/Browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import RNFS from 'react-native-fs';
import CustomWebview from '../CustomWebview'; // eslint-disable-line import/no-unresolved
import { Alert, Platform, StyleSheet, TextInput, View } from 'react-native';
import { colors, baseStyles } from '../../styles/common';
import BackgroundBridge from '../../core/BackgroundBridge';

const styles = StyleSheet.create({
urlBar: {
Expand Down Expand Up @@ -50,7 +51,11 @@ export default class Browser extends Component {
/**
* Initial URL to load in the WebView
*/
defaultURL: PropTypes.string.isRequired
defaultURL: PropTypes.string.isRequired,
/**
* Instance of a core engine object
*/
engine: PropTypes.object.isRequired
};

state = {
Expand All @@ -70,16 +75,18 @@ export default class Browser extends Component {
webview = React.createRef();

async componentDidMount() {
this.backgroundBridge = new BackgroundBridge(this.props.engine, this.webview);

// TODO: The presence of these async statement breaks Jest code coverage
const entryScript =
Platform.OS === 'ios'
? await RNFS.readFile(`${RNFS.MainBundlePath}/entry.js`, 'utf8')
: await RNFS.readFileAssets(`entry.js`);
? await RNFS.readFile(`${RNFS.MainBundlePath}/InpageBridge.js`, 'utf8')
: await RNFS.readFileAssets(`InpageBridge.js`);

const entryScriptWeb3 =
Platform.OS === 'ios'
? await RNFS.readFile(`${RNFS.MainBundlePath}/entry-web3.js`, 'utf8')
: await RNFS.readFileAssets(`entry-web3.js`);
? await RNFS.readFile(`${RNFS.MainBundlePath}/InpageBridgeWeb3.js`, 'utf8')
: await RNFS.readFileAssets(`InpageBridgeWeb3.js`);

this.injection = { ...this.injection, entryScript, entryScriptWeb3 };
}
Expand Down Expand Up @@ -129,6 +136,9 @@ export default class Browser extends Component {
this.injection.includeWeb3 = !!data.web3;
this.handleProviderRequest();
break;
case 'INPAGE_REQUEST':
this.backgroundBridge.onMessage(data);
break;
}
};

Expand Down
10 changes: 9 additions & 1 deletion app/components/BrowserScreen/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Browser from '../Browser';
import Screen from '../Screen';

/**
* Main view component for the browser screen
*/
export default class BrowserScreen extends Component {
static propTypes = {
/**
* Instance of a core engine object
*/
engine: PropTypes.object.isRequired
};

render() {
return (
<Screen>
<Browser defaultURL="https://eip1102.herokuapp.com" />
<Browser defaultURL="https://eip1102.herokuapp.com" engine={this.props.engine} />
</Screen>
);
}
Expand Down
45 changes: 45 additions & 0 deletions app/core/BackgroundBridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export class InpageBridge {
_engine;
_webview;

_onInpageRequest(payload) {
const { current } = this._webview;
const { provider } = this._engine.api.network;
provider.sendAsync(payload, (error, response) => {
current &&
current.postMessage(
JSON.stringify({
type: 'INPAGE_RESPONSE',
payload: { error, response, __mmID: payload.__mmID }
})
);
});
}

_sendStateUpdate = () => {
const { current } = this._webview;
const { network, selectedAddress } = this._engine.datamodel.flatState;
current &&
current.postMessage({
type: 'STATE_UPDATE',
payload: { network, selectedAddress }
});
};

constructor(engine, webview) {
this._engine = engine;
this._webview = webview;
engine.api.network.subscribe(this._sendStateUpdate);
engine.api.preferences.subscribe(this._sendStateUpdate);
}

onMessage({ type, payload }) {
switch (type) {
case 'INPAGE_REQUEST':
this._onInpageRequest(payload);
break;
}
}
}

export default InpageBridge;
66 changes: 66 additions & 0 deletions app/core/BackgroundBridge.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import BackgroundBridge from './BackgroundBridge';

const MOCK_ENGINE = {
datamodel: {
flatState: {
selectedAddress: 'foo',
network: 'bar'
}
},
api: {
network: {
provider: {
sendAsync(payload, callback) {
callback(undefined, true);
}
},
subscribe(callback) {
callback(true);
}
},
preferences: {
subscribe(callback) {
callback(true);
}
}
}
};

const MOCK_WEBVIEW = {
current: {
postMessage() {
/* eslint-disable-line no-empty */
}
}
};

describe('BackgroundBridge', () => {
it('should subscribe to network store', () => {
const { network, preferences } = MOCK_ENGINE.api;
const stub1 = spyOn(network, 'subscribe');
const stub2 = spyOn(preferences, 'subscribe');
new BackgroundBridge(MOCK_ENGINE);
expect(stub1).toBeCalled();
expect(stub2).toBeCalled();
});

it('should relay response from provider', () => {
const bridge = new BackgroundBridge(MOCK_ENGINE, MOCK_WEBVIEW);
bridge.onMessage({ type: 'FOO' });
const stub = spyOn(MOCK_WEBVIEW.current, 'postMessage');
bridge.onMessage({ type: 'INPAGE_REQUEST', payload: { method: 'net_version' } });
expect(stub).toBeCalledWith(JSON.stringify({ type: 'INPAGE_RESPONSE', payload: { response: true } }));
});

it('should emit state update', () => {
const stub = spyOn(MOCK_WEBVIEW.current, 'postMessage');
new BackgroundBridge(MOCK_ENGINE, MOCK_WEBVIEW);
expect(stub).toBeCalledWith({
payload: {
network: 'bar',
selectedAddress: 'foo'
},
type: 'STATE_UPDATE'
});
});
});
18 changes: 18 additions & 0 deletions app/core/Engine.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Engine from './Engine';

describe('Engine', () => {
it('should expose an API', () => {
const engine = new Engine();
expect(engine.api).toHaveProperty('accountTracker');
expect(engine.api).toHaveProperty('addressBook');
expect(engine.api).toHaveProperty('blockHistory');
expect(engine.api).toHaveProperty('currencyRate');
expect(engine.api).toHaveProperty('keyring');
expect(engine.api).toHaveProperty('network');
expect(engine.api).toHaveProperty('networkStatus');
expect(engine.api).toHaveProperty('phishing');
expect(engine.api).toHaveProperty('preferences');
expect(engine.api).toHaveProperty('shapeShift');
expect(engine.api).toHaveProperty('tokenRates');
});
});
92 changes: 92 additions & 0 deletions app/core/InpageBridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
class InpageBridge {
_onMessage(data) {
try {
const { payload, type } = JSON.parse(data);
switch (type) {
case 'STATE_UPDATE':
this._onStateUpdate(payload);
break;

case 'INPAGE_RESPONSE':
this._onBackgroundResponse(payload);
break;
}
} catch (error) {
/* eslint-disable-line no-empty */
}
}

_onBackgroundResponse({ __mmID, error, response }) {
const callback = this._pending[__mmID];
callback && callback(error, response);
delete this._pending[__mmID];
}

_onStateUpdate(state) {
this._selectedAddress = state.selectedAddress;
this._network = state.network;
}

constructor() {
this._pending = {};
this.isMetamask = true;
document.addEventListener('message', ({ data }) => {
this._onMessage(data);
});
}

isConnected() {
return true;
}

send(payload) {
let result;

switch (payload.method) {
case 'eth_accounts':
result = this._selectedAddress ? [this._selectedAddress] : [];
break;

case 'eth_coinbase':
result = this._selectedAddress;
break;

case 'eth_uninstallFilter':
this.sendAsync(payload);
break;

case 'net_version':
result = this._network;
break;

default:
throw new Error(
`This provider requires a callback to be passed when executing methods like ${
payload.method
}. This is because all methods are always executed asynchonously. See https://git.io/fNi6S for more information.`
);
}

return {
id: payload.id,
jsonrpc: payload.jsonrpc,
result
};
}

sendAsync(payload, callback) {
payload = { ...payload, ...{ __mmID: Date.now() } };
this._pending[payload.__mmID] = callback;
window.postMessage(
{
payload,
type: 'INPAGE_REQUEST'
},
'*'
);
}
}

window.ethereum = new InpageBridge();

window.originalPostMessage({ type: 'ETHEREUM_PROVIDER_SUCCESS' }, '*');
Loading

0 comments on commit 3f52dcd

Please sign in to comment.