aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/components')
-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/send/send-content/send-gas-row/send-gas-row.container.js4
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js1
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-container.test.js8
-rw-r--r--ui/app/components/send/send.utils.js6
-rw-r--r--ui/app/components/send/tests/send-utils.test.js3
-rw-r--r--ui/app/components/shapeshift-form.js2
10 files changed, 443 insertions, 10 deletions
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/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
index 50cb47178..a187d61a2 100644
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
@@ -26,7 +26,7 @@ import {
} from '../../../../ducks/gas.duck'
import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js'
import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../actions'
-import { getAdvancedInlineGasShown, getCurrentEthBalance } from '../../../../selectors'
+import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../selectors'
import SendGasRow from './send-gas-row.component'
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow)
@@ -42,7 +42,7 @@ function mapStateToProps (state) {
const balance = getCurrentEthBalance(state)
const insufficientBalance = !isBalanceSufficient({
- amount: getSendAmount(state),
+ amount: getSelectedToken(state) ? '0x0' : getSendAmount(state),
gasTotal,
balance,
conversionRate,
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
index 723c406f7..12e78657b 100644
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
@@ -35,6 +35,7 @@ proxyquire('../send-gas-row.container.js', {
'../../../../selectors': {
getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`,
getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`,
+ getSelectedToken: () => false,
},
'../../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`,
diff --git a/ui/app/components/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
index cf4c893ee..daefa5103 100644
--- a/ui/app/components/send/send-footer/tests/send-footer-container.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
@@ -14,7 +14,9 @@ const actionSpies = {
}
const utilsStubs = {
addressIsNew: sinon.stub().returns(true),
- constructTxParams: sinon.stub().returns('mockConstructedTxParams'),
+ constructTxParams: sinon.stub().returns({
+ value: 'mockAmount',
+ }),
constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'),
}
@@ -115,7 +117,7 @@ describe('send-footer container', () => {
)
assert.deepEqual(
actionSpies.signTokenTx.getCall(0).args,
- [ '0xabc', 'mockTo', 'mockAmount', 'mockConstructedTxParams' ]
+ [ '0xabc', 'mockTo', 'mockAmount', { value: 'mockAmount' } ]
)
})
@@ -143,7 +145,7 @@ describe('send-footer container', () => {
)
assert.deepEqual(
actionSpies.signTx.getCall(0).args,
- [ 'mockConstructedTxParams' ]
+ [ { value: 'mockAmount' } ]
)
})
})
diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js
index b2ac41e9c..d78b7736f 100644
--- a/ui/app/components/send/send.utils.js
+++ b/ui/app/components/send/send.utils.js
@@ -234,10 +234,14 @@ async function estimateGas ({
if (to) {
paramsForGasEstimate.to = to
}
+
+ if (!value || value === '0') {
+ paramsForGasEstimate.value = '0xff'
+ }
}
// if not, fall back to block gasLimit
- paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, {
+ paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit || '0x5208', 0.95, {
multiplicandBase: 16,
multiplierBase: 10,
roundDown: '0',
diff --git a/ui/app/components/send/tests/send-utils.test.js b/ui/app/components/send/tests/send-utils.test.js
index f31e1221b..48fa09392 100644
--- a/ui/app/components/send/tests/send-utils.test.js
+++ b/ui/app/components/send/tests/send-utils.test.js
@@ -316,6 +316,7 @@ describe('send utils', () => {
from: 'mockAddress',
gas: '0x64x0.95',
to: '0xisContract',
+ value: '0xff',
}
beforeEach(() => {
@@ -373,7 +374,7 @@ describe('send utils', () => {
assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual(
baseMockParams.estimateGasMethod.getCall(0).args[0],
- { gasPrice: undefined, value: undefined, data, from: baseExpectedCall.from, gas: baseExpectedCall.gas},
+ { gasPrice: undefined, value: '0xff', data, from: baseExpectedCall.from, gas: baseExpectedCall.gas},
)
assert.equal(result, '0xabc16')
})
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')