mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 01:39:44 +01:00
Merge branch 'master' into i#563forgotPassword
This commit is contained in:
commit
547894ed39
@ -5,11 +5,13 @@
|
||||
- Add a back button and and functionality to unlock screen so
|
||||
that you can recover your vault from seed or create a new one
|
||||
if you forget your password.
|
||||
- Changed transaction approval from notifications system to popup system.
|
||||
- Forms now retain their values even when closing the popup and reopening it.
|
||||
|
||||
## 2.9.2 2016-08-24
|
||||
|
||||
- Fixed shortcut bug from preventing installation.
|
||||
|
||||
|
||||
## 2.9.1 2016-08-24
|
||||
|
||||
- Added static image as fallback for when WebGL isn't supported.
|
||||
|
16
app/notification.html
Normal file
16
app/notification.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MetaMask Notification</title>
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app-content"></div>
|
||||
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
@ -3,9 +3,7 @@ const extend = require('xtend')
|
||||
const Dnode = require('dnode')
|
||||
const eos = require('end-of-stream')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const createUnlockRequestNotification = require('./lib/notifications.js').createUnlockRequestNotification
|
||||
const createTxNotification = require('./lib/notifications.js').createTxNotification
|
||||
const createMsgNotification = require('./lib/notifications.js').createMsgNotification
|
||||
const notification = require('./lib/notifications.js')
|
||||
const messageManager = require('./lib/message-manager')
|
||||
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
|
||||
const MetamaskController = require('./metamask-controller')
|
||||
@ -26,41 +24,15 @@ const controller = new MetamaskController({
|
||||
const idStore = controller.idStore
|
||||
|
||||
function unlockAccountMessage () {
|
||||
createUnlockRequestNotification({
|
||||
title: 'Account Unlock Request',
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
|
||||
function showUnconfirmedMessage (msgParams, msgId) {
|
||||
var controllerState = controller.getState()
|
||||
|
||||
createMsgNotification({
|
||||
imageifyIdenticons: false,
|
||||
txData: {
|
||||
msgParams: msgParams,
|
||||
time: (new Date()).getTime(),
|
||||
},
|
||||
identities: controllerState.identities,
|
||||
accounts: controllerState.accounts,
|
||||
onConfirm: idStore.approveMessage.bind(idStore, msgId, noop),
|
||||
onCancel: idStore.cancelMessage.bind(idStore, msgId),
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
|
||||
function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
|
||||
var controllerState = controller.getState()
|
||||
|
||||
createTxNotification({
|
||||
imageifyIdenticons: false,
|
||||
txData: {
|
||||
txParams: txParams,
|
||||
time: (new Date()).getTime(),
|
||||
},
|
||||
identities: controllerState.identities,
|
||||
accounts: controllerState.accounts,
|
||||
onConfirm: idStore.approveTransaction.bind(idStore, txData.id, noop),
|
||||
onCancel: idStore.cancelTransaction.bind(idStore, txData.id),
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
|
||||
//
|
||||
@ -69,7 +41,7 @@ function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
|
||||
|
||||
extension.runtime.onConnect.addListener(connectRemote)
|
||||
function connectRemote (remotePort) {
|
||||
var isMetaMaskInternalProcess = (remotePort.name === 'popup')
|
||||
var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification'
|
||||
var portStream = new PortStream(remotePort)
|
||||
if (isMetaMaskInternalProcess) {
|
||||
// communication with popup
|
||||
@ -109,7 +81,7 @@ function setupControllerConnection (stream) {
|
||||
dnode.on('remote', (remote) => {
|
||||
// push updates to popup
|
||||
controller.ethStore.on('update', controller.sendUpdate.bind(controller))
|
||||
controller.remote = remote
|
||||
controller.listeners.push(remote)
|
||||
idStore.on('update', controller.sendUpdate.bind(controller))
|
||||
|
||||
// teardown on disconnect
|
||||
@ -189,4 +161,3 @@ function setData (data) {
|
||||
window.localStorage[STORAGE_KEY] = JSON.stringify(data)
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
|
@ -41,6 +41,12 @@ function Extension () {
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
if (browser[api]) {
|
||||
_this[api] = browser[api]
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
_this.api = browser.extension[api]
|
||||
} catch (e) {}
|
||||
|
8
app/scripts/lib/is-popup-or-notification.js
Normal file
8
app/scripts/lib/is-popup-or-notification.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = function isPopupOrNotification() {
|
||||
const url = window.location.href
|
||||
if (url.match(/popup.html$/)) {
|
||||
return 'popup'
|
||||
} else {
|
||||
return 'notification'
|
||||
}
|
||||
}
|
@ -1,159 +1,48 @@
|
||||
const createId = require('hat')
|
||||
const extend = require('xtend')
|
||||
const unmountComponentAtNode = require('react-dom').unmountComponentAtNode
|
||||
const findDOMNode = require('react-dom').findDOMNode
|
||||
const render = require('react-dom').render
|
||||
const h = require('react-hyperscript')
|
||||
const PendingTxDetails = require('../../../ui/app/components/pending-tx-details')
|
||||
const PendingMsgDetails = require('../../../ui/app/components/pending-msg-details')
|
||||
const MetaMaskUiCss = require('../../../ui/css')
|
||||
const extension = require('./extension')
|
||||
var notificationHandlers = {}
|
||||
|
||||
const notifications = {
|
||||
createUnlockRequestNotification: createUnlockRequestNotification,
|
||||
createTxNotification: createTxNotification,
|
||||
createMsgNotification: createMsgNotification,
|
||||
show,
|
||||
getPopup,
|
||||
closePopup,
|
||||
}
|
||||
module.exports = notifications
|
||||
window.METAMASK_NOTIFIER = notifications
|
||||
|
||||
setupListeners()
|
||||
|
||||
function setupListeners () {
|
||||
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
|
||||
if (!extension.notifications) return console.error('Chrome notifications API missing...')
|
||||
|
||||
// notification button press
|
||||
extension.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) {
|
||||
var handlers = notificationHandlers[notificationId]
|
||||
if (buttonIndex === 0) {
|
||||
handlers.confirm()
|
||||
} else {
|
||||
handlers.cancel()
|
||||
function show () {
|
||||
getPopup((popup) => {
|
||||
if (popup) {
|
||||
return extension.windows.update(popup.id, { focused: true })
|
||||
}
|
||||
extension.notifications.clear(notificationId)
|
||||
})
|
||||
|
||||
// notification teardown
|
||||
extension.notifications.onClosed.addListener(function (notificationId) {
|
||||
delete notificationHandlers[notificationId]
|
||||
extension.windows.create({
|
||||
url: 'notification.html',
|
||||
type: 'detached_panel',
|
||||
focused: true,
|
||||
width: 360,
|
||||
height: 500,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// creation helper
|
||||
function createUnlockRequestNotification (opts) {
|
||||
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
|
||||
if (!extension.notifications) return console.error('Chrome notifications API missing...')
|
||||
var message = 'An Ethereum app has requested a signature. Please unlock your account.'
|
||||
function getPopup(cb) {
|
||||
|
||||
var id = createId()
|
||||
extension.notifications.create(id, {
|
||||
type: 'basic',
|
||||
iconUrl: '/images/icon-128.png',
|
||||
title: opts.title,
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
||||
function createTxNotification (state) {
|
||||
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
|
||||
if (!extension.notifications) return console.error('Chrome notifications API missing...')
|
||||
|
||||
renderTxNotificationSVG(state, function (err, notificationSvgSource) {
|
||||
if (err) throw err
|
||||
|
||||
showNotification(extend(state, {
|
||||
title: 'New Unsigned Transaction',
|
||||
imageUrl: toSvgUri(notificationSvgSource),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function createMsgNotification (state) {
|
||||
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
|
||||
if (!extension.notifications) return console.error('Chrome notifications API missing...')
|
||||
|
||||
renderMsgNotificationSVG(state, function (err, notificationSvgSource) {
|
||||
if (err) throw err
|
||||
|
||||
showNotification(extend(state, {
|
||||
title: 'New Unsigned Message',
|
||||
imageUrl: toSvgUri(notificationSvgSource),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function showNotification (state) {
|
||||
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
|
||||
if (!extension.notifications) return console.error('Chrome notifications API missing...')
|
||||
|
||||
var id = createId()
|
||||
extension.notifications.create(id, {
|
||||
type: 'image',
|
||||
requireInteraction: true,
|
||||
iconUrl: '/images/icon-128.png',
|
||||
imageUrl: state.imageUrl,
|
||||
title: state.title,
|
||||
message: '',
|
||||
buttons: [{
|
||||
title: 'Approve',
|
||||
}, {
|
||||
title: 'Reject',
|
||||
}],
|
||||
})
|
||||
notificationHandlers[id] = {
|
||||
confirm: state.onConfirm,
|
||||
cancel: state.onCancel,
|
||||
// Ignore in test environment
|
||||
if (!extension.windows) {
|
||||
return cb(null)
|
||||
}
|
||||
}
|
||||
|
||||
function renderTxNotificationSVG (state, cb) {
|
||||
var content = h(PendingTxDetails, state)
|
||||
renderNotificationSVG(content, cb)
|
||||
}
|
||||
extension.windows.getAll({}, (windows) => {
|
||||
let popup = windows.find((win) => {
|
||||
return win.type === 'popup'
|
||||
})
|
||||
|
||||
function renderMsgNotificationSVG (state, cb) {
|
||||
var content = h(PendingMsgDetails, state)
|
||||
renderNotificationSVG(content, cb)
|
||||
}
|
||||
|
||||
function renderNotificationSVG (content, cb) {
|
||||
var container = document.createElement('div')
|
||||
var confirmView = h('div.app-primary', {
|
||||
style: {
|
||||
width: '360px',
|
||||
height: '240px',
|
||||
padding: '16px',
|
||||
// background: '#F7F7F7',
|
||||
background: 'white',
|
||||
},
|
||||
}, [
|
||||
h('style', MetaMaskUiCss()),
|
||||
content,
|
||||
])
|
||||
|
||||
render(confirmView, container, function ready() {
|
||||
var rootElement = findDOMNode(this)
|
||||
var viewSource = rootElement.outerHTML
|
||||
unmountComponentAtNode(container)
|
||||
var svgSource = svgWrapper(viewSource)
|
||||
// insert content into svg wrapper
|
||||
cb(null, svgSource)
|
||||
cb(popup)
|
||||
})
|
||||
}
|
||||
|
||||
function svgWrapper (content) {
|
||||
var wrapperSource = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="240">
|
||||
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||
<body xmlns="http://www.w3.org/1999/xhtml" height="100%">{{content}}</body>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
`
|
||||
return wrapperSource.split('{{content}}').join(content)
|
||||
}
|
||||
|
||||
function toSvgUri (content) {
|
||||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(content)
|
||||
function closePopup() {
|
||||
getPopup((popup) => {
|
||||
if (!popup) return
|
||||
extension.windows.remove(popup.id, console.error)
|
||||
})
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ module.exports = class MetamaskController {
|
||||
|
||||
constructor (opts) {
|
||||
this.opts = opts
|
||||
this.listeners = []
|
||||
this.configManager = new ConfigManager(opts)
|
||||
this.idStore = new IdentityStore({
|
||||
configManager: this.configManager,
|
||||
@ -112,9 +113,9 @@ module.exports = class MetamaskController {
|
||||
}
|
||||
|
||||
sendUpdate () {
|
||||
if (this.remote) {
|
||||
this.remote.sendUpdate(this.getState())
|
||||
}
|
||||
this.listeners.forEach((remote) => {
|
||||
remote.sendUpdate(this.getState())
|
||||
})
|
||||
}
|
||||
|
||||
initializeProvider (opts) {
|
||||
@ -130,10 +131,17 @@ module.exports = class MetamaskController {
|
||||
},
|
||||
// tx signing
|
||||
approveTransaction: this.newUnsignedTransaction.bind(this),
|
||||
signTransaction: idStore.signTransaction.bind(idStore),
|
||||
signTransaction: (...args) => {
|
||||
idStore.signTransaction(...args)
|
||||
this.sendUpdate()
|
||||
},
|
||||
|
||||
// msg signing
|
||||
approveMessage: this.newUnsignedMessage.bind(this),
|
||||
signMessage: idStore.signMessage.bind(idStore),
|
||||
signMessage: (...args) => {
|
||||
idStore.signMessage(...args)
|
||||
this.sendUpdate()
|
||||
},
|
||||
}
|
||||
|
||||
var provider = MetaMaskProvider(providerOpts)
|
||||
@ -193,6 +201,8 @@ module.exports = class MetamaskController {
|
||||
|
||||
// It's locked
|
||||
if (!state.isUnlocked) {
|
||||
|
||||
// Allow the environment to define an unlock message.
|
||||
this.opts.unlockAccountMessage()
|
||||
idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, noop)
|
||||
|
||||
@ -200,6 +210,7 @@ module.exports = class MetamaskController {
|
||||
} else {
|
||||
idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => {
|
||||
if (err) return onTxDoneCb(err)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb)
|
||||
})
|
||||
}
|
||||
@ -212,6 +223,7 @@ module.exports = class MetamaskController {
|
||||
this.opts.unlockAccountMessage()
|
||||
} else {
|
||||
this.addUnconfirmedMessage(msgParams, cb)
|
||||
this.sendUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,9 @@ const injectCss = require('inject-css')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const StreamProvider = require('web3-stream-provider')
|
||||
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
|
||||
const isPopupOrNotification = require('./lib/is-popup-or-notification')
|
||||
const extension = require('./lib/extension')
|
||||
const notification = require('./lib/notifications')
|
||||
|
||||
// setup app
|
||||
var css = MetaMaskUiCss()
|
||||
@ -22,7 +24,11 @@ async.parallel({
|
||||
|
||||
function connectToAccountManager (cb) {
|
||||
// setup communication with background
|
||||
var pluginPort = extension.runtime.connect({name: 'popup'})
|
||||
|
||||
var name = isPopupOrNotification()
|
||||
closePopupIfOpen(name)
|
||||
window.METAMASK_UI_TYPE = name
|
||||
var pluginPort = extension.runtime.connect({ name })
|
||||
var portStream = new PortStream(pluginPort)
|
||||
// setup multiplexing
|
||||
var mx = setupMultiplex(portStream)
|
||||
@ -93,3 +99,9 @@ function setupApp (err, opts) {
|
||||
networkVersion: opts.networkVersion,
|
||||
})
|
||||
}
|
||||
|
||||
function closePopupIfOpen(name) {
|
||||
if (name !== 'notification') {
|
||||
notification.closePopup()
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
39
development/states/restore-vault.json
Normal file
39
development/states/restore-vault.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"metamask": {
|
||||
"isInitialized": false,
|
||||
"isUnlocked": false,
|
||||
"isEthConfirmed": false,
|
||||
"currentDomain": "example.com",
|
||||
"rpcTarget": "https://rawtestrpc.metamask.io/",
|
||||
"identities": {},
|
||||
"unconfTxs": {},
|
||||
"currentFiat": "USD",
|
||||
"conversionRate": 0,
|
||||
"conversionDate": "N/A",
|
||||
"accounts": {},
|
||||
"transactions": [],
|
||||
"seedWords": null,
|
||||
"isConfirmed": true,
|
||||
"unconfMsgs": {},
|
||||
"messages": [],
|
||||
"shapeShiftTxList": [],
|
||||
"provider": {
|
||||
"type": "testnet"
|
||||
},
|
||||
"network": "2"
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
"currentView": {
|
||||
"name": "restoreVault"
|
||||
},
|
||||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"currentDomain": "extensions",
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null
|
||||
},
|
||||
"identities": {}
|
||||
}
|
76
development/states/send.json
Normal file
76
development/states/send.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"metamask": {
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"isEthConfirmed": false,
|
||||
"currentDomain": "example.com",
|
||||
"rpcTarget": "https://rawtestrpc.metamask.io/",
|
||||
"identities": {
|
||||
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
|
||||
"name": "Wallet 1",
|
||||
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
|
||||
"mayBeFauceting": false
|
||||
},
|
||||
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
|
||||
"name": "Wallet 2",
|
||||
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
|
||||
"mayBeFauceting": false
|
||||
},
|
||||
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
|
||||
"name": "Wallet 3",
|
||||
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d",
|
||||
"mayBeFauceting": false
|
||||
}
|
||||
},
|
||||
"unconfTxs": {},
|
||||
"currentFiat": "USD",
|
||||
"conversionRate": 11.21283484,
|
||||
"conversionDate": 1472158984,
|
||||
"accounts": {
|
||||
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
|
||||
"code": "0x",
|
||||
"balance": "0x34693f54a1e25900",
|
||||
"nonce": "0x100013",
|
||||
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
|
||||
},
|
||||
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
|
||||
"code": "0x",
|
||||
"nonce": "0x100000",
|
||||
"balance": "0x18af912cee770000",
|
||||
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
|
||||
},
|
||||
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
|
||||
"code": "0x",
|
||||
"nonce": "0x100000",
|
||||
"balance": "0x2386f26fc10000",
|
||||
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
|
||||
}
|
||||
},
|
||||
"transactions": [],
|
||||
"network": "2",
|
||||
"seedWords": null,
|
||||
"isConfirmed": true,
|
||||
"unconfMsgs": {},
|
||||
"messages": [],
|
||||
"shapeShiftTxList": [],
|
||||
"provider": {
|
||||
"type": "testnet"
|
||||
},
|
||||
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
"currentView": {
|
||||
"name": "sendTransaction"
|
||||
},
|
||||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"currentDomain": "127.0.0.1:9966",
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null,
|
||||
"detailView": {}
|
||||
},
|
||||
"identities": {}
|
||||
}
|
348
development/states/shapeshift.json
Normal file
348
development/states/shapeshift.json
Normal file
@ -0,0 +1,348 @@
|
||||
{
|
||||
"metamask": {
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"isEthConfirmed": true,
|
||||
"currentDomain": "example.com",
|
||||
"rpcTarget": "https://rawtestrpc.metamask.io/",
|
||||
"identities": {
|
||||
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
|
||||
"name": "Wallet 1",
|
||||
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
|
||||
"mayBeFauceting": false
|
||||
},
|
||||
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
|
||||
"name": "Wallet 2",
|
||||
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
|
||||
"mayBeFauceting": false
|
||||
},
|
||||
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
|
||||
"name": "Wallet 3",
|
||||
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d",
|
||||
"mayBeFauceting": false
|
||||
}
|
||||
},
|
||||
"unconfTxs": {},
|
||||
"currentFiat": "USD",
|
||||
"conversionRate": 11.21274318,
|
||||
"conversionDate": 1472159644,
|
||||
"accounts": {
|
||||
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
|
||||
"code": "0x",
|
||||
"nonce": "0x13",
|
||||
"balance": "0x461d4a64e937d3d1",
|
||||
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
|
||||
},
|
||||
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
|
||||
"code": "0x",
|
||||
"nonce": "0x0",
|
||||
"balance": "0x0",
|
||||
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
|
||||
},
|
||||
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
|
||||
"code": "0x",
|
||||
"balance": "0x0",
|
||||
"nonce": "0x0",
|
||||
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
|
||||
}
|
||||
},
|
||||
"transactions": [],
|
||||
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
|
||||
"network": "1",
|
||||
"seedWords": null,
|
||||
"isConfirmed": true,
|
||||
"unconfMsgs": {},
|
||||
"messages": [],
|
||||
"shapeShiftTxList": [],
|
||||
"provider": {
|
||||
"type": "mainnet"
|
||||
},
|
||||
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
"currentView": {
|
||||
"name": "buyEth",
|
||||
"context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
|
||||
},
|
||||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"currentDomain": "127.0.0.1:9966",
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"detailView": {},
|
||||
"buyView": {
|
||||
"subview": "buyForm",
|
||||
"formView": {
|
||||
"coinbase": false,
|
||||
"shapeshift": true,
|
||||
"marketinfo": {
|
||||
"pair": "btc_eth",
|
||||
"rate": 51.14252949,
|
||||
"minerFee": 0.01,
|
||||
"limit": 2.60306578,
|
||||
"minimum": 0.00038935,
|
||||
"maxLimit": 8.67688592
|
||||
},
|
||||
"coinOptions": {
|
||||
"BTC": {
|
||||
"name": "Bitcoin",
|
||||
"symbol": "BTC",
|
||||
"image": "https://shapeshift.io/images/coins/bitcoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"BCY": {
|
||||
"name": "BitCrystals",
|
||||
"symbol": "BCY",
|
||||
"image": "https://shapeshift.io/images/coins/bitcrystals.png",
|
||||
"status": "available"
|
||||
},
|
||||
"BLK": {
|
||||
"name": "Blackcoin",
|
||||
"symbol": "BLK",
|
||||
"image": "https://shapeshift.io/images/coins/blackcoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"BTS": {
|
||||
"name": "Bitshares",
|
||||
"symbol": "BTS",
|
||||
"specialReturn": false,
|
||||
"specialOutgoing": true,
|
||||
"specialIncoming": true,
|
||||
"fieldName": "destTag",
|
||||
"fieldKey": "destTag",
|
||||
"image": "https://shapeshift.io/images/coins/bitshares.png",
|
||||
"status": "available"
|
||||
},
|
||||
"CLAM": {
|
||||
"name": "Clams",
|
||||
"symbol": "CLAM",
|
||||
"image": "https://shapeshift.io/images/coins/clams.png",
|
||||
"status": "available"
|
||||
},
|
||||
"DASH": {
|
||||
"name": "Dash",
|
||||
"symbol": "DASH",
|
||||
"image": "https://shapeshift.io/images/coins/dash.png",
|
||||
"status": "available"
|
||||
},
|
||||
"DGB": {
|
||||
"name": "Digibyte",
|
||||
"symbol": "DGB",
|
||||
"image": "https://shapeshift.io/images/coins/digibyte.png",
|
||||
"status": "available"
|
||||
},
|
||||
"DAO": {
|
||||
"name": "TheDao",
|
||||
"symbol": "DAO",
|
||||
"image": "https://shapeshift.io/images/coins/thedao.png",
|
||||
"status": "available"
|
||||
},
|
||||
"DGD": {
|
||||
"name": "DigixDao",
|
||||
"symbol": "DGD",
|
||||
"image": "https://shapeshift.io/images/coins/digixdao.png",
|
||||
"status": "available"
|
||||
},
|
||||
"DOGE": {
|
||||
"name": "Dogecoin",
|
||||
"symbol": "DOGE",
|
||||
"image": "https://shapeshift.io/images/coins/dogecoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"EMC": {
|
||||
"name": "Emercoin",
|
||||
"symbol": "EMC",
|
||||
"image": "https://shapeshift.io/images/coins/emercoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"ETH": {
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"image": "https://shapeshift.io/images/coins/ether.png",
|
||||
"status": "available"
|
||||
},
|
||||
"ETC": {
|
||||
"name": "Ether Classic",
|
||||
"symbol": "ETC",
|
||||
"image": "https://shapeshift.io/images/coins/etherclassic.png",
|
||||
"status": "available"
|
||||
},
|
||||
"FCT": {
|
||||
"name": "Factoids",
|
||||
"symbol": "FCT",
|
||||
"image": "https://shapeshift.io/images/coins/factoids.png",
|
||||
"status": "available"
|
||||
},
|
||||
"LBC": {
|
||||
"name": "LBRY Credits",
|
||||
"symbol": "LBC",
|
||||
"image": "https://shapeshift.io/images/coins/lbry.png",
|
||||
"status": "available"
|
||||
},
|
||||
"LSK": {
|
||||
"name": "Lisk",
|
||||
"symbol": "LSK",
|
||||
"image": "https://shapeshift.io/images/coins/lisk.png",
|
||||
"status": "available"
|
||||
},
|
||||
"LTC": {
|
||||
"name": "Litecoin",
|
||||
"symbol": "LTC",
|
||||
"image": "https://shapeshift.io/images/coins/litecoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"MAID": {
|
||||
"name": "Maidsafe",
|
||||
"symbol": "MAID",
|
||||
"image": "https://shapeshift.io/images/coins/maidsafe.png",
|
||||
"status": "available"
|
||||
},
|
||||
"MINT": {
|
||||
"name": "Mintcoin",
|
||||
"symbol": "MINT",
|
||||
"image": "https://shapeshift.io/images/coins/mintcoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"MONA": {
|
||||
"name": "Monacoin",
|
||||
"symbol": "MONA",
|
||||
"image": "https://shapeshift.io/images/coins/monacoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"MSC": {
|
||||
"name": "Omni",
|
||||
"symbol": "MSC",
|
||||
"image": "https://shapeshift.io/images/coins/mastercoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"NBT": {
|
||||
"name": "Nubits",
|
||||
"symbol": "NBT",
|
||||
"image": "https://shapeshift.io/images/coins/nubits.png",
|
||||
"status": "available"
|
||||
},
|
||||
"NMC": {
|
||||
"name": "Namecoin",
|
||||
"symbol": "NMC",
|
||||
"image": "https://shapeshift.io/images/coins/namecoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"NVC": {
|
||||
"name": "Novacoin",
|
||||
"symbol": "NVC",
|
||||
"image": "https://shapeshift.io/images/coins/novacoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"NXT": {
|
||||
"name": "Nxt",
|
||||
"symbol": "NXT",
|
||||
"specialReturn": false,
|
||||
"specialOutgoing": true,
|
||||
"specialIncoming": true,
|
||||
"specialIncomingStatus": false,
|
||||
"fieldName": "Public Key (only for unfunded accounts!)",
|
||||
"fieldKey": "rsAddress",
|
||||
"image": "https://shapeshift.io/images/coins/nxt.png",
|
||||
"status": "available"
|
||||
},
|
||||
"PPC": {
|
||||
"name": "Peercoin",
|
||||
"symbol": "PPC",
|
||||
"image": "https://shapeshift.io/images/coins/peercoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"RDD": {
|
||||
"name": "Reddcoin",
|
||||
"symbol": "RDD",
|
||||
"image": "https://shapeshift.io/images/coins/reddcoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"SDC": {
|
||||
"name": "Shadowcash",
|
||||
"symbol": "SDC",
|
||||
"image": "https://shapeshift.io/images/coins/shadowcash.png",
|
||||
"status": "available"
|
||||
},
|
||||
"SC": {
|
||||
"name": "Siacoin",
|
||||
"symbol": "SC",
|
||||
"image": "https://shapeshift.io/images/coins/siacoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"SJCX": {
|
||||
"name": "StorjX",
|
||||
"symbol": "SJCX",
|
||||
"image": "https://shapeshift.io/images/coins/storjcoinx.png",
|
||||
"status": "available"
|
||||
},
|
||||
"START": {
|
||||
"name": "Startcoin",
|
||||
"symbol": "START",
|
||||
"image": "https://shapeshift.io/images/coins/startcoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"STEEM": {
|
||||
"name": "Steem",
|
||||
"symbol": "STEEM",
|
||||
"specialReturn": false,
|
||||
"specialOutgoing": true,
|
||||
"specialIncoming": true,
|
||||
"fieldName": "destTag",
|
||||
"fieldKey": "destTag",
|
||||
"image": "https://shapeshift.io/images/coins/steem.png",
|
||||
"status": "available"
|
||||
},
|
||||
"USDT": {
|
||||
"name": "Tether",
|
||||
"symbol": "USDT",
|
||||
"image": "https://shapeshift.io/images/coins/tether.png",
|
||||
"status": "available"
|
||||
},
|
||||
"VOX": {
|
||||
"name": "Voxels",
|
||||
"symbol": "VOX",
|
||||
"image": "https://shapeshift.io/images/coins/voxels.png",
|
||||
"status": "available"
|
||||
},
|
||||
"VRC": {
|
||||
"name": "Vericoin",
|
||||
"symbol": "VRC",
|
||||
"image": "https://shapeshift.io/images/coins/vericoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"VTC": {
|
||||
"name": "Vertcoin",
|
||||
"symbol": "VTC",
|
||||
"image": "https://shapeshift.io/images/coins/vertcoin.png",
|
||||
"status": "available"
|
||||
},
|
||||
"XCP": {
|
||||
"name": "Counterparty",
|
||||
"symbol": "XCP",
|
||||
"image": "https://shapeshift.io/images/coins/counterparty.png",
|
||||
"status": "available"
|
||||
},
|
||||
"XMR": {
|
||||
"name": "Monero",
|
||||
"symbol": "XMR",
|
||||
"specialReturn": false,
|
||||
"specialOutgoing": true,
|
||||
"specialIncoming": true,
|
||||
"fieldName": "Payment Id",
|
||||
"qrName": "tx_payment_id",
|
||||
"fieldKey": "paymentId",
|
||||
"image": "https://shapeshift.io/images/coins/monero.png",
|
||||
"status": "available"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
|
||||
"amount": "5.00",
|
||||
"warning": null
|
||||
},
|
||||
"isSubLoading": false
|
||||
},
|
||||
"identities": {}
|
||||
}
|
28
docs/form_persisting_architecture.md
Normal file
28
docs/form_persisting_architecture.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Form Persisting Architecture
|
||||
|
||||
Since:
|
||||
- The popup is torn down completely on every click outside of it.
|
||||
- We have forms with multiple fields (like passwords & seed phrases) that might encourage a user to leave our panel to refer to a password manager.
|
||||
|
||||
We cause user friction when we lose the contents of certain forms.
|
||||
|
||||
This calls for an architecture of a form component that can completely persist its values to LocalStorage on every relevant change, and restore those values on reopening.
|
||||
|
||||
To achieve this, we have defined a class, a subclass of `React.Component`, called `PersistentForm`, and it's stored at `ui/lib/persistent-form.js`.
|
||||
|
||||
To use this class, simply take your form component (the component that renders `input`, `select`, or `textarea` elements), and make it subclass from `PersistentForm` instead of `React.Component`.
|
||||
|
||||
You can see an example of this in use in `ui/app/first-time/restore-vault.js`.
|
||||
|
||||
Additionally, any field whose value should be persisted, should have a `persistentFormId` attribute, which needs to be assigned under a `dataset` key on the main `attributes` hash. For example:
|
||||
|
||||
```javascript
|
||||
return h('textarea.twelve-word-phrase.letter-spacey', {
|
||||
dataset: {
|
||||
persistentFormId: 'wallet-seed',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
That's it! This field should be persisted to `localStorage` on each `keyUp`, those values should be restored on view load, and the cached values should be cleared when navigating deliberately away from the form.
|
||||
|
@ -98,6 +98,11 @@ App.prototype.render = function () {
|
||||
}
|
||||
|
||||
App.prototype.renderAppBar = function () {
|
||||
|
||||
if (window.METAMASK_UI_TYPE === 'notification') {
|
||||
return null
|
||||
}
|
||||
|
||||
const props = this.props
|
||||
const state = this.state || {}
|
||||
const isNetworkMenuOpen = state.isNetworkMenuOpen || false
|
||||
|
@ -1,4 +1,4 @@
|
||||
const Component = require('react').Component
|
||||
const PersistentForm = require('../../lib/persistent-form')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
@ -17,12 +17,15 @@ function mapStateToProps(state) {
|
||||
}
|
||||
}
|
||||
|
||||
inherits(ShapeshiftForm, Component)
|
||||
inherits(ShapeshiftForm, PersistentForm)
|
||||
|
||||
function ShapeshiftForm () {
|
||||
Component.call(this)
|
||||
PersistentForm.call(this)
|
||||
this.persistentFormParentId = 'shapeshift-buy-form'
|
||||
}
|
||||
|
||||
ShapeshiftForm.prototype.render = function () {
|
||||
|
||||
return h(ReactCSSTransitionGroup, {
|
||||
className: 'css-transition-group',
|
||||
transitionName: 'main',
|
||||
@ -66,6 +69,9 @@ ShapeshiftForm.prototype.renderMain = function () {
|
||||
h('input#fromCoin.buy-inputs.ex-coins', {
|
||||
type: 'text',
|
||||
list: 'coinList',
|
||||
dataset: {
|
||||
persistentFormId: 'input-coin',
|
||||
},
|
||||
style: {
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
@ -159,6 +165,9 @@ ShapeshiftForm.prototype.renderMain = function () {
|
||||
h('input#fromCoinAddress.buy-inputs', {
|
||||
type: 'text',
|
||||
placeholder: `Your ${coin} Refund Address`,
|
||||
dataset: {
|
||||
persistentFormId: 'refund-address',
|
||||
},
|
||||
style: {
|
||||
boxSizing: 'border-box',
|
||||
width: '278px',
|
||||
|
@ -5,6 +5,7 @@ const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('./actions')
|
||||
const txHelper = require('../lib/tx-helper')
|
||||
const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification')
|
||||
|
||||
const PendingTx = require('./components/pending-tx')
|
||||
const PendingMsg = require('./components/pending-msg')
|
||||
@ -36,6 +37,7 @@ ConfirmTxScreen.prototype.render = function () {
|
||||
var unconfTxList = txHelper(unconfTxs, unconfMsgs)
|
||||
var index = state.index !== undefined ? state.index : 0
|
||||
var txData = unconfTxList[index] || unconfTxList[0] || {}
|
||||
var isNotification = isPopupOrNotification() === 'notification'
|
||||
|
||||
return (
|
||||
|
||||
@ -43,9 +45,9 @@ ConfirmTxScreen.prototype.render = function () {
|
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [
|
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
|
||||
!isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
|
||||
onClick: this.goHome.bind(this),
|
||||
}),
|
||||
}) : null,
|
||||
h('h2.page-subtitle', 'Confirm Transaction'),
|
||||
]),
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const PersistentForm = require('../../lib/persistent-form')
|
||||
const connect = require('react-redux').connect
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('../actions')
|
||||
|
||||
module.exports = connect(mapStateToProps)(RestoreVaultScreen)
|
||||
|
||||
inherits(RestoreVaultScreen, Component)
|
||||
inherits(RestoreVaultScreen, PersistentForm)
|
||||
function RestoreVaultScreen () {
|
||||
Component.call(this)
|
||||
PersistentForm.call(this)
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
@ -19,6 +19,8 @@ function mapStateToProps (state) {
|
||||
|
||||
RestoreVaultScreen.prototype.render = function () {
|
||||
var state = this.props
|
||||
this.persistentFormParentId = 'restore-vault-form'
|
||||
|
||||
return (
|
||||
|
||||
h('.initialize-screen.flex-column.flex-center.flex-grow', [
|
||||
@ -39,6 +41,9 @@ RestoreVaultScreen.prototype.render = function () {
|
||||
// wallet seed entry
|
||||
h('h3', 'Wallet Seed'),
|
||||
h('textarea.twelve-word-phrase.letter-spacey', {
|
||||
dataset: {
|
||||
persistentFormId: 'wallet-seed',
|
||||
},
|
||||
placeholder: 'Enter your secret twelve word phrase here to restore your vault.',
|
||||
}),
|
||||
|
||||
@ -47,6 +52,9 @@ RestoreVaultScreen.prototype.render = function () {
|
||||
type: 'password',
|
||||
id: 'password-box',
|
||||
placeholder: 'New Password (min 8 chars)',
|
||||
dataset: {
|
||||
persistentFormId: 'password',
|
||||
},
|
||||
style: {
|
||||
width: 260,
|
||||
marginTop: 12,
|
||||
@ -59,6 +67,9 @@ RestoreVaultScreen.prototype.render = function () {
|
||||
id: 'password-box-confirm',
|
||||
placeholder: 'Confirm Password',
|
||||
onKeyPress: this.onMaybeCreate.bind(this),
|
||||
dataset: {
|
||||
persistentFormId: 'password-confirmation',
|
||||
},
|
||||
style: {
|
||||
width: 260,
|
||||
marginTop: 16,
|
||||
|
@ -1,6 +1,7 @@
|
||||
const extend = require('xtend')
|
||||
const actions = require('../actions')
|
||||
const txHelper = require('../../lib/tx-helper')
|
||||
const notification = require('../../../app/scripts/lib/notifications')
|
||||
|
||||
module.exports = reduceApp
|
||||
|
||||
@ -271,6 +272,9 @@ function reduceApp (state, action) {
|
||||
warning: null,
|
||||
})
|
||||
} else {
|
||||
|
||||
notification.closePopup()
|
||||
|
||||
return extend(appState, {
|
||||
transForward: false,
|
||||
warning: null,
|
||||
@ -536,4 +540,3 @@ function indexForPending (state, txId) {
|
||||
return idx
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const PersistentForm = require('../lib/persistent-form')
|
||||
const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const Identicon = require('./components/identicon')
|
||||
@ -29,12 +29,14 @@ function mapStateToProps (state) {
|
||||
return result
|
||||
}
|
||||
|
||||
inherits(SendTransactionScreen, Component)
|
||||
inherits(SendTransactionScreen, PersistentForm)
|
||||
function SendTransactionScreen () {
|
||||
Component.call(this)
|
||||
PersistentForm.call(this)
|
||||
}
|
||||
|
||||
SendTransactionScreen.prototype.render = function () {
|
||||
this.persistentFormParentId = 'send-tx-form'
|
||||
|
||||
var state = this.props
|
||||
var address = state.address
|
||||
var account = state.account
|
||||
@ -137,6 +139,9 @@ SendTransactionScreen.prototype.render = function () {
|
||||
h('input.large-input', {
|
||||
name: 'address',
|
||||
placeholder: 'Recipient Address',
|
||||
dataset: {
|
||||
persistentFormId: 'recipient-address',
|
||||
},
|
||||
}),
|
||||
]),
|
||||
|
||||
@ -150,6 +155,9 @@ SendTransactionScreen.prototype.render = function () {
|
||||
style: {
|
||||
marginRight: 6,
|
||||
},
|
||||
dataset: {
|
||||
persistentFormId: 'tx-amount',
|
||||
},
|
||||
}),
|
||||
|
||||
h('button.primary', {
|
||||
@ -185,11 +193,12 @@ SendTransactionScreen.prototype.render = function () {
|
||||
width: '100%',
|
||||
resize: 'none',
|
||||
},
|
||||
dataset: {
|
||||
persistentFormId: 'tx-data',
|
||||
},
|
||||
}),
|
||||
]),
|
||||
|
||||
])
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
57
ui/lib/persistent-form.js
Normal file
57
ui/lib/persistent-form.js
Normal file
@ -0,0 +1,57 @@
|
||||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const defaultKey = 'persistent-form-default'
|
||||
const eventName = 'keyup'
|
||||
|
||||
module.exports = PersistentForm
|
||||
|
||||
function PersistentForm () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
inherits(PersistentForm, Component)
|
||||
|
||||
PersistentForm.prototype.componentDidMount = function () {
|
||||
const fields = document.querySelectorAll('[data-persistent-formid]')
|
||||
const store = this.getPersistentStore()
|
||||
fields.forEach((field) => {
|
||||
const key = field.getAttribute('data-persistent-formid')
|
||||
const cached = store[key]
|
||||
if (cached !== undefined) {
|
||||
field.value = cached
|
||||
}
|
||||
|
||||
field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this))
|
||||
})
|
||||
}
|
||||
|
||||
PersistentForm.prototype.getPersistentStore = function () {
|
||||
let store = window.localStorage[this.persistentFormParentId || defaultKey]
|
||||
if (store && store !== 'null') {
|
||||
store = JSON.parse(store)
|
||||
} else {
|
||||
store = {}
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
PersistentForm.prototype.setPersistentStore = function (newStore) {
|
||||
window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore)
|
||||
}
|
||||
|
||||
PersistentForm.prototype.persistentFieldDidUpdate = function (event) {
|
||||
const field = event.target
|
||||
const store = this.getPersistentStore()
|
||||
const key = field.getAttribute('data-persistent-formid')
|
||||
const val = field.value
|
||||
store[key] = val
|
||||
this.setPersistentStore(store)
|
||||
}
|
||||
|
||||
PersistentForm.prototype.componentWillUnmount = function () {
|
||||
const fields = document.querySelectorAll('[data-persistent-formid]')
|
||||
fields.forEach((field) => {
|
||||
field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this))
|
||||
})
|
||||
this.setPersistentStore({})
|
||||
}
|
Loading…
Reference in New Issue
Block a user