aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorDan Finlay <dan@danfinlay.com>2016-04-14 06:28:44 +0800
committerDan Finlay <dan@danfinlay.com>2016-04-14 06:28:44 +0800
commitd814a45dffa6a872f6e336cad33ca41ffb102887 (patch)
treed8cdd0c4b8c6559efaf6846b24f0d6440f3c94f5 /ui
parent9f1438b85b3dac8f1dd98f7bd6e101035cfce0a5 (diff)
downloadtangerine-wallet-browser-d814a45dffa6a872f6e336cad33ca41ffb102887.tar
tangerine-wallet-browser-d814a45dffa6a872f6e336cad33ca41ffb102887.tar.gz
tangerine-wallet-browser-d814a45dffa6a872f6e336cad33ca41ffb102887.tar.bz2
tangerine-wallet-browser-d814a45dffa6a872f6e336cad33ca41ffb102887.tar.lz
tangerine-wallet-browser-d814a45dffa6a872f6e336cad33ca41ffb102887.tar.xz
tangerine-wallet-browser-d814a45dffa6a872f6e336cad33ca41ffb102887.tar.zst
tangerine-wallet-browser-d814a45dffa6a872f6e336cad33ca41ffb102887.zip
Moved UI into repo with its own dependency stack
Diffstat (limited to 'ui')
-rw-r--r--ui/.gitignore66
-rw-r--r--ui/README.md14
-rw-r--r--ui/app/account-detail.js154
-rw-r--r--ui/app/accounts.js116
-rw-r--r--ui/app/actions.js418
-rw-r--r--ui/app/app.js242
-rw-r--r--ui/app/components/account-panel.js93
-rw-r--r--ui/app/components/mascot.js65
-rw-r--r--ui/app/conf-tx.js140
-rw-r--r--ui/app/config.js103
-rw-r--r--ui/app/css/debug.css21
-rw-r--r--ui/app/css/fonts.css2
-rw-r--r--ui/app/css/index.css489
-rw-r--r--ui/app/css/lib.css143
-rw-r--r--ui/app/css/reset.css48
-rw-r--r--ui/app/css/transitions.css47
-rw-r--r--ui/app/first-time/create-vault-complete.js57
-rw-r--r--ui/app/first-time/create-vault.js123
-rw-r--r--ui/app/first-time/init-menu.js123
-rw-r--r--ui/app/first-time/restore-vault.js116
-rw-r--r--ui/app/img/identicon-tardigrade.pngbin0 -> 141119 bytes
-rw-r--r--ui/app/img/identicon-walrus.pngbin0 -> 388973 bytes
-rw-r--r--ui/app/info.js90
-rw-r--r--ui/app/loading.js51
-rw-r--r--ui/app/reducers.js41
-rw-r--r--ui/app/reducers/app.js281
-rw-r--r--ui/app/reducers/identities.js18
-rw-r--r--ui/app/reducers/metamask.js73
-rw-r--r--ui/app/root.js24
-rw-r--r--ui/app/send.js139
-rw-r--r--ui/app/settings.js69
-rw-r--r--ui/app/store.js19
-rw-r--r--ui/app/template.js31
-rw-r--r--ui/app/unlock.js101
-rw-r--r--ui/app/util.js102
-rw-r--r--ui/css.js26
-rw-r--r--ui/design/1st_time_use.pngbin0 -> 937556 bytes
-rw-r--r--ui/design/metamask_wfs_jan_13.pdfbin0 -> 452413 bytes
-rw-r--r--ui/design/metamask_wfs_jan_13.pngbin0 -> 419066 bytes
-rw-r--r--ui/design/metamask_wfs_jan_18.pdfbin0 -> 612778 bytes
-rw-r--r--ui/example.js123
-rw-r--r--ui/index.html38
-rw-r--r--ui/index.js55
-rw-r--r--ui/package.json58
-rw-r--r--ui/test/setup.js8
-rw-r--r--ui/test/unit/actions/config_test.js43
-rw-r--r--ui/test/unit/actions/restore_vault_test.js54
-rw-r--r--ui/test/unit/actions/set_selected_account_test.js28
-rw-r--r--ui/test/unit/actions/tx_test.js168
-rw-r--r--ui/test/unit/actions/view_info_test.js23
-rw-r--r--ui/test/unit/actions/warning_test.js24
-rw-r--r--ui/test/unit/util_test.js102
52 files changed, 4369 insertions, 0 deletions
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 000000000..c6b1254b5
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,66 @@
+
+# Created by https://www.gitignore.io/api/osx,node
+
+### OSX ###
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
+node_modules
+
+# Optional npm cache directory
+.npm
+
+# Optional REPL history
+.node_repl_history
+
diff --git a/ui/README.md b/ui/README.md
new file mode 100644
index 000000000..fdac37f02
--- /dev/null
+++ b/ui/README.md
@@ -0,0 +1,14 @@
+## Installation
+
+```
+git clone git@github.com:MetaMask/metamask-ui.git
+cd metamask-ui
+npm install
+grunt dev
+```
+
+## Testing
+
+Requires `mocha` installed. Run `npm install -g mocha`.
+
+You can either run the test suite once with `npm test`, or you can reload on file changes, by running `mocha watch test/**/**`.
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js
new file mode 100644
index 000000000..6ed23d482
--- /dev/null
+++ b/ui/app/account-detail.js
@@ -0,0 +1,154 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const copyToClipboard = require('copy-to-clipboard')
+const actions = require('./actions')
+const AccountPanel = require('./components/account-panel')
+
+module.exports = connect(mapStateToProps)(AccountDetailScreen)
+
+function mapStateToProps(state) {
+ var accountDetail = state.appState.accountDetail
+ return {
+ identities: state.metamask.identities,
+ accounts: state.metamask.accounts,
+ address: state.appState.currentView.context,
+ accountDetail: accountDetail,
+ }
+}
+
+inherits(AccountDetailScreen, Component)
+function AccountDetailScreen() {
+ Component.call(this)
+}
+
+
+AccountDetailScreen.prototype.render = function() {
+ var state = this.props
+ var identity = state.identities[state.address]
+ var account = state.accounts[state.address]
+ var accountDetail = state.accountDetail
+
+ return (
+
+ h('.account-detail-section.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.navigateToAccounts.bind(this),
+ }),
+ h('h2.page-subtitle', 'Account Detail'),
+ ]),
+
+ // account summary, with embedded action buttons
+ h(AccountPanel, {
+ showFullAddress: true,
+ identity: identity,
+ account: account,
+ }, [
+ h('.flex-row.flex-space-around', [
+ // h('button', 'GET ETH'), DISABLED UNTIL WORKING
+
+ h('button', {
+ onClick: () => {
+ copyToClipboard(identity.address)
+ },
+ }, 'COPY ADDR'),
+
+ h('button', {
+ onClick: () => {
+ this.props.dispatch(actions.showSendPage())
+ },
+ }, 'SEND'),
+
+ h('button', {
+ onClick: () => {
+ this.requestAccountExport(identity.address)
+ },
+ }, 'EXPORT'),
+ ]),
+ ]),
+
+ this.exportedAccount(accountDetail),
+
+ // transaction table
+ /*
+ h('section.flex-column', [
+ h('span', 'your transaction history will go here.'),
+ ]),
+ */
+ ])
+ )
+}
+
+AccountDetailScreen.prototype.navigateToAccounts = function(event){
+ event.stopPropagation()
+ this.props.dispatch(actions.showAccountsPage())
+}
+
+AccountDetailScreen.prototype.exportAccount = function(address) {
+ this.props.dispatch(actions.exportAccount(address))
+}
+
+AccountDetailScreen.prototype.requestAccountExport = function() {
+ this.props.dispatch(actions.requestExportAccount())
+}
+
+AccountDetailScreen.prototype.exportedAccount = function(accountDetail) {
+ if (!accountDetail) return
+ var accountExport = accountDetail.accountExport
+
+ var notExporting = accountExport === 'none'
+ var exportRequested = accountExport === 'requested'
+ var accountExported = accountExport === 'completed'
+
+ if (notExporting) return
+
+ if (exportRequested) {
+ var warning = `Exporting your private key is very dangerous,
+ and you should only do it if you know what you're doing.`
+ var confirmation = `If you're absolutely sure, type "I understand" below and
+ hit Enter.`
+ return h('div', {}, [
+ h('p.error', warning),
+ h('p', confirmation),
+ h('input#exportAccount', {
+ onKeyPress: this.onExportKeyPress.bind(this),
+ })
+ ])
+ }
+
+ if (accountExported) {
+ return h('div.privateKey', {
+
+ }, [
+ h('label', 'Your private key (click to copy):'),
+ h('p.error.cursor-pointer', {
+ style: {
+ textOverflow: 'ellipsis',
+ overflow: 'hidden',
+ webkitUserSelect: 'text',
+ width: '100%',
+ },
+ onClick: function(event) {
+ copyToClipboard(accountDetail.privateKey)
+ }
+ }, accountDetail.privateKey),
+ ])
+ }
+}
+
+AccountDetailScreen.prototype.onExportKeyPress = function(event) {
+ if (event.key !== 'Enter') return
+ event.preventDefault()
+
+ var input = document.getElementById('exportAccount')
+ if (input.value === 'I understand') {
+ this.props.dispatch(actions.exportAccount(this.props.address))
+ } else {
+ input.value = ''
+ input.placeholder = 'Please retype "I understand" exactly.'
+ }
+}
diff --git a/ui/app/accounts.js b/ui/app/accounts.js
new file mode 100644
index 000000000..d35e80678
--- /dev/null
+++ b/ui/app/accounts.js
@@ -0,0 +1,116 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const extend = require('xtend')
+const actions = require('./actions')
+const AccountPanel = require('./components/account-panel')
+const valuesFor = require('./util').valuesFor
+
+module.exports = connect(mapStateToProps)(AccountsScreen)
+
+
+function mapStateToProps(state) {
+ return {
+ accounts: state.metamask.accounts,
+ identities: state.metamask.identities,
+ unconfTxs: state.metamask.unconfTxs,
+ selectedAddress: state.metamask.selectedAddress,
+ currentDomain: state.appState.currentDomain,
+ }
+}
+
+inherits(AccountsScreen, Component)
+function AccountsScreen() {
+ Component.call(this)
+}
+
+
+AccountsScreen.prototype.render = function() {
+ var state = this.props
+ var identityList = valuesFor(state.identities)
+ var unconfTxList = valuesFor(state.unconfTxs)
+ var actions = {
+ onSelect: this.onSelect.bind(this),
+ onShowDetail: this.onShowDetail.bind(this),
+ }
+ return (
+
+ h('.accounts-section.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-column.flex-center', [
+ h('h2.page-subtitle', 'Accounts'),
+ ]),
+
+ // current domain
+ /* AUDIT
+ * Temporarily removed
+ * since accounts are currently injected
+ * regardless of the current domain.
+ */
+ h('.current-domain-panel.flex-center.font-small', [
+ h('spam', 'Selected address is visible to all sites you visit.'),
+ // h('span', state.currentDomain),
+ ]),
+
+ // identity selection
+ h('section.identity-section.flex-column', {
+ style: {
+ maxHeight: '290px',
+ overflowY: 'auto',
+ overflowX: 'hidden',
+ }
+ },
+ identityList.map(renderAccountPanel)
+ ),
+
+ unconfTxList.length ? (
+
+ h('.unconftx-link.flex-row.flex-center', {
+ onClick: this.navigateToConfTx.bind(this),
+ }, [
+ h('span', 'Unconfirmed Txs'),
+ h('i.fa.fa-arrow-right.fa-lg'),
+ ])
+
+ ) : (
+ null
+ ),
+
+
+ ])
+
+ )
+
+ function renderAccountPanel(identity){
+ var mayBeFauceting = identity.mayBeFauceting
+ var isSelected = state.selectedAddress === identity.address
+ var account = state.accounts[identity.address]
+ var isFauceting = mayBeFauceting && account.balance === '0x0'
+ var componentState = extend(actions, {
+ identity: identity,
+ account: account,
+ isSelected: isSelected,
+ isFauceting: isFauceting,
+ })
+ return h(AccountPanel, componentState)
+ }
+}
+
+AccountsScreen.prototype.navigateToConfTx = function(){
+ event.stopPropagation()
+ this.props.dispatch(actions.showConfTxPage())
+}
+
+AccountsScreen.prototype.onSelect = function(address, event){
+ event.stopPropagation()
+ // if already selected, deselect
+ if (this.props.selectedAddress === address) address = null
+ this.props.dispatch(actions.setSelectedAddress(address))
+}
+
+AccountsScreen.prototype.onShowDetail = function(address, event){
+ event.stopPropagation()
+ this.props.dispatch(actions.showAccountDetail(address))
+}
diff --git a/ui/app/actions.js b/ui/app/actions.js
new file mode 100644
index 000000000..339c28be3
--- /dev/null
+++ b/ui/app/actions.js
@@ -0,0 +1,418 @@
+var actions = {
+ // remote state
+ UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
+ updateMetamaskState: updateMetamaskState,
+ // intialize screen
+ CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
+ SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
+ SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
+ SHOW_INIT_MENU: 'SHOW_INIT_MENU',
+ SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
+ SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
+ RECOVER_FROM_SEED: 'RECOVER_FROM_SEED',
+ CLEAR_SEED_WORD_CACHE: 'CLEAR_SEED_WORD_CACHE',
+ clearSeedWordCache: clearSeedWordCache,
+ recoverFromSeed: recoverFromSeed,
+ unlockMetamask: unlockMetamask,
+ unlockFailed: unlockFailed,
+ showCreateVault: showCreateVault,
+ showRestoreVault: showRestoreVault,
+ showInitializeMenu: showInitializeMenu,
+ createNewVault: createNewVault,
+ createNewVaultInProgress: createNewVaultInProgress,
+ showNewVaultSeed: showNewVaultSeed,
+ showInfoPage: showInfoPage,
+ // unlock screen
+ UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
+ UNLOCK_FAILED: 'UNLOCK_FAILED',
+ UNLOCK_METAMASK: 'UNLOCK_METAMASK',
+ LOCK_METAMASK: 'LOCK_METAMASK',
+ tryUnlockMetamask: tryUnlockMetamask,
+ lockMetamask: lockMetamask,
+ unlockInProgress: unlockInProgress,
+ // error handling
+ displayWarning: displayWarning,
+ DISPLAY_WARNING: 'DISPLAY_WARNING',
+ HIDE_WARNING: 'HIDE_WARNING',
+ hideWarning: hideWarning,
+ // accounts screen
+ SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT',
+ SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL',
+ SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
+ SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
+ // account detail screen
+ SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
+ showSendPage: showSendPage,
+ REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
+ requestExportAccount: requestExportAccount,
+ EXPORT_ACCOUNT: 'EXPORT_ACCOUNT',
+ exportAccount: exportAccount,
+ SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
+ showPrivateKey: showPrivateKey,
+ // tx conf screen
+ COMPLETED_TX: 'COMPLETED_TX',
+ TRANSACTION_ERROR: 'TRANSACTION_ERROR',
+ NEXT_TX: 'NEXT_TX',
+ PREVIOUS_TX: 'PREV_TX',
+ setSelectedAddress: setSelectedAddress,
+ signTx: signTx,
+ sendTx: sendTx,
+ cancelTx: cancelTx,
+ completedTx: completedTx,
+ txError: txError,
+ nextTx: nextTx,
+ previousTx: previousTx,
+ // app messages
+ showAccountDetail: showAccountDetail,
+ BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL',
+ backToAccountDetail: backToAccountDetail,
+ showAccountsPage: showAccountsPage,
+ showConfTxPage: showConfTxPage,
+ confirmSeedWords: confirmSeedWords,
+ // config screen
+ SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE',
+ SET_RPC_TARGET: 'SET_RPC_TARGET',
+ USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER',
+ useEtherscanProvider: useEtherscanProvider,
+ showConfigPage: showConfigPage,
+ setRpcTarget: setRpcTarget,
+ // hacky - need a way to get a reference to account manager
+ _setAccountManager: _setAccountManager,
+ // loading overlay
+ SHOW_LOADING: 'SHOW_LOADING_INDICATION',
+ HIDE_LOADING: 'HIDE_LOADING_INDICATION',
+ showLoadingIndication: showLoadingIndication,
+ hideLoadingIndication: hideLoadingIndication,
+}
+
+module.exports = actions
+
+
+var _accountManager = null
+function _setAccountManager(accountManager){
+ _accountManager = accountManager
+}
+
+// async actions
+
+function tryUnlockMetamask(password) {
+ return (dispatch) => {
+ dispatch(this.unlockInProgress())
+ _accountManager.submitPassword(password, (err) => {
+ dispatch(this.hideLoadingIndication())
+ if (err) {
+ dispatch(this.unlockFailed())
+ } else {
+ dispatch(this.unlockMetamask())
+ dispatch(this.setSelectedAddress())
+ }
+ })
+ }
+}
+
+function createNewVault(password, entropy) {
+ return (dispatch) => {
+ dispatch(this.createNewVaultInProgress())
+ _accountManager.createNewVault(password, entropy, (err, result) => {
+ dispatch(this.showNewVaultSeed(result))
+ })
+ }
+}
+
+function recoverFromSeed(password, seed) {
+ return (dispatch) => {
+ // dispatch(this.createNewVaultInProgress())
+ dispatch(this.showLoadingIndication())
+ _accountManager.recoverFromSeed(password, seed, (err, result) => {
+ if (err) {
+ dispatch(this.hideLoadingIndication())
+ var message = err.message
+ return dispatch(this.displayWarning(err.message))
+ }
+
+ dispatch(this.unlockMetamask())
+ dispatch(this.setSelectedAddress())
+ dispatch(this.updateMetamaskState(result))
+ dispatch(this.hideLoadingIndication())
+ dispatch(this.showAccountsPage())
+ })
+ }
+}
+
+function showInfoPage() {
+ return {
+ type: this.SHOW_INFO_PAGE,
+ }
+}
+
+function setSelectedAddress(address) {
+ return (dispatch) => {
+ _accountManager.setSelectedAddress(address)
+ }
+}
+
+function signTx(txData) {
+ return (dispatch) => {
+ dispatch(this.showLoadingIndication())
+
+ web3.eth.sendTransaction(txData, (err, data) => {
+ dispatch(this.hideLoadingIndication())
+
+ if (err) return dispatch(this.displayWarning(err.message))
+ dispatch(this.hideWarning())
+ dispatch(this.showAccountsPage())
+ })
+ }
+}
+
+function sendTx(txData) {
+ return (dispatch) => {
+ _accountManager.approveTransaction(txData.id, (err) => {
+ if (err) {
+ alert(err.message)
+ dispatch(this.txError(err))
+ return console.error(err.message)
+ }
+ dispatch(this.completedTx(txData.id))
+ })
+ }
+}
+
+function completedTx(id) {
+ return {
+ type: this.COMPLETED_TX,
+ id,
+ }
+}
+
+function txError(err) {
+ return {
+ type: this.TRANSACTION_ERROR,
+ message: err.message,
+ }
+}
+
+function cancelTx(txData){
+ return (dispatch) => {
+ _accountManager.cancelTransaction(txData.id)
+ dispatch(this.showAccountsPage())
+ }
+}
+
+//
+// initialize screen
+//
+
+
+function showCreateVault() {
+ return {
+ type: this.SHOW_CREATE_VAULT,
+ }
+}
+
+function showRestoreVault() {
+ return {
+ type: this.SHOW_RESTORE_VAULT,
+ }
+}
+
+function showInitializeMenu() {
+ return {
+ type: this.SHOW_INIT_MENU,
+ }
+}
+
+function createNewVaultInProgress() {
+ return {
+ type: this.CREATE_NEW_VAULT_IN_PROGRESS,
+ }
+}
+
+function showNewVaultSeed(seed) {
+ return {
+ type: this.SHOW_NEW_VAULT_SEED,
+ value: seed,
+ }
+}
+
+//
+// unlock screen
+//
+
+function unlockInProgress() {
+ return {
+ type: this.UNLOCK_IN_PROGRESS,
+ }
+}
+
+function unlockFailed() {
+ return {
+ type: this.UNLOCK_FAILED,
+ }
+}
+
+function unlockMetamask() {
+ return {
+ type: this.UNLOCK_METAMASK,
+ }
+}
+
+function updateMetamaskState(newState) {
+ return {
+ type: this.UPDATE_METAMASK_STATE,
+ value: newState,
+ }
+}
+
+function lockMetamask() {
+ return (dispatch) => {
+ _accountManager.setLocked((err) => {
+ dispatch({
+ type: this.LOCK_METAMASK,
+ })
+ dispatch(this.hideLoadingIndication())
+ })
+ }
+}
+
+function showAccountDetail(address) {
+ return {
+ type: this.SHOW_ACCOUNT_DETAIL,
+ value: address,
+ }
+}
+
+function backToAccountDetail(address) {
+ return {
+ type: this.BACK_TO_ACCOUNT_DETAIL,
+ value: address,
+ }
+}
+function clearSeedWordCache() {
+ return {
+ type: this.CLEAR_SEED_WORD_CACHE
+ }
+}
+
+function confirmSeedWords() {
+ return (dispatch) => {
+ dispatch(this.showLoadingIndication())
+ _accountManager.clearSeedWordCache((err) => {
+ dispatch(this.clearSeedWordCache())
+ console.log('Seed word cache cleared.')
+ dispatch(this.setSelectedAddress())
+ })
+ }
+}
+
+function showAccountsPage() {
+ return {
+ type: this.SHOW_ACCOUNTS_PAGE,
+ }
+}
+
+function showConfTxPage() {
+ return {
+ type: this.SHOW_CONF_TX_PAGE,
+ }
+}
+
+function nextTx() {
+ return {
+ type: this.NEXT_TX,
+ }
+}
+
+function previousTx() {
+ return {
+ type: this.PREVIOUS_TX,
+ }
+}
+
+function showConfigPage() {
+ return {
+ type: this.SHOW_CONFIG_PAGE,
+ }
+}
+
+//
+// config
+//
+
+function setRpcTarget(newRpc) {
+ _accountManager.setRpcTarget(newRpc)
+ return {
+ type: this.SET_RPC_TARGET,
+ value: newRpc,
+ }
+}
+
+function useEtherscanProvider() {
+ _accountManager.useEtherscanProvider()
+ return {
+ type: this.USE_ETHERSCAN_PROVIDER,
+ }
+}
+
+function showLoadingIndication() {
+ return {
+ type: this.SHOW_LOADING,
+ }
+}
+
+function hideLoadingIndication() {
+ return {
+ type: this.HIDE_LOADING,
+ }
+}
+
+function displayWarning(text) {
+ return {
+ type: this.DISPLAY_WARNING,
+ value: text,
+ }
+}
+
+function hideWarning() {
+ return {
+ type: this.HIDE_WARNING,
+ }
+}
+
+function requestExportAccount() {
+ return {
+ type: this.REQUEST_ACCOUNT_EXPORT,
+ }
+}
+
+function exportAccount(address) {
+ var self = this
+
+ return function(dispatch) {
+ dispatch(self.showLoadingIndication())
+
+ _accountManager.exportAccount(address, function(err, result) {
+ dispatch(self.hideLoadingIndication())
+
+ if (err) {
+ console.error(err)
+ return dispatch(self.displayWarning('Had a problem exporting the account.'))
+ }
+
+ dispatch(self.showPrivateKey(result))
+ })
+ }
+}
+
+function showPrivateKey(key) {
+ return {
+ type: this.SHOW_PRIVATE_KEY,
+ value: key,
+ }
+}
+
+function showSendPage() {
+ return {
+ type: this.SHOW_SEND_PAGE,
+ }
+}
diff --git a/ui/app/app.js b/ui/app/app.js
new file mode 100644
index 000000000..fa375fb7f
--- /dev/null
+++ b/ui/app/app.js
@@ -0,0 +1,242 @@
+const inherits = require('util').inherits
+const React = require('react')
+const Component = require('react').Component
+const PropTypes = require('react').PropTypes
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const extend = require('xtend')
+const actions = require('./actions')
+const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
+// init
+const InitializeMenuScreen = require('./first-time/init-menu')
+const CreateVaultScreen = require('./first-time/create-vault')
+const CreateVaultCompleteScreen = require('./first-time/create-vault-complete')
+const RestoreVaultScreen = require('./first-time/restore-vault')
+// unlock
+const UnlockScreen = require('./unlock')
+// accounts
+const AccountsScreen = require('./accounts')
+const AccountDetailScreen = require('./account-detail')
+const SendTransactionScreen = require('./send')
+const ConfirmTxScreen = require('./conf-tx')
+// other views
+const ConfigScreen = require('./config')
+const InfoScreen = require('./info')
+const LoadingIndicator = require('./loading')
+
+module.exports = connect(mapStateToProps)(App)
+
+
+inherits(App, Component)
+function App() { Component.call(this) }
+
+function mapStateToProps(state) {
+ return {
+ // state from plugin
+ isInitialized: state.metamask.isInitialized,
+ isUnlocked: state.metamask.isUnlocked,
+ currentView: state.appState.currentView,
+ activeAddress: state.appState.activeAddress,
+ transForward: state.appState.transForward,
+ seedWords: state.metamask.seedWords,
+ }
+}
+
+App.prototype.render = function() {
+ // const { selectedReddit, posts, isFetching, lastUpdated } = this.props
+ var state = this.props
+ var view = state.currentView.name
+ var transForward = state.transForward
+ var shouldHaveFooter = true
+ switch (view) {
+ case 'restoreVault':
+ shouldHaveFooter = false;
+ case 'createVault':
+ shouldHaveFooter = false;
+ case 'createVaultComplete':
+ shouldHaveFooter = false;
+ }
+
+ return (
+
+ h('.flex-column.flex-grow.full-height', {
+ style: {
+ // Windows was showing a vertical scroll bar:
+ overflowY: 'hidden',
+ }
+ },
+ [
+
+ h(LoadingIndicator),
+
+ // top row
+ h('.app-header.flex-column.flex-center', {
+ }, [
+ h('h1', 'MetaMask'),
+ ]),
+
+ // panel content
+ h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), {
+ style: {
+ height: '380px',
+ }
+ }, [
+ h(ReactCSSTransitionGroup, {
+ transitionName: "main",
+ transitionEnterTimeout: 300,
+ transitionLeaveTimeout: 300,
+ }, [
+ this.renderPrimary(),
+ ]),
+ ]),
+
+ // footer
+ h('.app-footer.flex-row.flex-space-around', {
+ style: {
+ display: shouldHaveFooter ? 'flex' : 'none',
+ alignItems: 'center',
+ height: '56px',
+ }
+ }, [
+
+ // settings icon
+ h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), {
+ style: {
+ opacity: state.isUnlocked ? '1.0' : '0.0',
+ transition: 'opacity 200ms ease-in',
+ //transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`,
+ },
+ onClick: function(ev) {
+ state.dispatch(actions.showConfigPage())
+ },
+ }),
+
+ // toggle
+ onOffToggle({
+ toggleMetamaskActive: this.toggleMetamaskActive.bind(this),
+ isUnlocked: state.isUnlocked,
+ }),
+
+ // help
+ h('i.fa.fa-question.fa-lg.cursor-pointer', {
+ style: {
+ opacity: state.isUnlocked ? '1.0' : '0.0',
+ },
+ onClick() { state.dispatch(actions.showInfoPage()) }
+ }),
+ ]),
+ ])
+ )
+}
+
+App.prototype.toggleMetamaskActive = function(){
+ if (!this.props.isUnlocked) {
+ // currently inactive: redirect to password box
+ var passwordBox = document.querySelector('input[type=password]')
+ if (!passwordBox) return
+ passwordBox.focus()
+ } else {
+ // currently active: deactivate
+ this.props.dispatch(actions.lockMetamask(false))
+ }
+}
+
+App.prototype.renderPrimary = function(state){
+ var state = this.props
+
+ // If seed words haven't been dismissed yet, show them still.
+ /*
+ if (state.seedWords) {
+ return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
+ }
+ */
+
+ // show initialize screen
+ if (!state.isInitialized) {
+
+ // show current view
+ switch (state.currentView.name) {
+
+ case 'createVault':
+ return h(CreateVaultScreen, {key: 'createVault'})
+
+ case 'restoreVault':
+ return h(RestoreVaultScreen, {key: 'restoreVault'})
+
+ default:
+ return h(InitializeMenuScreen, {key: 'menuScreenInit'})
+
+ }
+ }
+
+ // show unlock screen
+ if (!state.isUnlocked) {
+ return h(UnlockScreen, {key: 'locked'})
+ }
+
+ // show current view
+ switch (state.currentView.name) {
+
+ case 'createVaultComplete':
+ return h(CreateVaultCompleteScreen, {key: 'created-vault'})
+
+ case 'accounts':
+ return h(AccountsScreen, {key: 'accounts'})
+
+ case 'accountDetail':
+ return h(AccountDetailScreen, {key: 'account-detail'})
+
+ case 'sendTransaction':
+ return h(SendTransactionScreen, {key: 'send-transaction'})
+
+ case 'confTx':
+ return h(ConfirmTxScreen, {key: 'confirm-tx'})
+
+ case 'config':
+ return h(ConfigScreen, {key: 'config'})
+
+ case 'info':
+ return h(InfoScreen, {key: 'info'})
+
+ case 'createVault':
+ return h(CreateVaultScreen, {key: 'createVault'})
+
+ default:
+ return h(AccountsScreen, {key: 'accounts'})
+ }
+}
+
+function onOffToggle(state){
+ var buttonSize = '50px';
+ var lockWidth = '20px';
+ return (
+ h('.app-toggle.flex-row.flex-center.lock' + (state.isUnlocked ? '.unlocked' : '.locked'), {
+ width: buttonSize,
+ height: buttonSize,
+ }, [
+ h('div', {
+ onClick: state.toggleMetamaskActive,
+ style: {
+ width: lockWidth,
+ height: '' + parseInt(lockWidth) * 1.5 + 'px',
+ position: 'relative',
+ }
+ }, [
+ h('img.lock-top', {
+ src: 'images/lock-top.png',
+ style: {
+ width: lockWidth,
+ position: 'absolute',
+ }
+ }),
+ h('img', {
+ src: 'images/lock-base.png',
+ style: {
+ width: lockWidth,
+ position: 'absolute',
+ }
+ }),
+ ])
+ ])
+ )
+}
diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js
new file mode 100644
index 000000000..4e433b87d
--- /dev/null
+++ b/ui/app/components/account-panel.js
@@ -0,0 +1,93 @@
+const inherits = require('util').inherits
+const ethUtil = require('ethereumjs-util')
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const addressSummary = require('../util').addressSummary
+const formatBalance = require('../util').formatBalance
+
+module.exports = AccountPanel
+
+
+inherits(AccountPanel, Component)
+function AccountPanel() {
+ Component.call(this)
+}
+
+AccountPanel.prototype.render = function() {
+ var state = this.props
+ var identity = state.identity || {}
+ var account = state.account || {}
+ var isFauceting = state.isFauceting
+
+ return (
+
+ h('.identity-panel.flex-row.flex-space-between'+(state.isSelected?'.selected':''), {
+ style: {
+ flex: '1 0 auto',
+ },
+ onClick: state.onSelect && state.onSelect.bind(null, identity.address),
+ }, [
+
+ // account identicon
+ h('.identicon-wrapper.flex-column.select-none', [
+ h('.identicon', {
+ style: { backgroundImage: 'url("https://ipfs.io/ipfs/'+identity.img+'")' }
+ }),
+ h('span.font-small', identity.name),
+ ]),
+
+ // account address, balance
+ h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
+
+ h('.flex-row.flex-space-between', [
+ h('label.font-small', 'ADDRESS'),
+ h('span.font-small', addressSummary(identity.address)),
+ ]),
+
+ balanceOrFaucetingIndication(account, isFauceting),
+
+ // outlet for inserting additional stuff
+ state.children,
+
+ ]),
+
+ // navigate to account detail
+ !state.onShowDetail ? null :
+ h('.arrow-right.cursor-pointer', {
+ onClick: state.onShowDetail && state.onShowDetail.bind(null, identity.address),
+ }, [
+ h('i.fa.fa-chevron-right.fa-lg'),
+ ]),
+ ])
+ )
+}
+
+function balanceOrFaucetingIndication(account, isFauceting) {
+
+ // Temporarily deactivating isFauceting indication
+ // because it shows fauceting for empty restored accounts.
+ if (/*isFauceting*/ false) {
+
+ return h('.flex-row.flex-space-between', [
+ h('span.font-small', {
+ }, [
+ 'Account is auto-funding,',
+ h('br'),
+ 'please wait.'
+ ]),
+ ])
+
+ } else {
+
+ return h('.flex-row.flex-space-between', [
+ h('label.font-small', 'BALANCE'),
+ h('span.font-small', {
+ style: {
+ overflowX: 'hidden',
+ maxWidth: '136px',
+ }
+ }, formatBalance(account.balance)),
+ ])
+
+ }
+}
diff --git a/ui/app/components/mascot.js b/ui/app/components/mascot.js
new file mode 100644
index 000000000..e043caca1
--- /dev/null
+++ b/ui/app/components/mascot.js
@@ -0,0 +1,65 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const metamaskLogo = require('metamask-logo')
+const getCaretCoordinates = require('textarea-caret')
+const debounce = require('debounce')
+
+module.exports = Mascot
+
+
+inherits(Mascot, Component)
+function Mascot() {
+ Component.call(this)
+ this.logo = metamaskLogo({
+ followMouse: true,
+ pxNotRatio: true,
+ width: 200,
+ height: 200,
+ })
+ if (!this.logo) return
+ this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000)
+ this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false)
+}
+
+
+Mascot.prototype.render = function() {
+ // this is a bit hacky
+ // the event emitter is on `this.props`
+ // and we dont get that until render
+ this.handleAnimationEvents()
+
+ return (
+
+ h('#metamask-mascot-container')
+
+ )
+}
+
+Mascot.prototype.componentDidMount = function() {
+ if (!this.logo) return
+ var targetDivId = 'metamask-mascot-container'
+ var container = document.getElementById(targetDivId)
+ container.appendChild(this.logo.canvas)
+}
+
+Mascot.prototype.componentWillUnmount = function() {
+ if (!this.logo) return
+ this.logo.canvas.remove()
+}
+
+Mascot.prototype.handleAnimationEvents = function(){
+ if (!this.logo) return
+ // only setup listeners once
+ if (this.animations) return
+ this.animations = this.props.animationEventEmitter
+ this.animations.on('point', this.lookAt.bind(this))
+ this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo))
+}
+
+Mascot.prototype.lookAt = function(target){
+ if (!this.logo) return
+ this.unfollowMouse()
+ this.logo.lookAt(target)
+ this.refollowMouse()
+}
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
new file mode 100644
index 000000000..983070013
--- /dev/null
+++ b/ui/app/conf-tx.js
@@ -0,0 +1,140 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const copyToClipboard = require('copy-to-clipboard')
+const actions = require('./actions')
+const AccountPanel = require('./components/account-panel')
+const valuesFor = require('./util').valuesFor
+const addressSummary = require('./util').addressSummary
+const readableDate = require('./util').readableDate
+const formatBalance = require('./util').formatBalance
+const dataSize = require('./util').dataSize
+
+module.exports = connect(mapStateToProps)(ConfirmTxScreen)
+
+function mapStateToProps(state) {
+ return {
+ identities: state.metamask.identities,
+ accounts: state.metamask.accounts,
+ selectedAddress: state.metamask.selectedAddress,
+ unconfTxs: state.metamask.unconfTxs,
+ index: state.appState.currentView.context,
+ }
+}
+
+inherits(ConfirmTxScreen, Component)
+function ConfirmTxScreen() {
+ Component.call(this)
+}
+
+
+ConfirmTxScreen.prototype.render = function() {
+ var state = this.props
+ var unconfTxList = valuesFor(state.unconfTxs).sort(tx => tx.time)
+ var txData = unconfTxList[state.index] || {}
+ var txParams = txData.txParams || {}
+ var address = txParams.from || state.selectedAddress
+ var identity = state.identities[address] || { address: address }
+ var account = state.accounts[address] || { address: address }
+
+ return (
+
+ h('.unconftx-section.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.navigateToAccounts.bind(this),
+ }),
+ h('h2.page-subtitle', 'Confirm Transaction'),
+ ]),
+
+ h('h3', {
+ style: {
+ alignSelf: 'center',
+ display: unconfTxList.length > 1 ? 'block' : 'none',
+ },
+ }, [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ style: {
+ display: state.index === 0 ? 'none' : 'inline-block',
+ },
+ onClick: () => state.dispatch(actions.previousTx()),
+ }),
+ ` Transaction ${state.index + 1} of ${unconfTxList.length} `,
+ h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', {
+ style: {
+ display: state.index + 1 === unconfTxList.length ? 'none' : 'inline-block',
+ },
+ onClick: () => state.dispatch(actions.nextTx()),
+ }),
+ ]),
+
+ h(ReactCSSTransitionGroup, {
+ transitionName: "main",
+ transitionEnterTimeout: 300,
+ transitionLeaveTimeout: 300,
+ }, [
+
+ h('.transaction', {
+ key: txData.id,
+ }, [
+
+ // account that will sign
+ h(AccountPanel, {
+ showFullAddress: true,
+ identity: identity,
+ account: account,
+ }),
+
+ // tx data
+ h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
+
+ h('.flex-row.flex-space-between', [
+ h('label.font-small', 'TO ADDRESS'),
+ h('span.font-small', addressSummary(txParams.to)),
+ ]),
+
+ h('.flex-row.flex-space-between', [
+ h('label.font-small', 'DATE'),
+ h('span.font-small', readableDate(txData.time)),
+ ]),
+
+ h('.flex-row.flex-space-between', [
+ h('label.font-small', 'AMOUNT'),
+ h('span.font-small', formatBalance(txParams.value)),
+ ]),
+
+ ]),
+
+ // send + cancel
+ h('.flex-row.flex-space-around', [
+ h('button', {
+ onClick: this.cancelTransaction.bind(this, txData),
+ }, 'Cancel'),
+ h('button', {
+ onClick: this.sendTransaction.bind(this, txData),
+ }, 'Send'),
+ ]),
+ ]),
+ ]),
+ ]) // No comma or semicolon can go here
+ )
+}
+
+ConfirmTxScreen.prototype.sendTransaction = function(txData, event){
+ event.stopPropagation()
+ this.props.dispatch(actions.sendTx(txData))
+}
+
+ConfirmTxScreen.prototype.cancelTransaction = function(txData, event){
+ event.stopPropagation()
+ this.props.dispatch(actions.cancelTx(txData))
+}
+
+ConfirmTxScreen.prototype.navigateToAccounts = function(event){
+ event.stopPropagation()
+ this.props.dispatch(actions.showAccountsPage())
+}
diff --git a/ui/app/config.js b/ui/app/config.js
new file mode 100644
index 000000000..33d87bcc2
--- /dev/null
+++ b/ui/app/config.js
@@ -0,0 +1,103 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('./actions')
+
+module.exports = connect(mapStateToProps)(ConfigScreen)
+
+function mapStateToProps(state) {
+ return {
+ rpc: state.metamask.rpcTarget,
+ metamask: state.metamask,
+ }
+}
+
+inherits(ConfigScreen, Component)
+function ConfigScreen() {
+ Component.call(this)
+}
+
+
+ConfigScreen.prototype.render = function() {
+ var state = this.props
+ var rpc = state.rpc
+ var metamaskState = state.metamask
+
+ return (
+ h('.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: (event) => {
+ state.dispatch(actions.showAccountsPage())
+ }
+ }),
+ h('h2.page-subtitle', 'Configuration'),
+ ]),
+
+ // conf view
+ h('.flex-column.flex-justify-center.flex-grow.select-none', [
+ h('.flex-space-around', {
+ style: {
+ padding: '20px',
+ }
+ }, [
+
+ currentProviderDisplay(metamaskState),
+
+
+ h('div', [
+ h('input', {
+ placeholder: 'New RPC URL',
+ style: {
+ width: '100%',
+ },
+ onKeyPress(event) {
+ if (event.key === 'Enter') {
+ var element = event.target
+ var newRpc = element.value
+ state.dispatch(actions.setRpcTarget(newRpc))
+ }
+ }
+ }),
+ ]),
+
+ h('div', [
+ h('button', {
+ style: {
+ alignSelf: 'center',
+ },
+ onClick(event) {
+ event.preventDefault()
+ state.dispatch(actions.setRpcTarget('https://rpc.metamask.io/'))
+ }
+ }, 'Use Main Network')
+ ]),
+
+ h('div', [
+ h('button', {
+ style: {
+ alignSelf: 'center',
+ },
+ onClick(event) {
+ event.preventDefault()
+ state.dispatch(actions.setRpcTarget('https://testrpc.metamask.io/'))
+ }
+ }, 'Use Morden Test Network')
+ ]),
+
+ ]),
+ ]),
+ ])
+ )
+}
+
+function currentProviderDisplay(metamaskState) {
+ var rpc = metamaskState.rpcTarget
+ return h('div', [
+ h('h3', {style: { fontWeight: 'bold' }}, 'Currently using RPC'),
+ h('p', rpc)
+ ])
+}
diff --git a/ui/app/css/debug.css b/ui/app/css/debug.css
new file mode 100644
index 000000000..3e125bcd4
--- /dev/null
+++ b/ui/app/css/debug.css
@@ -0,0 +1,21 @@
+/*
+debug / dev
+*/
+
+#app-content {
+ border: 2px solid green;
+}
+
+#design-container {
+ position: absolute;
+ left: 360px;
+ top: -42px;
+ width: calc(100vw - 360px);
+ height: 100vh;
+ overflow: scroll;
+}
+
+#design-container img {
+ width: 2000px;
+ margin-right: 600px;
+} \ No newline at end of file
diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css
new file mode 100644
index 000000000..dd1a755fb
--- /dev/null
+++ b/ui/app/css/fonts.css
@@ -0,0 +1,2 @@
+@import url(https://fonts.googleapis.com/css?family=Roboto:300,500);
+@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); \ No newline at end of file
diff --git a/ui/app/css/index.css b/ui/app/css/index.css
new file mode 100644
index 000000000..4871a650f
--- /dev/null
+++ b/ui/app/css/index.css
@@ -0,0 +1,489 @@
+/*
+faint orange (textfield shades) #FAF6F0
+light orange (button shades): #F5C26D
+dark orange (text): #F5A623
+borders/font/any gray: #4A4A4A
+*/
+
+/*
+application specific styles
+*/
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ /*font-family: 'Open Sans', Arial, sans-serif;*/
+ font-family: 'Roboto', 'Noto', sans-serif;
+ color: #4D4D4D;
+ font-weight: 300;
+ line-height: 1.4em;
+}
+
+#app-content {
+ overflow-x: hidden;
+ min-width: 357px;
+ width: 360px;
+ height: 500px;
+}
+
+button {
+ outline: none;
+ cursor: pointer;
+ margin: 10px;
+ padding: 6px;
+ border: none;
+ border-radius: 3px;
+ background: #F7861C;
+ font-weight: 500;
+ color: white;
+ transform-origin: center center;
+ transition: transform 50ms ease-in;
+}
+button:hover {
+ transform: scale(1.1);
+}
+button:active {
+ transform: scale(0.95);
+}
+
+button.primary {
+ margin: 10px;
+ padding: 6px;
+ border: none;
+ border-radius: 3px;
+ background: #F7861C;
+ font-weight: 500;
+ color: white;
+}
+
+input, textarea {
+ width: 300px;
+ padding: 6px;
+ border-radius: 6px;
+ border-style: solid;
+ outline: none;
+ border: 1px solid #F5A623;
+ background: #FAF6F0;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+a:hover{
+ color: #df6b0e;
+}
+
+/*
+app
+*/
+
+.active {
+ color: #909090;
+}
+
+button.btn-thin {
+ border: 1px solid;
+ border-color: #4D4D4D;
+ color: #4D4D4D;
+ background: rgb(255, 174, 41);
+ border-radius: 4px;
+ min-width: 200px;
+ margin: 12px 0;
+ padding: 6px;
+ font-size: 13px;
+}
+
+.app-header {
+ padding-top: 20px;
+}
+
+.app-header h1 {
+ font-size: 2em;
+ font-weight: 300;
+ height: 42px;
+}
+
+h2.page-subtitle {
+ font-size: 1em;
+ font-weight: 500;
+ height: 24px;
+ color: #F3C83E;
+}
+
+.app-primary {
+}
+
+.app-footer {
+ padding-bottom: 10px;
+ align-items: center;
+}
+
+.identicon {
+ height: 46px;
+ width: 46px;
+ background-size: cover;
+ border-radius: 100%;
+ border: 3px solid gray;
+}
+
+textarea.twelve-word-phrase {
+ margin-top: 20px;
+ width: 300px;
+ height: 180px;
+ font-size: 16px;
+ background: #FAF6F0;
+ resize: none;
+}
+
+/*
+app sections
+*/
+
+/* initialize */
+
+.initialize-screen hr {
+ width: 60px;
+ margin: 12px;
+ border-color: #F3C83E;
+ border-style: solid;
+}
+
+.initialize-screen input[type="password"], .initialize-screen textarea {
+ width: 300px;
+ padding: 6px;
+ border-radius: 6px;
+ border-style: solid;
+ outline: none;
+ border: 1px solid #F5A623;
+ background: #FAF6F0;
+}
+
+.initialize-screen label {
+ margin-top: 20px;
+}
+
+.initialize-screen button.create-vault {
+ margin-top: 40px;
+}
+
+.initialize-screen .warning {
+ font-size: 14px;
+ margin: 0 16px;
+}
+
+/* unlock */
+.error {
+ color: #E20202;
+}
+.lock {
+ width: 50px;
+ height: 50px;
+}
+
+.lock.locked {
+ transform: scale(1.5);
+ opacity: 0.0;
+ transition: opacity 400ms ease-in, transform 400ms ease-in;
+}
+.lock.unlocked {
+ transform: scale(1);
+ opacity: 1;
+ transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in;
+}
+
+.lock.locked .lock-top {
+ transform: scaleX(1) translateX(0);
+ transition: transform 250ms ease-in;
+}
+.lock.unlocked .lock-top {
+ transform: scaleX(-1) translateX(-12px);
+ transition: transform 250ms ease-in;
+}
+.lock.unlocked:hover {
+ border-radius: 4px;
+ background: #e5e5e5;
+ border: 1px solid #b1b1b1;
+}
+.lock.unlocked:active {
+ background: #c3c3c3;
+}
+
+.section-title .fa-arrow-left {
+ margin: -2px 8px 0px -8px;
+}
+
+.unlock-screen label {
+ color: #F3C83E;
+ font-weight: 500;
+}
+
+.unlock-screen input[type=password] {
+ width: 60%;
+ height: 22px;
+ padding: 2px;
+ border-radius: 4px;
+ border: 2px solid #F3C83E;
+ background: #FAF6F0;
+}
+
+.unlock-screen input[type=password]:focus {
+ outline: none;
+ border: 3px solid #F3C83E;
+}
+
+/* accounts */
+
+.accounts-section {
+ margin: 0 20px;
+}
+
+.current-domain-panel {
+ border: 1px solid #B7B7B7;
+}
+
+.unconftx-link {
+ margin-top: 24px;
+ cursor: pointer;
+}
+
+.unconftx-link .fa-arrow-right {
+ margin: 0px -8px 0px 8px;
+}
+
+/* identity panel */
+
+.identity-panel {
+ font-weight: 500;
+}
+
+.identity-panel .identicon-wrapper {
+ margin: 4px;
+ margin-top: 8px;
+}
+
+.identity-panel .identicon-wrapper span {
+ margin: 0 auto;
+}
+
+.identity-panel .identity-data {
+ margin: 8px 8px 8px 18px;
+}
+
+.identity-panel i {
+ margin-top: 32px;
+ margin-right: 6px;
+ color: #B9B9B9;
+}
+
+.identity-panel .arrow-right {
+ padding-left: 18px;
+ width: 42px;
+ min-width: 18px;
+ height: 100%;
+}
+
+/* accounts screen */
+
+.identity-section {
+ border: 2px solid #4D4D4D;
+ margin: 0;
+}
+
+.identity-section .identity-panel {
+ background: #E9E9E9;
+ border-bottom: 1px solid #B1B1B1;
+ cursor: pointer;
+}
+.identity-section .identity-panel:hover {
+ background: #F9F9F9;
+}
+
+.identity-section .identity-panel.selected {
+ background: white;
+ color: #F3C83E;
+}
+
+.identity-section .identity-panel.selected .identicon {
+ border-color: orange;
+}
+
+/* account detail screen */
+
+.account-detail-section {
+ margin: 0 20px;
+}
+
+/* tx confirm */
+
+.unconftx-section {
+ margin: 0 20px;
+}
+
+.unconftx-section input[type=password] {
+ height: 22px;
+ padding: 2px;
+ margin: 12px;
+ margin-bottom: 24px;
+ border-radius: 4px;
+ border: 2px solid #F3C83E;
+ background: #FAF6F0;
+}
+
+
+/*
+react toggle
+*/
+
+/* overrides */
+
+.react-toggle-track-check {
+ display: none;
+}
+.react-toggle-track-x {
+ display: none;
+}
+
+/* modified original */
+
+.react-toggle {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ background-color: transparent;
+ border: 0;
+ padding: 0;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+ -webkit-tap-highlight-color: transparent;
+}
+
+.react-toggle-screenreader-only {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
+
+.react-toggle--disabled {
+ opacity: 0.5;
+ -webkit-transition: opacity 0.25s;
+ transition: opacity 0.25s;
+}
+
+.react-toggle-track {
+ width: 50px;
+ height: 24px;
+ padding: 0;
+ border-radius: 30px;
+ background-color: #4D4D4D;
+ -webkit-transition: all 0.2s ease;
+ -moz-transition: all 0.2s ease;
+ transition: all 0.2s ease;
+}
+
+.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
+ background-color: #000000;
+}
+
+.react-toggle--checked .react-toggle-track {
+ background-color: rgb(255, 174, 41);
+}
+
+.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
+ background-color: rgb(243, 151, 0);
+}
+
+.react-toggle-track-check {
+ position: absolute;
+ width: 14px;
+ height: 10px;
+ top: 0px;
+ bottom: 0px;
+ margin-top: auto;
+ margin-bottom: auto;
+ line-height: 0;
+ left: 8px;
+ opacity: 0;
+ -webkit-transition: opacity 0.25s ease;
+ -moz-transition: opacity 0.25s ease;
+ transition: opacity 0.25s ease;
+}
+
+.react-toggle--checked .react-toggle-track-check {
+ opacity: 1;
+ -webkit-transition: opacity 0.25s ease;
+ -moz-transition: opacity 0.25s ease;
+ transition: opacity 0.25s ease;
+}
+
+.react-toggle-track-x {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ top: 0px;
+ bottom: 0px;
+ margin-top: auto;
+ margin-bottom: auto;
+ line-height: 0;
+ right: 10px;
+ opacity: 1;
+ -webkit-transition: opacity 0.25s ease;
+ -moz-transition: opacity 0.25s ease;
+ transition: opacity 0.25s ease;
+}
+
+.react-toggle--checked .react-toggle-track-x {
+ opacity: 0;
+}
+
+.react-toggle-thumb {
+ transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
+ position: absolute;
+ top: 1px;
+ left: 1px;
+ width: 22px;
+ height: 22px;
+ border: 1px solid #4D4D4D;
+ border-radius: 50%;
+ background-color: #FAFAFA;
+
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+
+ -webkit-transition: all 0.25s ease;
+ -moz-transition: all 0.25s ease;
+ transition: all 0.25s ease;
+}
+
+.react-toggle--checked .react-toggle-thumb {
+ left: 27px;
+ border-color: #828282;
+}
+/*
+ .react-toggle--focus .react-toggle-thumb {
+ -webkit-box-shadow: 0px 0px 3px 2px #0099E0;
+ -moz-box-shadow: 0px 0px 3px 2px #0099E0;
+ box-shadow: 0px 0px 2px 3px #0099E0;
+ }
+
+ .react-toggle:active .react-toggle-thumb {
+ -webkit-box-shadow: 0px 0px 5px 5px #0099E0;
+ -moz-box-shadow: 0px 0px 5px 5px #0099E0;
+ box-shadow: 0px 0px 5px 5px #0099E0;
+ }
diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css
new file mode 100644
index 000000000..b6b26402b
--- /dev/null
+++ b/ui/app/css/lib.css
@@ -0,0 +1,143 @@
+/* lib */
+
+.full-width {
+ width: 100%;
+}
+
+.full-height {
+ height: 100%;
+}
+
+.flex-column {
+ display: flex;
+ flex-direction: column;
+}
+
+.flex-column-bottom {
+ display: flex;
+ flex-direction: column-reverse;
+}
+
+.flex-row {
+ display: flex;
+ flex-direction: row;
+}
+
+.flex-space-between {
+ justify-content: space-between;
+}
+
+.flex-space-around {
+ justify-content: space-around;
+}
+
+.flex-right {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+}
+
+.flex-left {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+}
+
+.flex-fixed {
+ flex: none;
+}
+
+.flex-grow {
+ flex: 1 1 auto;
+}
+
+.flex-wrap {
+ flex-wrap: wrap;
+}
+
+.flex-center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.flex-justify-center {
+ justify-content: center;
+}
+
+.flex-align-center {
+ align-items: center;
+}
+
+.flex-self-end {
+ align-self: flex-end;
+}
+
+.flex-self-stretch {
+ align-self: stretch;
+}
+
+.flex-vertical {
+ flex-direction: column;
+}
+
+.z-bump {
+ z-index: 1;
+}
+
+.select-none {
+ cursor: default;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.cursor-pointer {
+ cursor: pointer;
+ transform-origin: center center;
+ transition: transform 50ms ease-in-out;
+}
+.cursor-pointer:hover {
+ transform: scale(1.1);
+}
+.cursor-pointer:active {
+ transform: scale(0.95);
+}
+
+.margin-bottom-sml {
+ margin-bottom: 20px;
+}
+
+.margin-bottom-med {
+ margin-bottom: 40px;
+}
+
+.margin-right-left {
+ margin: 0 20px;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.font-small {
+ font-size: 12px;
+}
+
+/* Send Screen */
+.send-screen {
+ margin: 0 20px;
+}
+.send-screen section {
+ margin: 7px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+}
+.send-screen details {
+ width: 100%;
+}
+.send-screen section input {
+ width: 100%;
+}
diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css
new file mode 100644
index 000000000..9ce89e8bc
--- /dev/null
+++ b/ui/app/css/reset.css
@@ -0,0 +1,48 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+} \ No newline at end of file
diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css
new file mode 100644
index 000000000..3d0bf46a1
--- /dev/null
+++ b/ui/app/css/transitions.css
@@ -0,0 +1,47 @@
+/* initial positions */
+.app-primary.from-right .main-enter {
+ transform: translateX(400px);
+ position: absolute;
+ width: 100%;
+ transition: transform 300ms ease-in-out;
+}
+.app-primary.from-left .main-enter {
+ transform: translateX(-400px);
+ position: absolute;
+ width: 100%;
+ transition: transform 300ms ease-in-out;
+}
+
+/* center position */
+.app-primary .main-enter.main-enter-active,
+.app-primary .main-leave {
+ transform: translateX(0px);
+ position: absolute;
+ width: 100%;
+ transition: transform 300ms ease-in-out;
+}
+
+/* final positions */
+.app-primary.from-left .main-leave-active {
+ transform: translateX(400px);
+ position: absolute;
+ width: 100%;
+ transition: transform 300ms ease-in-out;
+}
+.app-primary.from-right .main-leave-active {
+ transform: translateX(-400px);
+ position: absolute;
+ width: 100%;
+ transition: transform 300ms ease-in-out;
+}
+
+/* loader transitions */
+.loader-enter, .loader-leave-active {
+ opacity: 0.0;
+ transition: opacity 150 ease-in-out;
+}
+.loader-enter-active, .loader-leave {
+ opacity: 1.0;
+ transition: opacity 150 ease-in-out;
+}
+
diff --git a/ui/app/first-time/create-vault-complete.js b/ui/app/first-time/create-vault-complete.js
new file mode 100644
index 000000000..cd062effe
--- /dev/null
+++ b/ui/app/first-time/create-vault-complete.js
@@ -0,0 +1,57 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const actions = require('../actions')
+
+module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
+
+
+inherits(CreateVaultCompleteScreen, Component)
+function CreateVaultCompleteScreen() {
+ Component.call(this)
+}
+
+function mapStateToProps(state) {
+ return {
+ seed: state.appState.currentView.context,
+ cachedSeed: state.metamask.seedWords,
+ }
+}
+
+CreateVaultCompleteScreen.prototype.render = function() {
+ var state = this.props
+ var seed = state.seed || state.cachedSeed
+
+ return (
+
+ h('.initialize-screen.flex-column.flex-center.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('h2.page-subtitle', 'Vault Created'),
+ ]),
+
+ h('span.error', { // Error for the right red
+ style: {
+ padding: '12px 20px 0px 20px',
+ textAlign: 'center',
+ }
+ }, 'These 12 words can restore all of your MetaMask accounts for this vault.\nSave them somewhere safe and secret.'),
+
+ h('textarea.twelve-word-phrase', {
+ readOnly: true,
+ value: seed,
+ }),
+
+ h('button.btn-thin', {
+ onClick: () => this.confirmSeedWords(),
+ }, 'I\'ve copied it somewhere safe.'),
+ ])
+ )
+}
+
+CreateVaultCompleteScreen.prototype.confirmSeedWords = function() {
+ this.props.dispatch(actions.confirmSeedWords())
+}
+
diff --git a/ui/app/first-time/create-vault.js b/ui/app/first-time/create-vault.js
new file mode 100644
index 000000000..d7bf9cd5d
--- /dev/null
+++ b/ui/app/first-time/create-vault.js
@@ -0,0 +1,123 @@
+const inherits = require('util').inherits
+
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const actions = require('../actions')
+
+module.exports = connect(mapStateToProps)(CreateVaultScreen)
+
+
+inherits(CreateVaultScreen, Component)
+function CreateVaultScreen() {
+ Component.call(this)
+}
+
+function mapStateToProps(state) {
+ return {
+ warning: state.appState.warning,
+ }
+}
+
+CreateVaultScreen.prototype.render = function() {
+ var state = this.props
+ return (
+
+ h('.initialize-screen.flex-column.flex-center.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.showInitializeMenu.bind(this),
+ }),
+ h('h2.page-subtitle', 'Create Vault'),
+ ]),
+
+ // password
+ h('label', {
+ htmlFor: 'password-box',
+ }, 'Enter Password (min 8 chars):'),
+
+ h('input', {
+ type: 'password',
+ id: 'password-box',
+ }),
+
+ // confirm password
+ h('label', {
+ htmlFor: 'password-box-confirm',
+ }, 'Confirm Password:'),
+
+ h('input', {
+ type: 'password',
+ id: 'password-box-confirm',
+ onKeyPress: this.createVaultOnEnter.bind(this),
+ }),
+
+ /* ENTROPY TEXT INPUT CURRENTLY DISABLED
+ // entropy
+ h('label', {
+ htmlFor: 'entropy-text-entry',
+ }, 'Enter random text (optional)'),
+
+ h('textarea', {
+ id: 'entropy-text-entry',
+ style: { resize: 'none' },
+ onKeyPress: this.createVaultOnEnter.bind(this),
+ }),
+ */
+
+ // submit
+ h('button.create-vault.btn-thin', {
+ onClick: this.createNewVault.bind(this),
+ }, 'OK'),
+
+ (!state.inProgress && state.warning) && (
+ h('span.in-progress-notification', state.warning)
+
+ ),
+
+ state.inProgress && (
+ h('span.in-progress-notification', 'Generating Seed...')
+ ),
+ ])
+ )
+}
+
+CreateVaultScreen.prototype.componentDidMount = function(){
+ document.getElementById('password-box').focus()
+}
+
+CreateVaultScreen.prototype.showInitializeMenu = function() {
+ this.props.dispatch(actions.showInitializeMenu())
+}
+
+// create vault
+
+CreateVaultScreen.prototype.createVaultOnEnter = function(event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewVault()
+ }
+}
+
+CreateVaultScreen.prototype.createNewVault = function(){
+ var passwordBox = document.getElementById('password-box')
+ var password = passwordBox.value
+ var passwordConfirmBox = document.getElementById('password-box-confirm')
+ var passwordConfirm = passwordConfirmBox.value
+ // var entropy = document.getElementById('entropy-text-entry').value
+
+ if (password.length < 8) {
+ this.warning = 'password not long enough'
+ this.props.dispatch(actions.displayWarning(this.warning))
+ return
+ }
+ if (password !== passwordConfirm) {
+ this.warning = 'passwords dont match'
+ this.props.dispatch(actions.displayWarning(this.warning))
+ return
+ }
+
+ this.props.dispatch(actions.createNewVault(password, ''/*entropy*/))
+}
diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js
new file mode 100644
index 000000000..11b01a88b
--- /dev/null
+++ b/ui/app/first-time/init-menu.js
@@ -0,0 +1,123 @@
+const inherits = require('util').inherits
+const EventEmitter = require('events').EventEmitter
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const getCaretCoordinates = require('textarea-caret')
+const Mascot = require('../components/mascot')
+const actions = require('../actions')
+const CreateVaultScreen = require('./create-vault')
+const CreateVaultCompleteScreen = require('./create-vault-complete')
+
+module.exports = connect(mapStateToProps)(InitializeMenuScreen)
+
+inherits(InitializeMenuScreen, Component)
+function InitializeMenuScreen() {
+ Component.call(this)
+ this.animationEventEmitter = new EventEmitter()
+}
+
+function mapStateToProps(state) {
+ return {
+ // state from plugin
+ currentView: state.appState.currentView,
+ }
+}
+
+InitializeMenuScreen.prototype.render = function() {
+ var state = this.props
+
+ switch (state.currentView.name) {
+
+ case 'createVault':
+ return h(CreateVaultScreen)
+
+ case 'createVaultComplete':
+ return h(CreateVaultCompleteScreen)
+
+ case 'restoreVault':
+ return this.renderRestoreVault()
+
+ default:
+ return this.renderMenu()
+
+ }
+
+}
+
+// InitializeMenuScreen.prototype.componentDidMount = function(){
+// document.getElementById('password-box').focus()
+// }
+
+InitializeMenuScreen.prototype.renderMenu = function() {
+ var state = this.props
+ return (
+
+ h('.initialize-screen.flex-column.flex-center.flex-grow', [
+
+ h('h2.page-subtitle', 'Welcome!'),
+
+ h(Mascot, {
+ animationEventEmitter: this.animationEventEmitter,
+ }),
+
+ h('button.btn-thin', {
+ onClick: this.showCreateVault.bind(this),
+ }, 'Create New Vault'),
+
+ h('.flex-row.flex-center.flex-grow', [
+ h('hr'),
+ h('div', 'OR'),
+ h('hr'),
+ ]),
+
+ h('button.btn-thin', {
+ onClick: this.showRestoreVault.bind(this),
+ }, 'Restore Existing Vault'),
+
+ ])
+
+ )
+}
+
+InitializeMenuScreen.prototype.renderRestoreVault = function() {
+ var state = this.props
+ return (
+
+ h('.initialize-screen.flex-column.flex-center.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.showInitializeMenu.bind(this),
+ }),
+ h('h2.page-subtitle', 'Restore Vault'),
+ ]),
+
+
+ h('h3', 'Coming soon....'),
+ // h('textarea.twelve-word-phrase', {
+ // value: 'hey ho what the actual hello rubber duck bumbersnatch crumplezone frankenfurter',
+ // }),
+
+ ])
+
+ )
+}
+
+// InitializeMenuScreen.prototype.splitWor = function() {
+// this.props.dispatch(actions.showInitializeMenu())
+// }
+
+InitializeMenuScreen.prototype.showInitializeMenu = function() {
+ this.props.dispatch(actions.showInitializeMenu())
+}
+
+InitializeMenuScreen.prototype.showCreateVault = function() {
+ this.props.dispatch(actions.showCreateVault())
+}
+
+InitializeMenuScreen.prototype.showRestoreVault = function() {
+ this.props.dispatch(actions.showRestoreVault())
+}
+
diff --git a/ui/app/first-time/restore-vault.js b/ui/app/first-time/restore-vault.js
new file mode 100644
index 000000000..55041e8c0
--- /dev/null
+++ b/ui/app/first-time/restore-vault.js
@@ -0,0 +1,116 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const actions = require('../actions')
+
+module.exports = connect(mapStateToProps)(RestoreVaultScreen)
+
+
+inherits(RestoreVaultScreen, Component)
+function RestoreVaultScreen() {
+ Component.call(this)
+}
+
+function mapStateToProps(state) {
+ return {
+ warning: state.appState.warning,
+ }
+}
+
+
+RestoreVaultScreen.prototype.render = function() {
+ var state = this.props
+ return (
+
+ h('.initialize-screen.flex-column.flex-center.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.showInitializeMenu.bind(this),
+ }),
+ h('h2.page-subtitle', 'Restore Vault'),
+ ]),
+
+ // wallet seed entry
+ h('h3', 'Wallet Seed'),
+ h('textarea.twelve-word-phrase', {
+ placeholder: 'Enter your secret twelve word phrase here to restore your vault.'
+ }),
+
+ // password
+ h('label', {
+ htmlFor: 'password-box',
+ }, 'New Password (min 8 chars):'),
+
+ h('input', {
+ type: 'password',
+ id: 'password-box',
+ }),
+
+ // confirm password
+ h('label', {
+ htmlFor: 'password-box-confirm',
+ }, 'Confirm Password:'),
+
+ h('input', {
+ type: 'password',
+ id: 'password-box-confirm',
+ onKeyPress: this.onMaybeCreate.bind(this),
+ }),
+
+ (state.warning) && (
+ h('span.error.in-progress-notification', state.warning)
+ ),
+
+ // submit
+ h('button.btn-thin', {
+ onClick: this.restoreVault.bind(this),
+ }, 'I\'ve double checked the 12 word phrase.'),
+
+ ])
+
+ )
+}
+
+RestoreVaultScreen.prototype.showInitializeMenu = function() {
+ this.props.dispatch(actions.showInitializeMenu())
+}
+
+RestoreVaultScreen.prototype.onMaybeCreate = function(event) {
+ if (event.key === 'Enter') {
+ this.restoreVault()
+ }
+}
+
+RestoreVaultScreen.prototype.restoreVault = function(){
+ // check password
+ var passwordBox = document.getElementById('password-box')
+ var password = passwordBox.value
+ var passwordConfirmBox = document.getElementById('password-box-confirm')
+ var passwordConfirm = passwordConfirmBox.value
+ if (password.length < 8) {
+ this.warning = 'Password not long enough'
+
+ this.props.dispatch(actions.displayWarning(this.warning))
+ return
+ }
+ if (password !== passwordConfirm) {
+ this.warning = 'Passwords don\'t match'
+ this.props.dispatch(actions.displayWarning(this.warning))
+ return
+ }
+ // check seed
+ var seedBox = document.querySelector('textarea.twelve-word-phrase')
+ var seed = seedBox.value.trim()
+ if (seed.split(' ').length !== 12) {
+ this.warning = 'seed phrases are 12 words long'
+ this.props.dispatch(actions.displayWarning(this.warning))
+ return
+ }
+ // submit
+ this.warning = null
+ this.props.dispatch(actions.displayWarning(this.warning))
+ this.props.dispatch(actions.recoverFromSeed(password, seed))
+}
diff --git a/ui/app/img/identicon-tardigrade.png b/ui/app/img/identicon-tardigrade.png
new file mode 100644
index 000000000..1742a32b8
--- /dev/null
+++ b/ui/app/img/identicon-tardigrade.png
Binary files differ
diff --git a/ui/app/img/identicon-walrus.png b/ui/app/img/identicon-walrus.png
new file mode 100644
index 000000000..d58fae912
--- /dev/null
+++ b/ui/app/img/identicon-walrus.png
Binary files differ
diff --git a/ui/app/info.js b/ui/app/info.js
new file mode 100644
index 000000000..ae8c6efc5
--- /dev/null
+++ b/ui/app/info.js
@@ -0,0 +1,90 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('./actions')
+
+module.exports = connect(mapStateToProps)(InfoScreen)
+
+function mapStateToProps(state) {
+ return {}
+}
+
+inherits(InfoScreen, Component)
+function InfoScreen() {
+ Component.call(this)
+}
+
+InfoScreen.prototype.render = function() {
+ var state = this.props
+ var rpc = state.rpc
+
+ return (
+ h('.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: (event) => {
+ state.dispatch(actions.showAccountsPage())
+ }
+ }),
+ h('h2.page-subtitle', 'Info'),
+ ]),
+
+ // main view
+ h('.flex-column.flex-justify-center.flex-grow.select-none', [
+ h('.flex-space-around', {
+ style: {
+ padding: '20px',
+ }
+ }, [
+
+ h('div', [
+ h('a', {
+ href: 'https://consensys.slack.com/archives/team-metamask',
+ target: '_blank',
+ onClick(event) { this.navigateTo(event.target.href) },
+ }, 'Join the conversation on Slack'),
+ ]),
+
+ h('div', [
+ h('a', {
+ href: 'https://metamask.io/',
+ target: '_blank',
+ onClick(event) { this.navigateTo(event.target.href) },
+ }, 'Visit our web site'),
+ ]),
+
+ h('div', [
+ h('a', {
+ href: 'https://twitter.com/metamask_io',
+ target: '_blank',
+ onClick(event) { this.navigateTo(event.target.href) },
+ }, 'Follow us on Twitter'),
+ ]),
+
+ h('div', [
+ h('a', {
+ href: 'mailto:hello@metamask.io?subject=Feedback',
+ target: '_blank',
+ }, 'Email us any questions or comments!'),
+ ]),
+
+ h('div', [
+ h('a', {
+ href: 'https://github.com/metamask/talk/issues',
+ target: '_blank',
+ onClick(event) { this.navigateTo(event.target.href) },
+ }, 'Start a thread on Github'),
+ ]),
+
+ ]),
+ ]),
+ ])
+ )
+}
+
+InfoScreen.prototype.navigateTo = function(url) {
+ chrome.tabs.create({ url });
+}
diff --git a/ui/app/loading.js b/ui/app/loading.js
new file mode 100644
index 000000000..47b758cb6
--- /dev/null
+++ b/ui/app/loading.js
@@ -0,0 +1,51 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('./actions')
+const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
+
+module.exports = connect(mapStateToProps)(LoadingIndicator)
+
+function mapStateToProps(state) {
+ return {
+ isLoading: state.appState.isLoading,
+ }
+}
+
+inherits(LoadingIndicator, Component)
+function LoadingIndicator() {
+ Component.call(this)
+}
+
+LoadingIndicator.prototype.render = function() {
+ console.dir(this.props)
+ var isLoading = this.props.isLoading
+
+ return (
+ h(ReactCSSTransitionGroup, {
+ transitionName: "loader",
+ transitionEnterTimeout: 150,
+ transitionLeaveTimeout: 150,
+ }, [
+
+ isLoading ? h('div', {
+ style: {
+ position: 'absolute',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100%',
+ width: '100%',
+ background: 'rgba(255, 255, 255, 0.5)',
+ }
+ }, [
+ h('img', {
+ src: 'images/loading.svg',
+ }),
+ ]) : null,
+
+ ])
+ )
+}
+
diff --git a/ui/app/reducers.js b/ui/app/reducers.js
new file mode 100644
index 000000000..0f2ad4c21
--- /dev/null
+++ b/ui/app/reducers.js
@@ -0,0 +1,41 @@
+const combineReducers = require('redux').combineReducers
+const actions = require('./actions')
+const extend = require('xtend')
+
+//
+// Sub-Reducers take in the complete state and return their sub-state
+//
+const reduceIdentities = require('./reducers/identities')
+const reduceMetamask = require('./reducers/metamask')
+const reduceApp = require('./reducers/app')
+
+module.exports = rootReducer
+
+function rootReducer(state, action) {
+
+ // clone
+ state = extend(state)
+
+ //
+ // Identities
+ //
+
+ state.identities = reduceIdentities(state, action)
+
+ //
+ // MetaMask
+ //
+
+ state.metamask = reduceMetamask(state, action)
+
+ //
+ // AppState
+ //
+
+ state.appState = reduceApp(state, action)
+
+
+ return state
+
+}
+
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
new file mode 100644
index 000000000..582583185
--- /dev/null
+++ b/ui/app/reducers/app.js
@@ -0,0 +1,281 @@
+const extend = require('xtend')
+const actions = require('../actions')
+
+module.exports = reduceApp
+
+function reduceApp(state, action) {
+
+ // clone and defaults
+ var defaultView = {
+ name: 'accounts',
+ detailView: null,
+ }
+
+ // confirm seed words
+ var seedConfView = {
+ name: 'createVaultComplete',
+ }
+ var seedWords = state.metamask.seedWords
+
+ var appState = extend({
+ currentView: seedWords ? seedConfView : defaultView,
+ currentDomain: 'example.com',
+ transForward: true, // Used to render transition direction
+ isLoading: false, // Used to display loading indicator
+ warning: null, // Used to display error text
+ }, state.appState)
+
+ switch (action.type) {
+
+ // intialize
+
+ case actions.SHOW_CREATE_VAULT:
+ return extend(appState, {
+ currentView: {
+ name: 'createVault',
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.SHOW_RESTORE_VAULT:
+ return extend(appState, {
+ currentView: {
+ name: 'restoreVault',
+ },
+ transForward: true,
+ })
+
+ case actions.SHOW_INIT_MENU:
+ return extend(appState, {
+ currentView: defaultView,
+ transForward: false,
+ })
+
+ case actions.SHOW_CONFIG_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'config',
+ },
+ transForward: true,
+ })
+
+ case actions.SHOW_INFO_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'info',
+ },
+ transForward: true,
+ })
+
+ case actions.CREATE_NEW_VAULT_IN_PROGRESS:
+ return extend(appState, {
+ currentView: {
+ name: 'createVault',
+ inProgress: true,
+ },
+ transForward: true,
+ isLoading: true,
+ })
+
+ case actions.SHOW_NEW_VAULT_SEED:
+ return extend(appState, {
+ currentView: {
+ name: 'createVaultComplete',
+ context: action.value,
+ },
+ transForward: true,
+ isLoading: false,
+ })
+
+ case actions.SHOW_SEND_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'sendTransaction',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ // unlock
+
+ case actions.UNLOCK_METAMASK:
+ return extend(appState, {
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.LOCK_METAMASK:
+ return extend(appState, {
+ currentView: defaultView,
+ transForward: false,
+ warning: null,
+ })
+
+ // accounts
+
+ case actions.SET_SELECTED_ACCOUNT:
+ return extend(appState, {
+ activeAddress: action.value,
+ })
+
+ case actions.SHOW_ACCOUNT_DETAIL:
+ return extend(appState, {
+ currentView: {
+ name: 'accountDetail',
+ context: action.value,
+ },
+ accountDetail: {
+ accountExport: 'none',
+ privateKey: '',
+ },
+ transForward: true,
+ })
+
+ case actions.BACK_TO_ACCOUNT_DETAIL:
+ return extend(appState, {
+ currentView: {
+ name: 'accountDetail',
+ context: action.value,
+ },
+ accountDetail: {
+ accountExport: 'none',
+ privateKey: '',
+ },
+ transForward: false,
+ })
+
+ case actions.SHOW_ACCOUNTS_PAGE:
+ var seedWords = state.metamask.seedWords
+ return extend(appState, {
+ currentView: {
+ name: seedWords ? 'createVaultComplete' : 'accounts',
+ },
+ transForward: appState.currentView.name == 'locked',
+ isLoading: false,
+ warning: null,
+ })
+
+ case actions.SHOW_CONF_TX_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'confTx',
+ context: 0,
+ },
+ transForward: true,
+ warning: null,
+ })
+
+ case actions.COMPLETED_TX:
+ var unconfTxs = Object.keys(state.metamask.unconfTxs).filter(tx => tx !== tx.id)
+ if (unconfTxs && unconfTxs.length > 0) {
+ return extend(appState, {
+ transForward: false,
+ currentView: {
+ name: 'confTx',
+ context: 0,
+ },
+ warning: null,
+ })
+ } else {
+ return extend(appState, {
+ transForward: false,
+ currentView: {
+ name: 'accounts',
+ context: 0,
+ },
+ transForward: false,
+ warning: null,
+ })
+ }
+
+ case actions.NEXT_TX:
+ return extend(appState, {
+ transForward: true,
+ currentView: {
+ name: 'confTx',
+ context: ++appState.currentView.context,
+ warning: null,
+ }
+ })
+
+ case actions.PREVIOUS_TX:
+ return extend(appState, {
+ transForward: false,
+ currentView: {
+ name: 'confTx',
+ context: --appState.currentView.context,
+ warning: null,
+ }
+ })
+
+ case actions.TRANSACTION_ERROR:
+ return extend(appState, {
+ currentView: {
+ name: 'confTx',
+ errorMessage: 'There was a problem submitting this transaction.',
+ },
+ })
+
+ case actions.UNLOCK_FAILED:
+ return extend(appState, {
+ warning: 'Incorrect password. Try again.'
+ })
+
+ case actions.SHOW_LOADING:
+ return extend(appState, {
+ isLoading: true,
+ })
+
+ case actions.HIDE_LOADING:
+ return extend(appState, {
+ isLoading: false,
+ })
+
+ case actions.CLEAR_SEED_WORD_CACHE:
+ return extend(appState, {
+ transForward: true,
+ currentView: {
+ name: 'accounts',
+ },
+ isLoading: false,
+ })
+
+ case actions.DISPLAY_WARNING:
+ return extend(appState, {
+ warning: action.value,
+ })
+
+ case actions.HIDE_WARNING:
+ return extend(appState, {
+ warning: undefined,
+ })
+
+ case actions.REQUEST_ACCOUNT_EXPORT:
+ return extend(appState, {
+ accountDetail: {
+ accountExport: 'requested',
+ },
+ })
+
+ case actions.EXPORT_ACCOUNT:
+ return extend(appState, {
+ accountDetail: {
+ accountExport: 'completed',
+ },
+ })
+
+ case actions.SHOW_PRIVATE_KEY:
+ return extend(appState, {
+ accountDetail: {
+ accountExport: 'completed',
+ privateKey: action.value,
+ },
+ })
+
+ default:
+ return appState
+
+ }
+}
diff --git a/ui/app/reducers/identities.js b/ui/app/reducers/identities.js
new file mode 100644
index 000000000..95ecd23f9
--- /dev/null
+++ b/ui/app/reducers/identities.js
@@ -0,0 +1,18 @@
+const extend = require('xtend')
+const actions = require('../actions')
+
+module.exports = reduceIdentities
+
+function reduceIdentities(state, action) {
+
+ // clone + defaults
+ var idState = extend({
+
+ }, state.identities)
+
+ switch (action.type) {
+ default:
+ return idState
+ }
+
+}
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
new file mode 100644
index 000000000..43bb3f761
--- /dev/null
+++ b/ui/app/reducers/metamask.js
@@ -0,0 +1,73 @@
+const extend = require('xtend')
+const actions = require('../actions')
+
+module.exports = reduceMetamask
+
+function reduceMetamask(state, action) {
+
+ // clone + defaults
+ var metamaskState = extend({
+ isInitialized: false,
+ isUnlocked: false,
+ currentDomain: 'example.com',
+ rpcTarget: 'https://rawtestrpc.metamask.io/',
+ identities: {},
+ unconfTxs: {},
+ }, state.metamask)
+
+ switch (action.type) {
+
+ case actions.SHOW_ACCOUNTS_PAGE:
+ var state = extend(metamaskState)
+ delete state.seedWords
+ return state
+
+ case actions.UPDATE_METAMASK_STATE:
+ return extend(metamaskState, action.value)
+
+ case actions.UNLOCK_METAMASK:
+ return extend(metamaskState, {
+ isUnlocked: true,
+ isInitialized: true,
+ })
+
+ case actions.LOCK_METAMASK:
+ return extend(metamaskState, {
+ isUnlocked: false,
+ })
+
+ case actions.SET_RPC_TARGET:
+ return extend(metamaskState, {
+ rpcTarget: action.value,
+ })
+
+ case actions.COMPLETED_TX:
+ var stringId = String(action.id)
+ var newState = extend(metamaskState, {
+ unconfTxs: {}
+ })
+ for (var id in metamaskState.unconfTxs) {
+ if (id !== stringId) {
+ newState.unconfTxs[id] = metamaskState.unconfTxs[id]
+ }
+ }
+ return newState
+
+ case actions.CLEAR_SEED_WORD_CACHE:
+ var newState = extend(metamaskState, {
+ isInitialized: true,
+ })
+ delete newState.seedWords
+ return newState
+
+ case actions.CREATE_NEW_VAULT_IN_PROGRESS:
+ return extend(metamaskState, {
+ isUnlocked: true,
+ isInitialized: true,
+ })
+
+ default:
+ return metamaskState
+
+ }
+}
diff --git a/ui/app/root.js b/ui/app/root.js
new file mode 100644
index 000000000..9fedf625f
--- /dev/null
+++ b/ui/app/root.js
@@ -0,0 +1,24 @@
+const inherits = require('util').inherits
+const React = require('react')
+const Component = require('react').Component
+const Provider = require('react-redux').Provider
+const h = require('react-hyperscript')
+const App = require('./app')
+
+module.exports = Root
+
+
+inherits(Root, Component)
+function Root() { Component.call(this) }
+
+Root.prototype.render = function() {
+ return (
+
+ h(Provider, {
+ store: this.props.store,
+ }, [
+ h(App)
+ ])
+
+ )
+}
diff --git a/ui/app/send.js b/ui/app/send.js
new file mode 100644
index 000000000..3cb56cb6e
--- /dev/null
+++ b/ui/app/send.js
@@ -0,0 +1,139 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('./actions')
+const util = require('./util')
+const numericBalance = require('./util').numericBalance
+const AccountPanel = require('./components/account-panel')
+const ethUtil = require('ethereumjs-util')
+
+module.exports = connect(mapStateToProps)(SendTransactionScreen)
+
+function mapStateToProps(state) {
+ var result = {
+ address: state.appState.currentView.context,
+ accounts: state.metamask.accounts,
+ identities: state.metamask.identities,
+ warning: state.appState.warning,
+ }
+
+ result.account = result.accounts[result.address]
+ result.identity = result.identities[result.address]
+ result.balance = result.account ? numericBalance(result.account.balance) : null
+
+ return result
+}
+
+inherits(SendTransactionScreen, Component)
+function SendTransactionScreen() {
+ Component.call(this)
+}
+
+SendTransactionScreen.prototype.render = function() {
+ var state = this.props
+ var account = state.account
+ var identity = state.identity
+
+ return (
+ h('.send-screen.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.back.bind(this),
+ }),
+ h('h2.page-subtitle', 'Send Transaction'),
+ ]),
+
+ h(AccountPanel, {
+ showFullAddress: true,
+ identity: identity,
+ account: account,
+ }),
+
+ h('section.recipient', [
+ h('input.address', {
+ placeholder: 'Recipient Address',
+ })
+ ]),
+
+ h('section.ammount', [
+ h('input.ether', {
+ placeholder: 'Amount',
+ type: 'number',
+ style: { marginRight: '6px' }
+ }),
+ h('select.currency', {
+ name: 'currency',
+ }, [
+ h('option', { value: 'ether' }, 'Ether (1e18 wei)'),
+ h('option', { value: 'wei' }, 'Wei'),
+ ]),
+ ]),
+
+ h('section.data', [
+ h('details', [
+ h('summary', {
+ style: {cursor: 'pointer'},
+ }, 'Advanced'),
+ h('textarea.txData', {
+ type: 'textarea',
+ placeholder: 'Transaction data (optional)',
+ style: {
+ height: '100px',
+ width: '100%',
+ resize: 'none',
+ }
+ })
+ ])
+ ]),
+
+ h('section', {
+ }, [
+ h('button', {
+ onClick: this.onSubmit.bind(this),
+ }, 'Send')
+ ]),
+
+ state.warning ? h('span.error', state.warning) : null,
+ ])
+ )
+}
+
+SendTransactionScreen.prototype.back = function() {
+ var address = this.props.address
+ this.props.dispatch(actions.backToAccountDetail(address))
+}
+
+SendTransactionScreen.prototype.onSubmit = function(event) {
+ var recipient = document.querySelector('input.address').value
+ var amount = new ethUtil.BN(document.querySelector('input.ether').value, 10)
+ var currency = document.querySelector('select.currency').value
+ var txData = document.querySelector('textarea.txData').value
+
+ var value = util.normalizeToWei(amount, currency)
+ var balance = this.props.balance
+
+ if (value.gt(balance)) {
+ var message = 'Insufficient funds.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+ if (recipient.length !== 42) {
+ var message = 'Recipient address is the incorrect length.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+
+ this.props.dispatch(actions.hideWarning())
+ this.props.dispatch(actions.showLoadingIndication())
+
+ var txParams = {
+ to: recipient,
+ from: this.props.address,
+ value: '0x' + value.toString(16),
+ }
+ if (txData) txParams.data = txData
+
+ this.props.dispatch(actions.signTx(txParams))
+}
+
diff --git a/ui/app/settings.js b/ui/app/settings.js
new file mode 100644
index 000000000..9a11ef680
--- /dev/null
+++ b/ui/app/settings.js
@@ -0,0 +1,69 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const copyToClipboard = require('copy-to-clipboard')
+const actions = require('./actions')
+const AccountPanel = require('./components/account-panel')
+
+module.exports = connect(mapStateToProps)(AppSettingsPage)
+
+function mapStateToProps(state) {
+ return {
+ identities: state.metamask.identities,
+ address: state.appState.currentView.context,
+ }
+}
+
+inherits(AppSettingsPage, Component)
+function AppSettingsPage() {
+ Component.call(this)
+}
+
+
+AppSettingsPage.prototype.render = function() {
+ var state = this.props
+ var identity = state.identities[state.address]
+ return (
+
+ h('.account-detail-section.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.navigateToAccounts.bind(this),
+ }),
+ h('h2.page-subtitle', 'Settings'),
+ ]),
+
+ h('label', {
+ htmlFor: 'settings-rpc-endpoint',
+ }, 'RPC Endpoint:'),
+ h('input', {
+ // value: '//testrpc.metamask.io',
+ type: 'url',
+ id: 'settings-rpc-endpoint',
+ onKeyPress: this.onKeyPress.bind(this),
+ }),
+
+ ])
+
+ )
+}
+
+AppSettingsPage.prototype.componentDidMount = function(){
+ document.querySelector('input').focus()
+}
+
+AppSettingsPage.prototype.onKeyPress = function(event) {
+ // get submit event
+ if (event.key === 'Enter') {
+ // this.submitPassword(event)
+ }
+}
+
+
+AppSettingsPage.prototype.navigateToAccounts = function(event){
+ event.stopPropagation()
+ this.props.dispatch(actions.showAccountsPage())
+}
diff --git a/ui/app/store.js b/ui/app/store.js
new file mode 100644
index 000000000..a738f1a12
--- /dev/null
+++ b/ui/app/store.js
@@ -0,0 +1,19 @@
+const createStore = require('redux').createStore
+const applyMiddleware = require('redux').applyMiddleware
+const thunkMiddleware = require('redux-thunk')
+const createLogger = require('redux-logger')
+const rootReducer = require('./reducers')
+
+module.exports = configureStore
+
+
+const loggerMiddleware = createLogger()
+
+const createStoreWithMiddleware = applyMiddleware(
+ thunkMiddleware,
+ loggerMiddleware
+)(createStore)
+
+function configureStore(initialState) {
+ return createStoreWithMiddleware(rootReducer, initialState)
+}
diff --git a/ui/app/template.js b/ui/app/template.js
new file mode 100644
index 000000000..3c2d902b5
--- /dev/null
+++ b/ui/app/template.js
@@ -0,0 +1,31 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('./actions')
+
+module.exports = connect(mapStateToProps)(COMPONENTNAME)
+
+function mapStateToProps(state) {
+ return {}
+}
+
+inherits(COMPONENTNAME, Component)
+function COMPONENTNAME() {
+ Component.call(this)
+}
+
+COMPONENTNAME.prototype.render = function() {
+ var state = this.props
+ var rpc = state.rpc
+
+ return (
+ h('div', {
+ style: {
+ display: 'none',
+ }
+ }, [
+ ])
+ )
+}
+
diff --git a/ui/app/unlock.js b/ui/app/unlock.js
new file mode 100644
index 000000000..8aac1b1ff
--- /dev/null
+++ b/ui/app/unlock.js
@@ -0,0 +1,101 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('./actions')
+const Mascot = require('./components/mascot')
+const getCaretCoordinates = require('textarea-caret')
+const EventEmitter = require('events').EventEmitter
+
+module.exports = connect(mapStateToProps)(UnlockScreen)
+
+
+inherits(UnlockScreen, Component)
+function UnlockScreen() {
+ Component.call(this)
+ this.animationEventEmitter = new EventEmitter()
+}
+
+function mapStateToProps(state) {
+ return {
+ warning: state.appState.warning,
+ }
+}
+
+UnlockScreen.prototype.render = function() {
+ const state = this.props
+ const warning = state.warning
+ return (
+
+ h('.unlock-screen.flex-column.flex-center.flex-grow', [
+
+ h('h2.page-subtitle', 'Welcome!'),
+
+ h(Mascot, {
+ animationEventEmitter: this.animationEventEmitter,
+ }),
+
+ h('label', {
+ htmlFor: 'password-box',
+ }, 'Enter Password:'),
+
+ h('input', {
+ type: 'password',
+ id: 'password-box',
+ onKeyPress: this.onKeyPress.bind(this),
+ onInput: this.inputChanged.bind(this),
+ }),
+
+ h('.error', {
+ style: {
+ display: warning ? 'block' : 'none',
+ }
+ }, warning),
+
+ h('button.primary.cursor-pointer', {
+ onClick: this.onSubmit.bind(this),
+ }, 'Unlock'),
+
+ ])
+
+ )
+}
+
+UnlockScreen.prototype.componentDidMount = function(){
+ document.getElementById('password-box').focus()
+}
+
+UnlockScreen.prototype.onSubmit = function(event) {
+ const input = document.getElementById('password-box')
+ const password = input.value
+ this.props.dispatch(actions.tryUnlockMetamask(password))
+}
+
+UnlockScreen.prototype.onKeyPress = function(event) {
+ if (event.key === 'Enter') {
+ this.submitPassword(event)
+ }
+}
+
+UnlockScreen.prototype.submitPassword = function(event){
+ var element = event.target
+ var password = element.value
+ // reset input
+ element.value = ''
+ this.props.dispatch(actions.tryUnlockMetamask(password))
+}
+
+UnlockScreen.prototype.inputChanged = function(event){
+ // tell mascot to look at page action
+ var element = event.target
+ var boundingRect = element.getBoundingClientRect()
+ var coordinates = getCaretCoordinates(element, element.selectionEnd)
+ this.animationEventEmitter.emit('point', {
+ x: boundingRect.left + coordinates.left - element.scrollLeft,
+ y: boundingRect.top + coordinates.top - element.scrollTop,
+ })
+}
+
+UnlockScreen.prototype.emitAnim = function(name, a, b, c){
+ this.animationEventEmitter.emit(name, a, b, c)
+}
diff --git a/ui/app/util.js b/ui/app/util.js
new file mode 100644
index 000000000..4c31e54f4
--- /dev/null
+++ b/ui/app/util.js
@@ -0,0 +1,102 @@
+const ethUtil = require('ethereumjs-util')
+
+var valueTable = {
+ wei: '1000000000000000000',
+ kwei: '1000000000000000',
+ mwei: '1000000000000',
+ gwei: '1000000000',
+ szabo: '1000000',
+ finney:'1000',
+ ether: '1',
+ kether:'0.001',
+ mether:'0.000001',
+ gether:'0.000000001',
+ tether:'0.000000000001',
+}
+var bnTable = {}
+for (var currency in valueTable) {
+ bnTable[currency] = new ethUtil.BN(valueTable[currency], 10)
+}
+
+module.exports = {
+ valuesFor: valuesFor,
+ addressSummary: addressSummary,
+ numericBalance: numericBalance,
+ formatBalance: formatBalance,
+ dataSize: dataSize,
+ readableDate: readableDate,
+ ethToWei: ethToWei,
+ weiToEth: weiToEth,
+ normalizeToWei: normalizeToWei,
+ valueTable: valueTable,
+ bnTable: bnTable,
+}
+
+
+function valuesFor(obj) {
+ if (!obj) return []
+ return Object.keys(obj)
+ .map(function(key){ return obj[key] })
+}
+
+function addressSummary(address) {
+ return address ? address.slice(0,2+8)+'...'+address.slice(-4) : '...'
+}
+
+// Takes wei Hex, returns wei BN, even if input is null
+function numericBalance(balance) {
+ if (!balance) return new ethUtil.BN(0, 16)
+ var stripped = ethUtil.stripHexPrefix(balance)
+ return new ethUtil.BN(stripped, 16)
+}
+
+// Takes eth BN, returns BN wei
+function ethToWei(bn) {
+ var eth = new ethUtil.BN('1000000000000000000')
+ var wei = bn.mul(eth)
+ return wei
+}
+
+// Takes BN in Wei, returns BN in eth
+function weiToEth(bn) {
+ var diff = new ethUtil.BN('1000000000000000000')
+ var eth = bn.div(diff)
+ return eth
+}
+
+function formatBalance(balance) {
+ if (!balance) return 'None'
+ var wei = numericBalance(balance)
+ var eth = weiToEth(wei)
+ return eth.toString(10) + ' ETH'
+}
+
+function dataSize(data) {
+ var size = data ? ethUtil.stripHexPrefix(data).length : 0
+ return size+' bytes'
+}
+
+// Takes a BN and an ethereum currency name,
+// returns a BN in wei
+function normalizeToWei(amount, currency) {
+ try {
+ var ether = amount.div(bnTable[currency])
+ var wei = ether.mul(bnTable.wei)
+ return wei
+ } catch (e) {}
+ return amount
+}
+
+function readableDate(ms) {
+ var date = new Date(ms)
+ var month = date.getMonth()
+ var day = date.getDate()
+ var year = date.getFullYear()
+ var hours = date.getHours()
+ var minutes = "0" + date.getMinutes()
+ var seconds = "0" + date.getSeconds()
+
+ var date = `${month}/${day}/${year}`
+ var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}`
+ return `${date} ${time}`
+}
diff --git a/ui/css.js b/ui/css.js
new file mode 100644
index 000000000..b7bc7d363
--- /dev/null
+++ b/ui/css.js
@@ -0,0 +1,26 @@
+const fs = require('fs')
+
+module.exports = bundleCss
+
+var cssFiles = {
+ 'fonts.css': fs.readFileSync(__dirname+'/app/css/fonts.css', 'utf8'),
+ 'reset.css': fs.readFileSync(__dirname+'/app/css/reset.css', 'utf8'),
+ 'lib.css': fs.readFileSync(__dirname+'/app/css/lib.css', 'utf8'),
+ 'index.css': fs.readFileSync(__dirname+'/app/css/index.css', 'utf8'),
+ 'transitions.css': fs.readFileSync(__dirname+'/app/css/transitions.css', 'utf8'),
+}
+
+function bundleCss() {
+ var cssBundle = Object.keys(cssFiles).reduce(function(bundle, fileName){
+ var fileContent = cssFiles[fileName]
+ var output = String()
+
+ output += '/*========== '+fileName+' ==========*/\n\n'
+ output += fileContent
+ output += '\n\n'
+
+ return bundle+output
+ }, String())
+
+ return cssBundle
+}
diff --git a/ui/design/1st_time_use.png b/ui/design/1st_time_use.png
new file mode 100644
index 000000000..c18ced5e2
--- /dev/null
+++ b/ui/design/1st_time_use.png
Binary files differ
diff --git a/ui/design/metamask_wfs_jan_13.pdf b/ui/design/metamask_wfs_jan_13.pdf
new file mode 100644
index 000000000..c77c9274a
--- /dev/null
+++ b/ui/design/metamask_wfs_jan_13.pdf
Binary files differ
diff --git a/ui/design/metamask_wfs_jan_13.png b/ui/design/metamask_wfs_jan_13.png
new file mode 100644
index 000000000..d71d7bdb4
--- /dev/null
+++ b/ui/design/metamask_wfs_jan_13.png
Binary files differ
diff --git a/ui/design/metamask_wfs_jan_18.pdf b/ui/design/metamask_wfs_jan_18.pdf
new file mode 100644
index 000000000..592ba8532
--- /dev/null
+++ b/ui/design/metamask_wfs_jan_18.pdf
Binary files differ
diff --git a/ui/example.js b/ui/example.js
new file mode 100644
index 000000000..b32da4be4
--- /dev/null
+++ b/ui/example.js
@@ -0,0 +1,123 @@
+const injectCss = require('inject-css')
+const MetaMaskUi = require('./index.js')
+const MetaMaskUiCss = require('./css.js')
+const EventEmitter = require('events').EventEmitter
+
+// account management
+
+var identities = {
+ '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': {
+ name: 'Walrus',
+ img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
+ address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111',
+ balance: 220,
+ txCount: 4,
+ },
+ '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': {
+ name: 'Tardus',
+ img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL',
+ address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222',
+ balance: 10.005,
+ txCount: 16,
+ },
+ '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': {
+ name: 'Gambler',
+ img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
+ address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333',
+ balance: 0.000001,
+ txCount: 1,
+ }
+}
+
+var unconfTxs = {}
+addUnconfTx({
+ from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222',
+ to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111',
+ value: '0x123',
+})
+addUnconfTx({
+ from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111',
+ to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333',
+ value: '0x0000',
+ data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000',
+})
+
+function addUnconfTx(txParams){
+ var time = (new Date()).getTime()
+ var id = createRandomId()
+ unconfTxs[id] = {
+ id: id,
+ txParams: txParams,
+ time: time,
+ }
+}
+
+var isUnlocked = false
+var selectedAddress = null
+
+function getState(){
+ return {
+ isUnlocked: isUnlocked,
+ identities: isUnlocked ? identities : {},
+ unconfTxs: isUnlocked ? unconfTxs : {},
+ selectedAddress: selectedAddress,
+ }
+}
+
+var accountManager = new EventEmitter()
+
+accountManager.getState = function(cb){
+ cb(null, getState())
+}
+
+accountManager.setLocked = function(){
+ isUnlocked = false
+ this._didUpdate()
+}
+
+accountManager.submitPassword = function(password, cb){
+ if (password === 'test') {
+ isUnlocked = true
+ cb(null, getState())
+ this._didUpdate()
+ } else {
+ cb(new Error('Bad password -- try "test"'))
+ }
+}
+
+accountManager.setSelectedAddress = function(address, cb){
+ selectedAddress = address
+ cb(null, getState())
+ this._didUpdate()
+}
+
+accountManager.signTransaction = function(txParams, cb){
+ alert('signing tx....')
+}
+
+accountManager._didUpdate = function(){
+ this.emit('update', getState())
+}
+
+// start app
+
+var container = document.getElementById('app-content')
+
+var css = MetaMaskUiCss()
+injectCss(css)
+
+var app = MetaMaskUi({
+ container: container,
+ accountManager: accountManager
+})
+
+// util
+
+function createRandomId(){
+ // 13 time digits
+ var datePart = new Date().getTime()*Math.pow(10, 3)
+ // 3 random digits
+ var extraPart = Math.floor(Math.random()*Math.pow(10, 3))
+ // 16 digits
+ return datePart+extraPart
+} \ No newline at end of file
diff --git a/ui/index.html b/ui/index.html
new file mode 100644
index 000000000..ba8a8baac
--- /dev/null
+++ b/ui/index.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>MetaMask</title>
+ </head>
+ <body>
+
+ <!-- app content -->
+ <div id="app-content"></div>
+ <script src="./bundle.js" type="text/javascript" charset="utf-8"></script>
+
+ <!-- design reference -->
+ <link rel="stylesheet" type="text/css" href="./app/css/debug.css">
+ <div id="design-container">
+ <img id="design-img" src="./design/metamask_wfs_jan_13.png">
+ <!-- persist scroll position on refresh -->
+ <script type="text/javascript">
+ var scrollElement = document.getElementById('design-container')
+ function getScrollPosition () {
+ var scrollTop = scrollElement.scrollTop, scrollLeft = scrollElement.scrollLeft
+ window.location.hash = 'scrollTop='+scrollTop+'&scrollLeft='+scrollLeft
+ }
+ window.onload = function () {
+ setInterval(getScrollPosition, 1000)
+ var hashLocation = window.location.hash.split('#')[1]
+ if (!hashLocation) return
+ var sections = hashLocation.split('&')
+ var scrollTop = sections[0].split('=')[1]
+ var scrollLeft = sections[1].split('=')[1]
+ scrollElement.scrollTop = scrollTop
+ scrollElement.scrollLeft = scrollLeft
+ }
+ </script>
+ </div>
+
+ </body>
+</html> \ No newline at end of file
diff --git a/ui/index.js b/ui/index.js
new file mode 100644
index 000000000..05d30d8d3
--- /dev/null
+++ b/ui/index.js
@@ -0,0 +1,55 @@
+const React = require('react')
+const render = require('react-dom').render
+const h = require('react-hyperscript')
+const extend = require('xtend')
+const Root = require('./app/root')
+const actions = require('./app/actions')
+const configureStore = require('./app/store')
+
+module.exports = launchApp
+
+function launchApp(opts) {
+
+ var accountManager = opts.accountManager
+ actions._setAccountManager(accountManager)
+
+ // check if we are unlocked first
+ accountManager.getState(function(err, metamaskState){
+ if (err) throw err
+ startApp(metamaskState, accountManager, opts)
+ })
+
+}
+
+function startApp(metamaskState, accountManager, opts){
+
+ // parse opts
+ var store = configureStore({
+
+ // metamaskState represents the cross-tab state
+ metamask: metamaskState,
+
+ // appState represents the current tab's popup state
+ appState: {
+ currentDomain: opts.currentDomain,
+ }
+ })
+
+ // if unconfirmed txs, start on txConf page
+ if (Object.keys(metamaskState.unconfTxs || {}).length) {
+ store.dispatch(actions.showConfTxPage())
+ }
+
+ accountManager.on('update', function(metamaskState){
+ store.dispatch(actions.updateMetamaskState(metamaskState))
+ })
+
+ // start app
+ render(
+ h(Root, {
+ // inject initial state
+ store: store,
+ }
+ ), opts.container)
+
+}
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 000000000..73c9b527d
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "metamask-ui",
+ "version": "1.5.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "mocha test/**/*test.js",
+ "watch": "mocha watch test/**/*test.js",
+ "start": "beefy example.js:bundle.js --live --open",
+ "build": "browserify example.js -g uglifyify -o bundle.js"
+ },
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "beefy": "^2.1.5",
+ "chai": "^3.5.0",
+ "deep-freeze-strict": "^1.1.1",
+ "jsdom": "^8.1.0",
+ "mocha": "^2.4.5",
+ "mocha-jsdom": "^1.1.0",
+ "sinon": "^1.17.3",
+ "uglifyify": "^3.0.1"
+ },
+ "browserify": {
+ "transform": [
+ [
+ "babelify",
+ {
+ "presets": [
+ "es2015"
+ ]
+ }
+ ],
+ "brfs"
+ ]
+ },
+ "dependencies": {
+ "babel-preset-es2015": "^6.3.13",
+ "babelify": "^7.2.0",
+ "brfs": "^1.4.2",
+ "browserify": "^12.0.1",
+ "copy-to-clipboard": "^1.1.1",
+ "debounce": "^1.0.0",
+ "ethereumjs-util": "^2.6.0",
+ "inject-css": "^0.1.1",
+ "metamask-logo": "^1.1.3",
+ "react": "^0.14.3",
+ "react-addons-css-transition-group": "^0.14.7",
+ "react-dom": "^0.14.3",
+ "react-hyperscript": "^2.2.2",
+ "react-redux": "^4.0.3",
+ "redux": "^3.0.5",
+ "redux-logger": "^2.3.1",
+ "redux-thunk": "^1.0.2",
+ "textarea-caret": "^3.0.1",
+ "xtend": "^4.0.1"
+ }
+}
diff --git a/ui/test/setup.js b/ui/test/setup.js
new file mode 100644
index 000000000..7985e9a00
--- /dev/null
+++ b/ui/test/setup.js
@@ -0,0 +1,8 @@
+if (typeof process === 'object') {
+ // Initialize node environment
+ global.expect = require('chai').expect
+ require('mocha-jsdom')()
+} else {
+ window.expect = window.chai.expect
+ window.require = function () { /* noop */ }
+}
diff --git a/ui/test/unit/actions/config_test.js b/ui/test/unit/actions/config_test.js
new file mode 100644
index 000000000..d38210bfc
--- /dev/null
+++ b/ui/test/unit/actions/config_test.js
@@ -0,0 +1,43 @@
+var jsdom = require('mocha-jsdom')
+var assert = require('assert')
+var freeze = require('deep-freeze-strict')
+var path = require('path')
+
+var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
+
+describe ('config view actions', function() {
+
+ var initialState = {
+ metamask: {
+ rpcTarget: 'foo',
+ },
+ appState: {
+ currentView: {
+ name: 'accounts',
+ }
+ }
+ }
+ freeze(initialState)
+
+ describe('SHOW_CONFIG_PAGE', function() {
+ it('should set appState.currentView.name to config', function() {
+ var result = reducers(initialState, actions.showConfigPage())
+ assert.equal(result.appState.currentView.name, 'config')
+ })
+ })
+
+ describe('SET_RPC_TARGET', function() {
+
+ it('sets the state.metamask.rpcTarget property of the state to the action.value', function() {
+ const action = {
+ type: actions.SET_RPC_TARGET,
+ value: 'bar',
+ }
+
+ var result = reducers(initialState, action)
+ assert.equal(result.metamask.rpcTarget, action.value)
+ })
+ })
+})
+
diff --git a/ui/test/unit/actions/restore_vault_test.js b/ui/test/unit/actions/restore_vault_test.js
new file mode 100644
index 000000000..da0d71ce7
--- /dev/null
+++ b/ui/test/unit/actions/restore_vault_test.js
@@ -0,0 +1,54 @@
+var jsdom = require('mocha-jsdom')
+var assert = require('assert')
+var freeze = require('deep-freeze-strict')
+var path = require('path')
+var sinon = require('sinon')
+
+var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
+
+describe('#recoverFromSeed(password, seed)', function() {
+
+ beforeEach(function() {
+ // sinon allows stubbing methods that are easily verified
+ this.sinon = sinon.sandbox.create()
+ })
+
+ afterEach(function() {
+ // sinon requires cleanup otherwise it will overwrite context
+ this.sinon.restore()
+ })
+
+ // stub out account manager
+ actions._setAccountManager({
+ recoverFromSeed(pw, seed, cb) { cb() },
+ })
+
+ it('sets metamask.isUnlocked to true', function() {
+ var initialState = {
+ metamask: {
+ isUnlocked: false,
+ isInitialized: false,
+ }
+ }
+ freeze(initialState)
+
+ const restorePhrase = 'invite heavy among daring outdoor dice jelly coil stable note seat vicious'
+ const password = 'foo'
+ const dispatchFunc = actions.recoverFromSeed(password, restorePhrase)
+
+ var dispatchStub = this.sinon.stub()
+ dispatchStub.withArgs({ TYPE: actions.unlockMetamask() }).onCall(0)
+ dispatchStub.withArgs({ TYPE: actions.showAccountsPage() }).onCall(1)
+
+ var action
+ var resultingState = initialState
+ dispatchFunc((newAction) => {
+ action = newAction
+ resultingState = reducers(resultingState, action)
+ })
+
+ assert.equal(resultingState.metamask.isUnlocked, true, 'was unlocked')
+ assert.equal(resultingState.metamask.isInitialized, true, 'was initialized')
+ });
+});
diff --git a/ui/test/unit/actions/set_selected_account_test.js b/ui/test/unit/actions/set_selected_account_test.js
new file mode 100644
index 000000000..1af6c964f
--- /dev/null
+++ b/ui/test/unit/actions/set_selected_account_test.js
@@ -0,0 +1,28 @@
+var jsdom = require('mocha-jsdom')
+var assert = require('assert')
+var freeze = require('deep-freeze-strict')
+var path = require('path')
+
+var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
+
+describe('SET_SELECTED_ACCOUNT', function() {
+
+ it('sets the state.appState.activeAddress property of the state to the action.value', function() {
+ var initialState = {
+ appState: {
+ activeAddress: 'foo',
+ }
+ }
+ freeze(initialState)
+
+ const action = {
+ type: actions.SET_SELECTED_ACCOUNT,
+ value: 'bar',
+ }
+ freeze(action)
+
+ var resultingState = reducers(initialState, action)
+ assert.equal(resultingState.appState.activeAddress, action.value)
+ });
+});
diff --git a/ui/test/unit/actions/tx_test.js b/ui/test/unit/actions/tx_test.js
new file mode 100644
index 000000000..d83ae16c0
--- /dev/null
+++ b/ui/test/unit/actions/tx_test.js
@@ -0,0 +1,168 @@
+var jsdom = require('mocha-jsdom')
+var assert = require('assert')
+var freeze = require('deep-freeze-strict')
+var path = require('path')
+
+var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
+
+describe('tx confirmation screen', function() {
+ var initialState, result
+
+ describe('when there is only one tx', function() {
+ var firstTxId = 1457634084250832
+
+ beforeEach(function() {
+
+ initialState = {
+ appState: {
+ currentView: {
+ name: 'confTx',
+ },
+ },
+ metamask: {
+ unconfTxs: {
+ '1457634084250832': {
+ id: 1457634084250832,
+ status: "unconfirmed",
+ time: 1457634084250,
+ }
+ },
+ }
+ }
+ freeze(initialState)
+ })
+
+ describe('cancelTx', function() {
+
+ before(function(done) {
+ actions._setAccountManager({
+ approveTransaction(txId, cb) { cb('An error!') },
+ cancelTransaction(txId) { /* noop */ },
+ clearSeedWordCache(cb) { cb() },
+ })
+
+ actions.cancelTx({id: firstTxId})(function(action) {
+ result = reducers(initialState, action)
+ done()
+ })
+ })
+
+ it('should transition to the accounts list', function() {
+ assert.equal(result.appState.currentView.name, 'accounts')
+ })
+
+ it('should have no unconfirmed txs remaining', function() {
+ var count = getUnconfirmedTxCount(result)
+ assert.equal(count, 0)
+ })
+ })
+
+ describe('sendTx', function() {
+ var result
+
+ describe('when there is an error', function() {
+
+ before(function(done) {
+ alert = () => {/* noop */}
+
+ actions._setAccountManager({
+ approveTransaction(txId, cb) { cb('An error!') },
+ })
+
+ actions.sendTx({id: firstTxId})(function(action) {
+ result = reducers(initialState, action)
+ done()
+ })
+ })
+
+ it('should stay on the page', function() {
+ assert.equal(result.appState.currentView.name, 'confTx')
+ })
+
+ it('should set errorMessage on the currentView', function() {
+ assert(result.appState.currentView.errorMessage)
+ })
+ })
+
+ describe('when there is success', function() {
+ before(function(done) {
+ actions._setAccountManager({
+ approveTransaction(txId, cb) { cb() },
+ })
+
+ actions.sendTx({id: firstTxId})(function(action) {
+ result = reducers(initialState, action)
+ done()
+ })
+ })
+
+ it('should navigate away from the tx page', function() {
+ assert.equal(result.appState.currentView.name, 'accounts')
+ })
+
+ it('should clear the tx from the unconfirmed transactions', function() {
+ assert(!(firstTxId in result.metamask.unconfTxs), 'tx is cleared')
+ })
+ })
+ })
+
+ describe('when there are two pending txs', function() {
+ var firstTxId = 1457634084250832
+ var result, initialState
+ before(function(done) {
+ initialState = {
+ appState: {
+ currentView: {
+ name: 'confTx',
+ },
+ },
+ metamask: {
+ unconfTxs: {
+ '1457634084250832': {
+ id: 1457634084250832,
+ status: "unconfirmed",
+ time: 1457634084250,
+ },
+ '1457634084250833': {
+ id: 1457634084250833,
+ status: "unconfirmed",
+ time: 1457634084255,
+ },
+ },
+ }
+ }
+ freeze(initialState)
+
+
+ actions._setAccountManager({
+ approveTransaction(txId, cb) { cb() },
+ })
+
+ actions.sendTx({id: firstTxId})(function(action) {
+ result = reducers(initialState, action)
+ done()
+ })
+ })
+
+ it('should stay on the confTx view', function() {
+ assert.equal(result.appState.currentView.name, 'confTx')
+ })
+
+ it('should transition to the first tx', function() {
+ assert.equal(result.appState.currentView.context, 0)
+ })
+
+ it('should only have one unconfirmed tx remaining', function() {
+ var count = getUnconfirmedTxCount(result)
+ assert.equal(count, 1)
+ })
+ })
+ })
+});
+
+function getUnconfirmedTxCount(state) {
+ var txs = state.metamask.unconfTxs
+ var count = Object.keys(txs).length
+ return count
+}
diff --git a/ui/test/unit/actions/view_info_test.js b/ui/test/unit/actions/view_info_test.js
new file mode 100644
index 000000000..888712c67
--- /dev/null
+++ b/ui/test/unit/actions/view_info_test.js
@@ -0,0 +1,23 @@
+var jsdom = require('mocha-jsdom')
+var assert = require('assert')
+var freeze = require('deep-freeze-strict')
+var path = require('path')
+
+var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
+
+describe('SHOW_INFO_PAGE', function() {
+
+ it('sets the state.appState.currentView.name property to info', function() {
+ var initialState = {
+ appState: {
+ activeAddress: 'foo',
+ }
+ }
+ freeze(initialState)
+
+ const action = actions.showInfoPage()
+ var resultingState = reducers(initialState, action)
+ assert.equal(resultingState.appState.currentView.name, 'info')
+ });
+});
diff --git a/ui/test/unit/actions/warning_test.js b/ui/test/unit/actions/warning_test.js
new file mode 100644
index 000000000..eee198656
--- /dev/null
+++ b/ui/test/unit/actions/warning_test.js
@@ -0,0 +1,24 @@
+var jsdom = require('mocha-jsdom')
+var assert = require('assert')
+var freeze = require('deep-freeze-strict')
+var path = require('path')
+
+var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
+var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
+
+describe('action DISPLAY_WARNING', function() {
+
+ it('sets appState.warning to provided value', function() {
+ var initialState = {
+ appState: {},
+ }
+ freeze(initialState)
+
+ const warningText = 'This is a sample warning message'
+
+ const action = actions.displayWarning(warningText)
+ const resultingState = reducers(initialState, action)
+
+ assert.equal(resultingState.appState.warning, warningText, 'warning text set')
+ });
+});
diff --git a/ui/test/unit/util_test.js b/ui/test/unit/util_test.js
new file mode 100644
index 000000000..52635eb89
--- /dev/null
+++ b/ui/test/unit/util_test.js
@@ -0,0 +1,102 @@
+var assert = require('assert')
+var sinon = require('sinon')
+const ethUtil = require('ethereumjs-util')
+
+var path = require('path')
+var util = require(path.join(__dirname, '..', '..', 'app', 'util.js'))
+
+describe('util', function() {
+ var ethInWei = '1'
+ for (var i = 0; i < 18; i++ ) { ethInWei += '0' }
+
+ beforeEach(function() {
+ this.sinon = sinon.sandbox.create()
+ })
+
+ afterEach(function() {
+ this.sinon.restore()
+ })
+
+ describe('numericBalance', function() {
+
+ it('should return a BN 0 if given nothing', function() {
+ var result = util.numericBalance()
+ assert.equal(result.toString(10), 0)
+ })
+
+ it('should work with hex prefix', function() {
+ var result = util.numericBalance('0x012')
+ assert.equal(result.toString(10), '18')
+ })
+
+ it('should work with no hex prefix', function() {
+ var result = util.numericBalance('012')
+ assert.equal(result.toString(10), '18')
+ })
+
+ })
+
+ describe('#ethToWei', function() {
+
+ it('should take an eth BN, returns wei BN', function() {
+ var input = new ethUtil.BN(1, 10)
+ var result = util.ethToWei(input)
+ assert.equal(result, ethInWei, '18 zeroes')
+ })
+
+ })
+
+ describe('#weiToEth', function() {
+
+ it('should take a wei BN and return an eth BN', function() {
+ var result = util.weiToEth(new ethUtil.BN(ethInWei))
+ assert.equal(result, '1', 'equals 1 eth')
+ })
+
+ })
+
+ describe('#formatBalance', function() {
+
+ it('when given nothing', function() {
+ var result = util.formatBalance()
+ assert.equal(result, 'None', 'should return "None"')
+ })
+
+ it('should return eth as string followed by ETH', function() {
+ var input = new ethUtil.BN(ethInWei).toJSON()
+ var result = util.formatBalance(input)
+ assert.equal(result, '1 ETH')
+ })
+
+ })
+
+ describe('#normalizeToWei', function() {
+ it('should convert an eth to the appropriate equivalent values', function() {
+ var valueTable = {
+ wei: '1000000000000000000',
+ kwei: '1000000000000000',
+ mwei: '1000000000000',
+ gwei: '1000000000',
+ szabo: '1000000',
+ finney:'1000',
+ ether: '1',
+ kether:'0.001',
+ mether:'0.000001',
+ // AUDIT: We're getting BN numbers on these ones.
+ // I think they're big enough to ignore for now.
+ // gether:'0.000000001',
+ // tether:'0.000000000001',
+ }
+ var oneEthBn = new ethUtil.BN(ethInWei, 10)
+
+ for(var currency in valueTable) {
+
+ var value = new ethUtil.BN(valueTable[currency], 10)
+ var output = util.normalizeToWei(value, currency)
+ assert.equal(output.toString(10), valueTable.wei, `value of ${output.toString(10)} ${currency} should convert to ${oneEthBn}`)
+
+ }
+ })
+ })
+
+})