Skip to content

Commit

Permalink
build: Pull @metamask/mobile-provider back into metamask-mobile (#…
Browse files Browse the repository at this point in the history
…7494)

## **Description**

Currently the inpage injected scripts used in the in-app browser (for
making the wallet provider object available to the dapp) are currently
built in a separate `@metamask/mobile-provider` package. This PR brings
the build process into the mobile repo itself. This is needed in order
to support passing build time variables to the provider initialization
(for EIP6963 support).

## **Manual testing steps**

1. Pull in branch
2. `yarn setup`
3. `yarn watch`
4. `yarn start:ios`
5. Go to in-app browser
6. Visit test dapp
7. Verify that buttons are still functional
8. Optionally: Open safari debug console on the test dapp webview
9. check that `window.ethereum` is defined and `window.ethereum.chainId`
logs a deprecation warning

## **Screenshots/Recordings**
Note the deprecation warnings which indicates that provider v13.0.0 is
now being used

![image](https://github.com/MetaMask/metamask-mobile/assets/918701/143a5f56-29d4-4b7c-8192-d4595689ebba)


## **Related issues**

See: MetaMask/MetaMask-planning#869

## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've clearly explained:
  - [x] What problem this PR is solving.
  - [x] How this problem was solved.
  - [x] How reviewers can test my changes.
- [x] I’ve indicated what issue this PR is linked to: Fixes #???
- [ ] I’ve included tests if applicable.
- [x] I’ve documented any added code.
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
- [x] I’ve properly set the pull request status:
  - [x] In case it's not yet "ready for review", I've set it to "draft".
- [x] In case it's "ready for review", I've changed it from "draft" to
"non-draft".

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: Cal Leung <cleun007@gmail.com>
  • Loading branch information
jiexi and Cal-L authored Oct 18, 2023
1 parent 298622f commit bf25c3e
Show file tree
Hide file tree
Showing 16 changed files with 740 additions and 63 deletions.
1 change: 1 addition & 0 deletions .depcheckrc.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# List things here that *are - 'used, that depcheck is wrong about'
ignores:
- '@metamask/oss-attribution-generator'
- 'webpack-cli'

# Note: Everything below this line should be removed after investigation
# TODO: Investigate each dependency to see whether it's used
Expand Down
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/scripts/inpage-bridge
/app/core/InpageBridgeWeb3.js
/app/util/blockies.js
__snapshots__
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ buck-out/
tests

# app-specific
/scripts/inpage-bridge/dist
/android/app/src/main/assets/InpageBridgeWeb3.js
/app/core/InpageBridgeWeb3.js

Expand Down
2 changes: 1 addition & 1 deletion app/util/streams.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable import/no-commonjs */
const Through = require('through2');
const ObjectMultiplex = require('obj-multiplex');
const ObjectMultiplex = require('@metamask/object-multiplex');
const pump = require('pump');

/**
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,6 @@
"metro-config": "^0.71.1",
"multihashes": "0.4.14",
"number-to-bn": "1.7.0",
"obj-multiplex": "1.0.0",
"obs-store": "4.0.3",
"path": "0.12.7",
"pbkdf2": "3.1.2",
Expand Down Expand Up @@ -325,7 +324,7 @@
"react-native-webview": "11.13.0",
"react-native-webview-invoke": "^0.6.2",
"react-redux": "7.2.4",
"readable-stream": "1.0.33",
"readable-stream": "2.3.7",
"redux": "4.1.1",
"redux-mock-store": "1.5.4",
"redux-persist": "6.0.0",
Expand All @@ -335,7 +334,7 @@
"reselect": "^4.0.0",
"rn-fetch-blob": "^0.12.0",
"socket.io-client": "^4.5.3",
"stream-browserify": "1.0.0",
"stream-browserify": "3.0.0",
"through2": "3.0.1",
"unicode-confusables": "^0.1.1",
"url": "0.11.0",
Expand All @@ -359,7 +358,9 @@
"@metamask/eslint-config": "^9.0.0",
"@metamask/eslint-config-typescript": "^9.0.0",
"@metamask/mobile-provider": "^3.0.0",
"@metamask/object-multiplex": "^1.1.0",
"@metamask/oss-attribution-generator": "^2.0.1",
"@metamask/providers": "^13.0.0",
"@metamask/test-dapp": "^7.1.0",
"@react-native-community/datetimepicker": "^7.5.0",
"@react-native-community/eslint-config": "^2.0.0",
Expand Down Expand Up @@ -468,6 +469,8 @@
"ts-node": "^10.5.0",
"typescript": "~4.8.4",
"wdio-cucumberjs-json-reporter": "^4.4.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"xml2js": "^0.5.0",
"yarn-deduplicate": "^6.0.2"
},
Expand Down
13 changes: 13 additions & 0 deletions scripts/build-inpage-bridge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail

rm -f app/core/InpageBridgeWeb3.js
mkdir -p scripts/inpage-bridge/dist && rm -rf scripts/inpage-bridge/dist/*
cd scripts/inpage-bridge/inpage
../../../node_modules/.bin/webpack --config webpack.config.js
cd ..
node content-script/build.js
cat dist/inpage-bundle.js content-script/index.js > dist/index-raw.js
../../node_modules/.bin/webpack --config webpack.config.js
cd ../..
cp scripts/inpage-bridge/dist/index.js app/core/InpageBridgeWeb3.js
1 change: 0 additions & 1 deletion scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ checkParameters(){

prebuild(){
# Import provider
cp node_modules/@metamask/mobile-provider/dist/index.js app/core/InpageBridgeWeb3.js
yarn --ignore-engines build:static-logos

# Load JS specific env variables
Expand Down
14 changes: 14 additions & 0 deletions scripts/inpage-bridge/content-script/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const fs = require('fs');
const path = require('path');

const distPath = path.join(__dirname, '..', '/', 'dist');

const inpageContent = fs
.readFileSync(path.join(distPath, 'inpage-content.js'))
.toString();

// wrap the inpage content in a variable declaration
const code = `const inpageBundle = ${JSON.stringify(inpageContent)}`;

fs.writeFileSync(path.join(distPath, 'inpage-bundle.js'), code, 'ascii');
console.log('content-script.js generated succesfully');
147 changes: 147 additions & 0 deletions scripts/inpage-bridge/content-script/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/* global inpageBundle */

if (shouldInject()) {
injectScript(inpageBundle);
start();
}

// Functions

/**
* Sets up the stream communication and submits site metadata
*
*/
async function start() {
await domIsReady();
window._metamaskSetupProvider();
}

/**
* Injects a script tag into the current document
*
* @param {string} content - Code to be executed in the current document
*/
function injectScript(content) {
try {
const container = document.head || document.documentElement;

// synchronously execute script in page context
const scriptTag = document.createElement('script');
scriptTag.setAttribute('async', false);
scriptTag.textContent = content;
container.insertBefore(scriptTag, container.children[0]);

// script executed; remove script element from DOM
container.removeChild(scriptTag);
} catch (err) {
console.error('MetaMask script injection failed', err);
}
}

/**
* Determines if the provider should be injected.
*
* @returns {boolean} {@code true} if the provider should be injected.
*/
function shouldInject() {
return (
doctypeCheck() &&
suffixCheck() &&
documentElementCheck() &&
!blockedDomainCheck()
);
}

/**
* Checks the doctype of the current document if it exists
*
* @returns {boolean} {@code true} if the doctype is html or if none exists
*/
function doctypeCheck() {
const { doctype } = window.document;
if (doctype) {
return doctype.name === 'html';
}
return true;
}

/**
* Returns whether or not the extension (suffix) of the current document is
* prohibited.
*
* This checks {@code window.location.pathname} against a set of file extensions
* that should not have the provider injected into them. This check is indifferent
* of query parameters in the location.
*
* @returns {boolean} whether or not the extension of the current document is prohibited
*/
function suffixCheck() {
const prohibitedTypes = [/\\.xml$/u, /\\.pdf$/u];
const currentUrl = window.location.pathname;
for (let i = 0; i < prohibitedTypes.length; i++) {
if (prohibitedTypes[i].test(currentUrl)) {
return false;
}
}
return true;
}

/**
* Checks the documentElement of the current document
*
* @returns {boolean} {@code true} if the documentElement is an html node or if none exists
*/
function documentElementCheck() {
const documentElement = document.documentElement.nodeName;
if (documentElement) {
return documentElement.toLowerCase() === 'html';
}
return true;
}

/**
* Checks if the current domain is blocked
*
* @returns {boolean} {@code true} if the current domain is blocked
*/
function blockedDomainCheck() {
const blockedDomains = [
'uscourts.gov',
'dropbox.com',
'webbyawards.com',
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
'adyen.com',
'gravityforms.com',
'harbourair.com',
'ani.gamer.com.tw',
'blueskybooking.com',
'sharefile.com',
];
const currentUrl = window.location.href;
let currentRegex;
for (let i = 0; i < blockedDomains.length; i++) {
const blockedDomain = blockedDomains[i].replace('.', '\\.');
currentRegex = new RegExp(
`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`,
'u',
);
if (!currentRegex.test(currentUrl)) {
return true;
}
}
return false;
}

/**
* Returns a promise that resolves when the DOM is loaded (does not wait for images to load)
*/
async function domIsReady() {
// already loaded
if (['interactive', 'complete'].includes(document.readyState)) {
return;
}
// wait for load
await new Promise((resolve) =>
window.addEventListener('DOMContentLoaded', resolve, { once: true }),
);
}
109 changes: 109 additions & 0 deletions scripts/inpage-bridge/inpage/MobilePortStream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const { inherits } = require('util');
const { Duplex } = require('readable-stream');

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();
};
Loading

0 comments on commit bf25c3e

Please sign in to comment.