commit
547894ed39
@ -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> |
@ -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] |
||||
}) |
||||
} |
||||
|
||||
// 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.' |
||||
|
||||
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), |
||||
})) |
||||
extension.windows.create({ |
||||
url: 'notification.html', |
||||
type: 'detached_panel', |
||||
focused: true, |
||||
width: 360, |
||||
height: 500, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
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...') |
||||
function getPopup(cb) { |
||||
|
||||
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) |
||||
}) |
||||
} |
||||
|
File diff suppressed because one or more lines are too long
@ -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": {} |
||||
} |
@ -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": {} |
||||
} |
@ -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": {} |
||||
} |
@ -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. |
||||
|
@ -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