aboutsummaryrefslogblamecommitdiffstats
path: root/ui/app/pages/mobile-sync/index.js
blob: 0938ad103107ceda7c9dc562a4e73c206c87abcc (plain) (tree)
1
2
3
4
5
6
7
8
9
10






                                          


                                                                                  


                                          

                                                              



















































































































































































































































































































































































                                                                                                                                             
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('../../store/actions')
const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
const actions = require('../../store/actions')

const qrCode = require('qrcode-generator')

import Button from '../../components/ui/button'
import LoadingScreen from '../../components/ui/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)