aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Finlay <542863+danfinlay@users.noreply.github.com>2019-02-26 03:10:13 +0800
committerWhymarrh Whitby <whymarrh.whitby@gmail.com>2019-02-26 03:10:13 +0800
commitf507f2a92710285679123c9429a37c9e330c7cac (patch)
tree30225a55e9d8fc273304f4e2548413a08f008aef
parentfdc7eb211340b3af035a7f7c023155a8f1b1675d (diff)
downloadtangerine-wallet-browser-f507f2a92710285679123c9429a37c9e330c7cac.tar
tangerine-wallet-browser-f507f2a92710285679123c9429a37c9e330c7cac.tar.gz
tangerine-wallet-browser-f507f2a92710285679123c9429a37c9e330c7cac.tar.bz2
tangerine-wallet-browser-f507f2a92710285679123c9429a37c9e330c7cac.tar.lz
tangerine-wallet-browser-f507f2a92710285679123c9429a37c9e330c7cac.tar.xz
tangerine-wallet-browser-f507f2a92710285679123c9429a37c9e330c7cac.tar.zst
tangerine-wallet-browser-f507f2a92710285679123c9429a37c9e330c7cac.zip
Feature Flag + Mobile Sync (#5955)
-rw-r--r--app/_locales/en/messages.json27
-rw-r--r--app/scripts/controllers/preferences.js13
-rw-r--r--app/scripts/metamask-controller.js58
-rw-r--r--docs/README.md1
-rw-r--r--docs/secret-preferences.md10
-rw-r--r--gulpfile.js3
-rw-r--r--mascara/src/app/buy-ether-widget/index.js2
-rw-r--r--mascara/src/app/shapeshift-form/index.js2
-rw-r--r--package-lock.json267
-rw-r--r--package.json3
-rw-r--r--ui/app/actions.js16
-rw-r--r--ui/app/app.js3
-rw-r--r--ui/app/components/pages/mobile-sync/index.js387
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.component.js38
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.container.js2
-rw-r--r--ui/app/components/qr-code.js2
-rw-r--r--ui/app/components/shapeshift-form.js2
-rw-r--r--ui/app/routes.js2
18 files changed, 760 insertions, 78 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index ee6437116..2bfe0ef07 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -794,6 +794,12 @@
"minutesShorthand": {
"message": "Min"
},
+ "mobileSyncTitle": {
+ "message": "Sync accounts with mobile"
+ },
+ "mobileSyncText": {
+ "message": "Please enter your password to confirm it's you!"
+ },
"myAccounts": {
"message": "My Accounts"
},
@@ -1333,6 +1339,27 @@
"symbolBetweenZeroTwelve": {
"message": "Symbol must be between 0 and 12 characters."
},
+ "syncWithMobile": {
+ "message": "Sync with mobile"
+ },
+ "syncWithMobileTitle": {
+ "message": "Sync with mobile"
+ },
+ "syncWithMobileDesc": {
+ "message": "You can sync your accounts and information with your mobile device. Open the MetaMask mobile app, go to \"Settings\" and tap on \"Sync from Browser Extension\""
+ },
+ "syncWithMobileDescNewUsers": {
+ "message": "If you just open the MetaMask Mobile app for the first time, just follow the steps in your phone."
+ },
+ "syncWithMobileScanThisCode": {
+ "message": "Scan this code with your MetaMask mobile app"
+ },
+ "syncWithMobileBeCareful": {
+ "message": "Make sure nobody else is looking at your screen when you scan this code"
+ },
+ "syncWithMobileComplete": {
+ "message": "Your data has been synced succesfully. Enjoy the MetaMask mobile app!"
+ },
"takesTooLong": {
"message": "Taking too long?"
},
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 565f4f292..584b6bc51 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -18,7 +18,9 @@ class PreferencesController {
* @property {object} store.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
- * user wishes to see that feature
+ * user wishes to see that feature.
+ *
+ * Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior.
* @property {object} store.knownMethodData Contains all data methods known by the user
* @property {string} store.currentLocale The preferred language locale key
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
@@ -33,6 +35,11 @@ class PreferencesController {
tokens: [],
suggestedTokens: {},
useBlockie: false,
+
+ // WARNING: Do not use feature flags for security-sensitive things.
+ // Feature flag toggling is available in the global namespace
+ // for convenient testing of pre-release features, and should never
+ // perform sensitive operations.
featureFlags: {},
knownMethodData: {},
currentLocale: opts.initLangCode,
@@ -52,6 +59,10 @@ class PreferencesController {
this.store = new ObservableStore(initState)
this.openPopup = opts.openPopup
this._subscribeProviderType()
+
+ global.setPreference = (key, value) => {
+ return this.setFeatureFlag(key, value)
+ }
}
// PUBLIC METHODS
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 6555f179d..41c3e3642 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -56,6 +56,7 @@ const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util')
const sigUtil = require('eth-sig-util')
+
module.exports = class MetamaskController extends EventEmitter {
/**
@@ -410,6 +411,9 @@ module.exports = class MetamaskController extends EventEmitter {
checkHardwareStatus: nodeify(this.checkHardwareStatus, this),
unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this),
+ // mobile
+ fetchInfoToSync: nodeify(this.fetchInfoToSync, this),
+
// vault management
submitPassword: nodeify(this.submitPassword, this),
@@ -586,6 +590,60 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
+ /**
+ * Collects all the information that we want to share
+ * with the mobile client for syncing purposes
+ * @returns Promise<Object> Parts of the state that we want to syncx
+ */
+ async fetchInfoToSync () {
+ // Preferences
+ const {
+ accountTokens,
+ currentLocale,
+ frequentRpcList,
+ identities,
+ selectedAddress,
+ tokens,
+ } = this.preferencesController.store.getState()
+
+ const preferences = {
+ accountTokens,
+ currentLocale,
+ frequentRpcList,
+ identities,
+ selectedAddress,
+ tokens,
+ }
+
+ // Accounts
+ const hdKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0]
+ const hdAccounts = await hdKeyring.getAccounts()
+ const accounts = {
+ hd: hdAccounts.filter((item, pos) => (hdAccounts.indexOf(item) === pos)).map(address => ethUtil.toChecksumAddress(address)),
+ simpleKeyPair: [],
+ ledger: [],
+ trezor: [],
+ }
+
+ // transactions
+
+ let transactions = this.txController.store.getState().transactions
+ // delete tx for other accounts that we're not importing
+ transactions = transactions.filter(tx => {
+ const checksummedTxFrom = ethUtil.toChecksumAddress(tx.txParams.from)
+ return (
+ accounts.hd.includes(checksummedTxFrom)
+ )
+ })
+
+ return {
+ accounts,
+ preferences,
+ transactions,
+ network: this.networkController.store.getState(),
+ }
+ }
+
/*
* Submits the user's password and attempts to unlock the vault.
* Also synchronizes the preferencesController, to ensure its schema
diff --git a/docs/README.md b/docs/README.md
index c6e7dae6c..3c9bc0b4a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -18,3 +18,4 @@ To learn how to develop MetaMask-compatible applications, visit our [Developer D
- [How to manage notices that appear when the app starts up](./notices.md)
- [How to port MetaMask to a new platform](./porting_to_new_environment.md)
- [How to generate a visualization of this repository's development](./development-visualization.md)
+- [How to add a feature behind a secret feature flag](./secret-preferences.md)
diff --git a/docs/secret-preferences.md b/docs/secret-preferences.md
new file mode 100644
index 000000000..f9d01a503
--- /dev/null
+++ b/docs/secret-preferences.md
@@ -0,0 +1,10 @@
+# Secret Preferences
+
+Sometimes we want to test a feature in the wild that may not be ready for public consumption.
+
+One example is our "sync with mobile" feature, which didn't make sense to roll out before the mobile version was live.
+
+To enable features like this, first open the background console, and then you can use the global method `global.setPreference(key, value)`.
+
+For example, if the feature flag was a booelan was called `mobileSync`, you might type `setPreference('mobileSync', true)`.
+
diff --git a/gulpfile.js b/gulpfile.js
index 31888faac..c672a9eff 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -304,6 +304,7 @@ createTasksForBuildJsMascara({ taskPrefix: 'dev:mascara:js', devMode: true })
function createTasksForBuildJsUIDeps ({ dependenciesToBundle, filename }) {
const destinations = browserPlatforms.map(platform => `./dist/${platform}`)
+
const bundleTaskOpts = Object.assign({
buildSourceMaps: true,
sourceMapDir: '../sourcemaps',
@@ -512,6 +513,8 @@ function generateBundler (opts, performBundle) {
bundler.transform(envify({
METAMASK_DEBUG: opts.devMode,
NODE_ENV: opts.devMode ? 'development' : 'production',
+ PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '',
+ PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '',
}), {
global: true,
})
diff --git a/mascara/src/app/buy-ether-widget/index.js b/mascara/src/app/buy-ether-widget/index.js
index c60221a0a..c8530ba4c 100644
--- a/mascara/src/app/buy-ether-widget/index.js
+++ b/mascara/src/app/buy-ether-widget/index.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {connect} from 'react-redux'
-import {qrcode} from 'qrcode-npm'
+import {qrcode} from 'qrcode-generator'
import copyToClipboard from 'copy-to-clipboard'
import ShapeShiftForm from '../shapeshift-form'
import {buyEth, showAccountDetail} from '../../../../ui/app/actions'
diff --git a/mascara/src/app/shapeshift-form/index.js b/mascara/src/app/shapeshift-form/index.js
index 6f3307487..fe7f7ffcb 100644
--- a/mascara/src/app/shapeshift-form/index.js
+++ b/mascara/src/app/shapeshift-form/index.js
@@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
-import {qrcode} from 'qrcode-npm'
+import qrcode from 'qrcode-generator'
import {connect} from 'react-redux'
import {shapeShiftSubview, pairUpdate, buyWithShapeShift} from '../../../../ui/app/actions'
import {isValidAddress} from '../../../../ui/app/util'
diff --git a/package-lock.json b/package-lock.json
index 3d3d38b19..739a0f57e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2761,11 +2761,18 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz",
"integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==",
- "dev": true,
"requires": {
"es6-promisify": "^5.0.0"
}
},
+ "agentkeepalive": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz",
+ "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==",
+ "requires": {
+ "humanize-ms": "^1.2.1"
+ }
+ },
"airbnb-js-shims": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/airbnb-js-shims/-/airbnb-js-shims-1.4.1.tgz",
@@ -6424,8 +6431,7 @@
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
- "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
- "dev": true
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"component-inherit": {
"version": "0.0.3",
@@ -6572,6 +6578,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
+ "cookiejar": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
+ "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
+ },
"copy-concurrently": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
@@ -7504,8 +7515,7 @@
"data-uri-to-buffer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz",
- "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==",
- "dev": true
+ "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ=="
},
"date-format": {
"version": "1.2.0",
@@ -7613,8 +7623,7 @@
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
- "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
- "dev": true
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
},
"deepmerge": {
"version": "0.2.10",
@@ -7697,7 +7706,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz",
"integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=",
- "dev": true,
"requires": {
"ast-types": "0.x.x",
"escodegen": "1.x.x",
@@ -7707,8 +7715,7 @@
"esprima": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
- "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
- "dev": true
+ "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
}
}
},
@@ -9066,14 +9073,12 @@
"es6-promise": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
- "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
- "dev": true
+ "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
- "dev": true,
"requires": {
"es6-promise": "^4.0.3"
}
@@ -9140,7 +9145,6 @@
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz",
"integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==",
- "dev": true,
"requires": {
"esprima": "^3.1.3",
"estraverse": "^4.2.0",
@@ -9152,14 +9156,12 @@
"esprima": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
- "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
- "dev": true
+ "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
"optional": true
}
}
@@ -9760,7 +9762,7 @@
}
},
"eth-contract-metadata": {
- "version": "github:MetaMask/eth-contract-metadata#4d855fea9a5c899059134e03986be9d98e844270",
+ "version": "github:MetaMask/eth-contract-metadata#f6201b0c4aca8e98321b9b0b65744bc2fe8e35fa",
"from": "github:MetaMask/eth-contract-metadata#master"
},
"eth-ens-namehash": {
@@ -9812,7 +9814,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
@@ -9902,7 +9904,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
@@ -9971,7 +9973,7 @@
"dependencies": {
"babelify": {
"version": "7.3.0",
- "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
"integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
"requires": {
"babel-core": "^6.0.14",
@@ -10082,7 +10084,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
@@ -10190,7 +10192,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
@@ -10262,7 +10264,7 @@
"integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
@@ -10485,7 +10487,7 @@
}
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
@@ -10698,7 +10700,7 @@
"integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
},
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
@@ -12098,8 +12100,7 @@
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "dev": true
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"filename-regex": {
"version": "2.0.1",
@@ -12737,6 +12738,11 @@
"samsam": "1.x"
}
},
+ "formidable": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
+ "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg=="
+ },
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -12909,7 +12915,6 @@
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz",
"integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=",
- "dev": true,
"requires": {
"readable-stream": "1.1.x",
"xregexp": "2.0.0"
@@ -12918,14 +12923,12 @@
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
- "dev": true
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
@@ -12936,8 +12939,7 @@
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
- "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
- "dev": true
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
@@ -18906,7 +18908,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.2.tgz",
"integrity": "sha512-ZD325dMZOgerGqF/rF6vZXyFGTAay62svjQIT+X/oU2PtxYpFxvSkbsdi+oxIrsNxlZVd4y8wUDqkaExWTI/Cw==",
- "dev": true,
"requires": {
"data-uri-to-buffer": "1",
"debug": "2",
@@ -21564,7 +21565,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
"integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
- "dev": true,
"requires": {
"agent-base": "4",
"debug": "3.1.0"
@@ -21574,7 +21574,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -21641,7 +21640,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
- "dev": true,
"requires": {
"agent-base": "^4.1.0",
"debug": "^3.1.0"
@@ -21651,7 +21649,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -21663,6 +21660,14 @@
"resolved": "https://registry.npmjs.org/human-standard-token-abi/-/human-standard-token-abi-2.0.0.tgz",
"integrity": "sha512-m1f5DiIvqaNmpgphNqx2OziyTCj4Lvmmk28uMSxGWrOc9/lMpAKH8UcMPhvb13DMNZPzxn07WYFhxOGKuPLryg=="
},
+ "humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=",
+ "requires": {
+ "ms": "^2.0.0"
+ }
+ },
"humanize-url": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz",
@@ -22146,8 +22151,7 @@
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
- "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
- "dev": true
+ "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"ipaddr.js": {
"version": "1.5.2",
@@ -24391,7 +24395,6 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
- "dev": true,
"requires": {
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2"
@@ -24430,6 +24433,11 @@
"resolve": "^1.1.7"
}
},
+ "lil-uuid": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/lil-uuid/-/lil-uuid-0.1.1.tgz",
+ "integrity": "sha1-+e3PI/AOQr9D8PhD2Y2LU/M0HxY="
+ },
"livereload-js": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz",
@@ -26428,8 +26436,7 @@
"netmask": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
- "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=",
- "dev": true
+ "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU="
},
"next-tick": {
"version": "1.0.0",
@@ -28525,7 +28532,6 @@
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
"integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
- "dev": true,
"requires": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.4",
@@ -28538,8 +28544,7 @@
"wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
- "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
- "dev": true
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
}
}
},
@@ -28664,7 +28669,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-2.0.2.tgz",
"integrity": "sha512-cDNAN1Ehjbf5EHkNY5qnRhGPUCp6SnpyVof5fRzN800QV1Y2OkzbH9rmjZkbBRa8igof903yOnjIl6z0SlAhxA==",
- "dev": true,
"requires": {
"agent-base": "^4.2.0",
"debug": "^3.1.0",
@@ -28680,7 +28684,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -28691,7 +28694,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz",
"integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==",
- "dev": true,
"requires": {
"co": "^4.6.0",
"degenerator": "^1.0.4",
@@ -30958,8 +30960,7 @@
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
- "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
- "dev": true
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
},
"prepend-file": {
"version": "1.3.1",
@@ -31158,11 +31159,49 @@
"ipaddr.js": "1.5.2"
}
},
+ "proxy-agent": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-2.3.1.tgz",
+ "integrity": "sha512-CNKuhC1jVtm8KJYFTS2ZRO71VCBx3QSA92So/e6NrY6GoJonkx3Irnk4047EsCcswczwqAekRj3s8qLRGahSKg==",
+ "requires": {
+ "agent-base": "^4.2.0",
+ "debug": "^3.1.0",
+ "http-proxy-agent": "^2.1.0",
+ "https-proxy-agent": "^2.2.1",
+ "lru-cache": "^4.1.2",
+ "pac-proxy-agent": "^2.0.1",
+ "proxy-from-env": "^1.0.0",
+ "socks-proxy-agent": "^3.0.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ }
+ }
+ },
"proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
- "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
- "dev": true
+ "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4="
},
"proxyquire": {
"version": "2.0.1",
@@ -31203,8 +31242,7 @@
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
- "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
- "dev": true
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
},
"psl": {
"version": "1.1.29",
@@ -31224,6 +31262,17 @@
"randombytes": "^2.0.1"
}
},
+ "pubnub": {
+ "version": "4.21.7",
+ "resolved": "https://registry.npmjs.org/pubnub/-/pubnub-4.21.7.tgz",
+ "integrity": "sha512-TZ96GuY+gZIu9rJaqcO2cZ6tl4JPLruoUcN01sljm1CcDgzIZbOfcDSZp4NcZas4ECSqAAwo/izMMiImRRS4Yg==",
+ "requires": {
+ "agentkeepalive": "^3.5.2",
+ "lil-uuid": "^0.1.1",
+ "superagent": "^3.8.1",
+ "superagent-proxy": "^1.0.3"
+ }
+ },
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -31270,10 +31319,10 @@
"integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
"dev": true
},
- "qrcode-npm": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/qrcode-npm/-/qrcode-npm-0.0.3.tgz",
- "integrity": "sha1-d+5vvvqcDyn6CdTRUggHxqYEK5o="
+ "qrcode-generator": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.1.tgz",
+ "integrity": "sha512-KOdSAyFBPf0/5Z3mra4JfSbjrDlUn2J3YH8Rm33tRGbptxP4vhogLWysvkQp8mp5ix9u80Wfr4vxHXTeR9o0Ug=="
},
"qs": {
"version": "6.5.1",
@@ -33816,8 +33865,7 @@
"smart-buffer": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz",
- "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=",
- "dev": true
+ "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY="
},
"snapdragon": {
"version": "0.8.2",
@@ -34633,7 +34681,6 @@
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz",
"integrity": "sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=",
- "dev": true,
"requires": {
"ip": "^1.1.4",
"smart-buffer": "^1.0.13"
@@ -34643,7 +34690,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz",
"integrity": "sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==",
- "dev": true,
"requires": {
"agent-base": "^4.1.0",
"socks": "^1.1.10"
@@ -35986,6 +36032,89 @@
}
}
},
+ "superagent": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
+ "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
+ "requires": {
+ "component-emitter": "^1.2.0",
+ "cookiejar": "^2.1.0",
+ "debug": "^3.1.0",
+ "extend": "^3.0.0",
+ "form-data": "^2.3.1",
+ "formidable": "^1.2.0",
+ "methods": "^1.1.1",
+ "mime": "^1.4.1",
+ "qs": "^6.5.1",
+ "readable-stream": "^2.3.5"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "superagent-proxy": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-1.0.3.tgz",
+ "integrity": "sha512-79Ujg1lRL2ICfuHUdX+H2MjIw73kB7bXsIkxLwHURz3j0XUmEEEoJ+u/wq+mKwna21Uejsm2cGR3OESA00TIjA==",
+ "requires": {
+ "debug": "^3.1.0",
+ "proxy-agent": "2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ }
+ }
+ },
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
@@ -36536,8 +36665,7 @@
"thunkify": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz",
- "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=",
- "dev": true
+ "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0="
},
"tildify": {
"version": "1.2.0",
@@ -36945,7 +37073,6 @@
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
- "dev": true,
"requires": {
"prelude-ls": "~1.1.2"
}
@@ -38451,8 +38578,7 @@
"xregexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz",
- "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=",
- "dev": true
+ "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM="
},
"xtend": {
"version": "4.0.1",
@@ -38467,8 +38593,7 @@
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
- "dev": true
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yargs": {
"version": "6.6.0",
diff --git a/package.json b/package.json
index f5600d019..bd3a190e3 100644
--- a/package.json
+++ b/package.json
@@ -173,9 +173,10 @@
"promise-filter": "^1.1.0",
"promise-to-callback": "^1.0.0",
"prop-types": "^15.6.1",
+ "pubnub": "^4.21.5",
"pump": "^3.0.0",
"pumpify": "^1.3.4",
- "qrcode-npm": "0.0.3",
+ "qrcode-generator": "1.4.1",
"ramda": "^0.24.1",
"react": "^15.6.2",
"react-addons-css-transition-group": "^15.6.0",
diff --git a/ui/app/actions.js b/ui/app/actions.js
index f3e9d2b27..a9c3d2ccf 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -63,6 +63,7 @@ var actions = {
CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
+ fetchInfoToSync,
FORGOT_PASSWORD: 'FORGOT_PASSWORD',
forgotPassword: forgotPassword,
markPasswordForgotten,
@@ -635,6 +636,21 @@ function requestRevealSeedWords (password) {
}
}
+function fetchInfoToSync () {
+ return dispatch => {
+ log.debug(`background.fetchInfoToSync`)
+ return new Promise((resolve, reject) => {
+ background.fetchInfoToSync((err, result) => {
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+ resolve(result)
+ })
+ })
+ }
+}
+
function resetAccount () {
return dispatch => {
dispatch(actions.showLoadingIndication())
diff --git a/ui/app/app.js b/ui/app/app.js
index 9c44bd553..1001adc9a 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -25,6 +25,7 @@ import Lock from './components/pages/lock'
import UiMigrationAnnouncement from './components/ui-migration-annoucement'
const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
+const MobileSyncPage = require('./components/pages/mobile-sync')
const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
@@ -55,6 +56,7 @@ import {
UNLOCK_ROUTE,
SETTINGS_ROUTE,
REVEAL_SEED_ROUTE,
+ MOBILE_SYNC_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE,
@@ -90,6 +92,7 @@ class App extends Component {
<Initialized path={UNLOCK_ROUTE} component={UnlockPage} exact />
<Initialized path={RESTORE_VAULT_ROUTE} component={RestoreVaultPage} exact />
<Authenticated path={REVEAL_SEED_ROUTE} component={RevealSeedConfirmation} exact />
+ <Authenticated path={MOBILE_SYNC_ROUTE} component={MobileSyncPage} exact />
<Authenticated path={SETTINGS_ROUTE} component={Settings} />
<Authenticated path={NOTICE_ROUTE} component={NoticeScreen} exact />
<Authenticated path={`${CONFIRM_TRANSACTION_ROUTE}/:id?`} component={ConfirmTransaction} />
diff --git a/ui/app/components/pages/mobile-sync/index.js b/ui/app/components/pages/mobile-sync/index.js
new file mode 100644
index 000000000..22a69d092
--- /dev/null
+++ b/ui/app/components/pages/mobile-sync/index.js
@@ -0,0 +1,387 @@
+const { Component } = require('react')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const classnames = require('classnames')
+const PubNub = require('pubnub')
+
+const { requestRevealSeedWords, fetchInfoToSync } = require('../../../actions')
+const { DEFAULT_ROUTE } = require('../../../routes')
+const actions = require('../../../actions')
+
+const qrCode = require('qrcode-generator')
+
+import Button from '../../button'
+import LoadingScreen from '../../loading-screen'
+
+const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
+const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
+
+class MobileSyncPage extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ selectedAddress: PropTypes.string,
+ displayWarning: PropTypes.func,
+ fetchInfoToSync: PropTypes.func,
+ requestRevealSeedWords: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ screen: PASSWORD_PROMPT_SCREEN,
+ password: '',
+ seedWords: null,
+ error: null,
+ syncing: false,
+ completed: false,
+ }
+
+ this.syncing = false
+ }
+
+ componentDidMount () {
+ const passwordBox = document.getElementById('password-box')
+ if (passwordBox) {
+ passwordBox.focus()
+ }
+ }
+
+ handleSubmit (event) {
+ event.preventDefault()
+ this.setState({ seedWords: null, error: null })
+ this.props.requestRevealSeedWords(this.state.password)
+ .then(seedWords => {
+ this.generateCipherKeyAndChannelName()
+ this.setState({ seedWords, screen: REVEAL_SEED_SCREEN })
+ this.initWebsockets()
+ })
+ .catch(error => this.setState({ error: error.message }))
+ }
+
+ generateCipherKeyAndChannelName () {
+ this.cipherKey = `${this.props.selectedAddress.substr(-4)}-${PubNub.generateUUID()}`
+ this.channelName = `mm-${PubNub.generateUUID()}`
+ }
+
+ initWebsockets () {
+ this.pubnub = new PubNub({
+ subscribeKey: process.env.PUBNUB_SUB_KEY,
+ publishKey: process.env.PUBNUB_PUB_KEY,
+ cipherKey: this.cipherKey,
+ ssl: true,
+ })
+
+ this.pubnubListener = this.pubnub.addListener({
+ message: (data) => {
+ const {channel, message} = data
+ // handle message
+ if (channel !== this.channelName || !message) {
+ return false
+ }
+
+ if (message.event === 'start-sync') {
+ this.startSyncing()
+ } else if (message.event === 'end-sync') {
+ this.disconnectWebsockets()
+ this.setState({syncing: false, completed: true})
+ }
+ },
+ })
+
+ this.pubnub.subscribe({
+ channels: [this.channelName],
+ withPresence: false,
+ })
+
+ }
+
+ disconnectWebsockets () {
+ if (this.pubnub && this.pubnubListener) {
+ this.pubnub.disconnect(this.pubnubListener)
+ }
+ }
+
+ // Calculating a PubNub Message Payload Size.
+ calculatePayloadSize (channel, message) {
+ return encodeURIComponent(
+ channel + JSON.stringify(message)
+ ).length + 100
+ }
+
+ chunkString (str, size) {
+ const numChunks = Math.ceil(str.length / size)
+ const chunks = new Array(numChunks)
+ for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
+ chunks[i] = str.substr(o, size)
+ }
+ return chunks
+ }
+
+ notifyError (errorMsg) {
+ return new Promise((resolve, reject) => {
+ this.pubnub.publish(
+ {
+ message: {
+ event: 'error-sync',
+ data: errorMsg,
+ },
+ channel: this.channelName,
+ sendByPost: false, // true to send via post
+ storeInHistory: false,
+ },
+ (status, response) => {
+ if (!status.error) {
+ resolve()
+ } else {
+ reject(response)
+ }
+ })
+ })
+ }
+
+ async startSyncing () {
+ if (this.syncing) return false
+ this.syncing = true
+ this.setState({syncing: true})
+
+ const { accounts, network, preferences, transactions } = await this.props.fetchInfoToSync()
+
+ const allDataStr = JSON.stringify({
+ accounts,
+ network,
+ preferences,
+ transactions,
+ udata: {
+ pwd: this.state.password,
+ seed: this.state.seedWords,
+ },
+ })
+
+ const chunks = this.chunkString(allDataStr, 17000)
+ const totalChunks = chunks.length
+ try {
+ for (let i = 0; i < totalChunks; i++) {
+ await this.sendMessage(chunks[i], i + 1, totalChunks)
+ }
+ } catch (e) {
+ this.props.displayWarning('Sync failed :(')
+ this.setState({syncing: false})
+ this.syncing = false
+ this.notifyError(e.toString())
+ }
+ }
+
+ sendMessage (data, pkg, count) {
+ return new Promise((resolve, reject) => {
+ this.pubnub.publish(
+ {
+ message: {
+ event: 'syncing-data',
+ data,
+ totalPkg: count,
+ currentPkg: pkg,
+ },
+ channel: this.channelName,
+ sendByPost: false, // true to send via post
+ storeInHistory: false,
+ },
+ (status, response) => {
+ if (!status.error) {
+ resolve()
+ } else {
+ reject(response)
+ }
+ }
+ )
+ })
+ }
+
+
+ componentWillUnmount () {
+ this.disconnectWebsockets()
+ }
+
+ renderWarning (text) {
+ return (
+ h('.page-container__warning-container', [
+ h('.page-container__warning-message', [
+ h('div', [text]),
+ ]),
+ ])
+ )
+ }
+
+ renderContent () {
+ const { t } = this.context
+
+ if (this.state.syncing) {
+ return h(LoadingScreen, {loadingMessage: 'Sync in progress'})
+ }
+
+ if (this.state.completed) {
+ return h('div.reveal-seed__content', {},
+ h('label.reveal-seed__label', {
+ style: {
+ width: '100%',
+ textAlign: 'center',
+ },
+ }, t('syncWithMobileComplete')),
+ )
+ }
+
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? h('div', {}, [
+ this.renderWarning(this.context.t('mobileSyncText')),
+ h('.reveal-seed__content', [
+ this.renderPasswordPromptContent(),
+ ]),
+ ])
+ : h('div', {}, [
+ this.renderWarning(this.context.t('syncWithMobileBeCareful')),
+ h('.reveal-seed__content', [ this.renderRevealSeedContent() ]),
+ ])
+ }
+
+ renderPasswordPromptContent () {
+ const { t } = this.context
+
+ return (
+ h('form', {
+ onSubmit: event => this.handleSubmit(event),
+ }, [
+ h('label.input-label', {
+ htmlFor: 'password-box',
+ }, t('enterPasswordContinue')),
+ h('.input-group', [
+ h('input.form-control', {
+ type: 'password',
+ placeholder: t('password'),
+ id: 'password-box',
+ value: this.state.password,
+ onChange: event => this.setState({ password: event.target.value }),
+ className: classnames({ 'form-control--error': this.state.error }),
+ }),
+ ]),
+ this.state.error && h('.reveal-seed__error', this.state.error),
+ ])
+ )
+ }
+
+ renderRevealSeedContent () {
+
+ const qrImage = qrCode(0, 'M')
+ qrImage.addData(`metamask-sync:${this.channelName}|@|${this.cipherKey}`)
+ qrImage.make()
+
+ const { t } = this.context
+ return (
+ h('div', [
+ h('label.reveal-seed__label', {
+ style: {
+ width: '100%',
+ textAlign: 'center',
+ },
+ }, t('syncWithMobileScanThisCode')),
+ h('.div.qr-wrapper', {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ dangerouslySetInnerHTML: {
+ __html: qrImage.createTableTag(4),
+ },
+ }),
+ ])
+ )
+ }
+
+ renderFooter () {
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? this.renderPasswordPromptFooter()
+ : this.renderRevealSeedFooter()
+ }
+
+ renderPasswordPromptFooter () {
+ return (
+ h('div.new-account-import-form__buttons', {style: {padding: 30}}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('cancel')),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: event => this.handleSubmit(event),
+ disabled: this.state.password === '',
+ }, this.context.t('next')),
+ ])
+ )
+ }
+
+ renderRevealSeedFooter () {
+ return (
+ h('.page-container__footer', {style: {padding: 30}}, [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('close')),
+ ])
+ )
+ }
+
+ render () {
+ return (
+ h('.page-container', [
+ h('.page-container__header', [
+ h('.page-container__title', this.context.t('syncWithMobileTitle')),
+ this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDesc')) : null,
+ this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDescNewUsers')) : null,
+ ]),
+ h('.page-container__content', [
+ this.renderContent(),
+ ]),
+ this.renderFooter(),
+ ])
+ )
+ }
+}
+
+MobileSyncPage.propTypes = {
+ requestRevealSeedWords: PropTypes.func,
+ fetchInfoToSync: PropTypes.func,
+ history: PropTypes.object,
+}
+
+MobileSyncPage.contextTypes = {
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
+ fetchInfoToSync: () => dispatch(fetchInfoToSync()),
+ displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
+ }
+
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { selectedAddress },
+ } = state
+
+ return {
+ selectedAddress,
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(MobileSyncPage)
diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
index 1c02b2507..661cbd1dc 100644
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
+++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
@@ -5,7 +5,7 @@ import validUrl from 'valid-url'
import { exportAsFile } from '../../../../util'
import SimpleDropdown from '../../../dropdowns/simple-dropdown'
import ToggleButton from 'react-toggle-button'
-import { REVEAL_SEED_ROUTE } from '../../../../routes'
+import { REVEAL_SEED_ROUTE, MOBILE_SYNC_ROUTE } from '../../../../routes'
import locales from '../../../../../../app/_locales/index.json'
import TextField from '../../../text-field'
import Button from '../../../button'
@@ -46,6 +46,7 @@ export default class SettingsTab extends PureComponent {
delRpcTarget: PropTypes.func,
displayWarning: PropTypes.func,
revealSeedConfirmation: PropTypes.func,
+ setFeatureFlagToBeta: PropTypes.func,
showClearApprovalModal: PropTypes.func,
showResetAccountConfirmationModal: PropTypes.func,
warning: PropTypes.string,
@@ -61,6 +62,7 @@ export default class SettingsTab extends PureComponent {
setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func,
setAdvancedInlineGasFeatureFlag: PropTypes.func,
advancedInlineGas: PropTypes.bool,
+ mobileSync: PropTypes.bool,
}
state = {
@@ -338,6 +340,39 @@ export default class SettingsTab extends PureComponent {
)
}
+
+ renderMobileSync () {
+ const { t } = this.context
+ const { history, mobileSync } = this.props
+
+ if (!mobileSync) {
+ return
+ }
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('syncWithMobile') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="primary"
+ large
+ onClick={event => {
+ event.preventDefault()
+ history.push(MOBILE_SYNC_ROUTE)
+ }}
+ >
+ { t('syncWithMobile') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+
renderResetAccount () {
const { t } = this.context
const { showResetAccountConfirmationModal } = this.props
@@ -538,6 +573,7 @@ export default class SettingsTab extends PureComponent {
{ this.renderHexDataOptIn() }
{ this.renderAdvancedGasInputInline() }
{ this.renderBlockieOptIn() }
+ { this.renderMobileSync() }
</div>
)
}
diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
index 49da0db12..72458ba33 100644
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
+++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
@@ -26,6 +26,7 @@ const mapStateToProps = state => {
sendHexData,
privacyMode,
advancedInlineGas,
+ mobileSync,
} = {},
provider = {},
currentLocale,
@@ -44,6 +45,7 @@ const mapStateToProps = state => {
privacyMode,
provider,
useNativeCurrencyAsPrimaryCurrency,
+ mobileSync,
}
}
diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js
index 1c07e244c..312815891 100644
--- a/ui/app/components/qr-code.js
+++ b/ui/app/components/qr-code.js
@@ -1,6 +1,6 @@
const Component = require('react').Component
const h = require('react-hyperscript')
-const qrCode = require('qrcode-npm').qrcode
+const qrCode = require('qrcode-generator')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const { isHexPrefixed } = require('ethereumjs-util')
diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js
index a842bcc8b..b22c01e8f 100644
--- a/ui/app/components/shapeshift-form.js
+++ b/ui/app/components/shapeshift-form.js
@@ -4,7 +4,7 @@ const PropTypes = require('prop-types')
const Component = require('react').Component
const connect = require('react-redux').connect
const classnames = require('classnames')
-const { qrcode } = require('qrcode-npm')
+const qrcode = require('qrcode-generator')
const { shapeShiftSubview, pairUpdate, buyWithShapeShift } = require('../actions')
const { isValidAddress } = require('../util')
const SimpleDropdown = require('./dropdowns/simple-dropdown')
diff --git a/ui/app/routes.js b/ui/app/routes.js
index fcf3d3e68..2f4863547 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -4,6 +4,7 @@ const LOCK_ROUTE = '/lock'
const SETTINGS_ROUTE = '/settings'
const INFO_ROUTE = '/settings/info'
const REVEAL_SEED_ROUTE = '/seed'
+const MOBILE_SYNC_ROUTE = '/mobile-sync'
const CONFIRM_SEED_ROUTE = '/confirm-seed'
const RESTORE_VAULT_ROUTE = '/restore-vault'
const ADD_TOKEN_ROUTE = '/add-token'
@@ -43,6 +44,7 @@ module.exports = {
SETTINGS_ROUTE,
INFO_ROUTE,
REVEAL_SEED_ROUTE,
+ MOBILE_SYNC_ROUTE,
CONFIRM_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,