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') |
const extension = require('./extension') |
||||||
var notificationHandlers = {} |
|
||||||
|
|
||||||
const notifications = { |
const notifications = { |
||||||
createUnlockRequestNotification: createUnlockRequestNotification, |
show, |
||||||
createTxNotification: createTxNotification, |
getPopup, |
||||||
createMsgNotification: createMsgNotification, |
closePopup, |
||||||
} |
} |
||||||
module.exports = notifications |
module.exports = notifications |
||||||
window.METAMASK_NOTIFIER = notifications |
window.METAMASK_NOTIFIER = notifications |
||||||
|
|
||||||
setupListeners() |
function show () { |
||||||
|
getPopup((popup) => { |
||||||
function setupListeners () { |
if (popup) { |
||||||
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
|
return extension.windows.update(popup.id, { focused: true }) |
||||||
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() |
|
||||||
} |
} |
||||||
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, { |
extension.windows.create({ |
||||||
title: 'New Unsigned Message', |
url: 'notification.html', |
||||||
imageUrl: toSvgUri(notificationSvgSource), |
type: 'detached_panel', |
||||||
})) |
focused: true, |
||||||
|
width: 360, |
||||||
|
height: 500, |
||||||
|
}) |
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|
||||||
function showNotification (state) { |
function getPopup(cb) { |
||||||
// 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() |
// Ignore in test environment
|
||||||
extension.notifications.create(id, { |
if (!extension.windows) { |
||||||
type: 'image', |
return cb(null) |
||||||
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, |
|
||||||
} |
} |
||||||
} |
|
||||||
|
|
||||||
function renderTxNotificationSVG (state, cb) { |
extension.windows.getAll({}, (windows) => { |
||||||
var content = h(PendingTxDetails, state) |
let popup = windows.find((win) => { |
||||||
renderNotificationSVG(content, cb) |
return win.type === 'popup' |
||||||
} |
}) |
||||||
|
|
||||||
function renderMsgNotificationSVG (state, cb) { |
cb(popup) |
||||||
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) |
|
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|
||||||
function svgWrapper (content) { |
function closePopup() { |
||||||
var wrapperSource = ` |
getPopup((popup) => { |
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="240"> |
if (!popup) return |
||||||
<foreignObject x="0" y="0" width="100%" height="100%"> |
extension.windows.remove(popup.id, console.error) |
||||||
<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) |
|
||||||
} |
} |
||||||
|
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