1
0
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:
Frankie 2016-08-25 16:06:54 -07:00
commit 547894ed39
20 changed files with 699 additions and 196 deletions

View File

@ -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
View 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>

View File

@ -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 () {}

View File

@ -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) {}

View File

@ -0,0 +1,8 @@
module.exports = function isPopupOrNotification() {
const url = window.location.href
if (url.match(/popup.html$/)) {
return 'popup'
} else {
return 'notification'
}
}

View File

@ -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)
})
}

View File

@ -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()
}
}

View File

@ -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

View 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": {}
}

View 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": {}
}

View 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": {}
}

View 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.

View File

@ -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

View File

@ -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',

View File

@ -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'),
]),

View File

@ -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,

View File

@ -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
}

View File

@ -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
View 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({})
}