Implementation encrypt/decrypt feature (#7831)
Implement `eth_decrypt` and `eth_getEncryptionPublicKey`. This allows decryption backed by the user's private key. The message decryption uses a confirmation flow similar to the messaging signing flow, where the message to be decrypted is also able to be decrypted inline for the user to read directly before confirming.feature/default_network_editable
parent
cab2f1b769
commit
6f47fece56
@ -0,0 +1,311 @@ |
||||
import EventEmitter from 'events' |
||||
import ObservableStore from 'obs-store' |
||||
import ethUtil from 'ethereumjs-util' |
||||
import { ethErrors } from 'eth-json-rpc-errors' |
||||
import createId from './random-id' |
||||
|
||||
const hexRe = /^[0-9A-Fa-f]+$/g |
||||
import log from 'loglevel' |
||||
|
||||
/** |
||||
* Represents, and contains data about, an 'eth_decrypt' type decryption request. These are created when a |
||||
* decryption for an eth_decrypt call is requested. |
||||
* |
||||
* @typedef {Object} DecryptMessage |
||||
* @property {number} id An id to track and identify the message object |
||||
* @property {Object} msgParams The parameters to pass to the decryptMessage method once the decryption request is |
||||
* approved. |
||||
* @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. |
||||
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the decryption request |
||||
* @property {number} time The epoch time at which the this message was created |
||||
* @property {string} status Indicates whether the decryption request is 'unapproved', 'approved', 'decrypted' or 'rejected' |
||||
* @property {string} type The json-prc decryption method for which a decryption request has been made. A 'Message' will |
||||
* always have a 'eth_decrypt' type. |
||||
* |
||||
*/ |
||||
|
||||
export default class DecryptMessageManager extends EventEmitter { |
||||
/** |
||||
* Controller in charge of managing - storing, adding, removing, updating - DecryptMessage. |
||||
* |
||||
* @typedef {Object} DecryptMessageManager |
||||
* @property {Object} memStore The observable store where DecryptMessage are saved with persistance. |
||||
* @property {Object} memStore.unapprovedDecryptMsgs A collection of all DecryptMessages in the 'unapproved' state |
||||
* @property {number} memStore.unapprovedDecryptMsgCount The count of all DecryptMessages in this.memStore.unapprobedMsgs |
||||
* @property {array} messages Holds all messages that have been created by this DecryptMessageManager |
||||
* |
||||
*/ |
||||
constructor () { |
||||
super() |
||||
this.memStore = new ObservableStore({ |
||||
unapprovedDecryptMsgs: {}, |
||||
unapprovedDecryptMsgCount: 0, |
||||
}) |
||||
this.messages = [] |
||||
} |
||||
|
||||
/** |
||||
* A getter for the number of 'unapproved' DecryptMessages in this.messages |
||||
* |
||||
* @returns {number} The number of 'unapproved' DecryptMessages in this.messages |
||||
* |
||||
*/ |
||||
get unapprovedDecryptMsgCount () { |
||||
return Object.keys(this.getUnapprovedMsgs()).length |
||||
} |
||||
|
||||
/** |
||||
* A getter for the 'unapproved' DecryptMessages in this.messages |
||||
* |
||||
* @returns {Object} An index of DecryptMessage ids to DecryptMessages, for all 'unapproved' DecryptMessages in |
||||
* this.messages |
||||
* |
||||
*/ |
||||
getUnapprovedMsgs () { |
||||
return this.messages.filter((msg) => msg.status === 'unapproved') |
||||
.reduce((result, msg) => { |
||||
result[msg.id] = msg; return result |
||||
}, {}) |
||||
} |
||||
|
||||
/** |
||||
* Creates a new DecryptMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add |
||||
* the new DecryptMessage to this.messages, and to save the unapproved DecryptMessages from that list to |
||||
* this.memStore. |
||||
* |
||||
* @param {Object} msgParams The params for the eth_decrypt call to be made after the message is approved. |
||||
* @param {Object} req (optional) The original request object possibly containing the origin |
||||
* @returns {Promise<Buffer>} The raw decrypted message contents |
||||
* |
||||
*/ |
||||
addUnapprovedMessageAsync (msgParams, req) { |
||||
return new Promise((resolve, reject) => { |
||||
if (!msgParams.from) { |
||||
reject(new Error('MetaMask Message for Decryption: from field is required.')) |
||||
} |
||||
const msgId = this.addUnapprovedMessage(msgParams, req) |
||||
this.once(`${msgId}:finished`, (data) => { |
||||
switch (data.status) { |
||||
case 'decrypted': |
||||
return resolve(data.rawData) |
||||
case 'rejected': |
||||
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message for Decryption: User denied message decryption.')) |
||||
case 'errored': |
||||
return reject(new Error('This message cannot be decrypted')) |
||||
default: |
||||
return reject(new Error(`MetaMask Message for Decryption: Unknown problem: ${JSON.stringify(msgParams)}`)) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Creates a new DecryptMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add |
||||
* the new DecryptMessage to this.messages, and to save the unapproved DecryptMessages from that list to |
||||
* this.memStore. |
||||
* |
||||
* @param {Object} msgParams The params for the eth_decryptMsg call to be made after the message is approved. |
||||
* @param {Object} req (optional) The original request object possibly containing the origin |
||||
* @returns {number} The id of the newly created DecryptMessage. |
||||
* |
||||
*/ |
||||
addUnapprovedMessage (msgParams, req) { |
||||
log.debug(`DecryptMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) |
||||
// add origin from request
|
||||
if (req) { |
||||
msgParams.origin = req.origin |
||||
} |
||||
msgParams.data = this.normalizeMsgData(msgParams.data) |
||||
// create txData obj with parameters and meta data
|
||||
const time = (new Date()).getTime() |
||||
const msgId = createId() |
||||
const msgData = { |
||||
id: msgId, |
||||
msgParams: msgParams, |
||||
time: time, |
||||
status: 'unapproved', |
||||
type: 'eth_decrypt', |
||||
} |
||||
this.addMsg(msgData) |
||||
|
||||
// signal update
|
||||
this.emit('update') |
||||
return msgId |
||||
} |
||||
|
||||
/** |
||||
* Adds a passed DecryptMessage to this.messages, and calls this._saveMsgList() to save the unapproved DecryptMessages from that |
||||
* list to this.memStore. |
||||
* |
||||
* @param {Message} msg The DecryptMessage to add to this.messages |
||||
* |
||||
*/ |
||||
addMsg (msg) { |
||||
this.messages.push(msg) |
||||
this._saveMsgList() |
||||
} |
||||
|
||||
/** |
||||
* Returns a specified DecryptMessage. |
||||
* |
||||
* @param {number} msgId The id of the DecryptMessage to get |
||||
* @returns {DecryptMessage|undefined} The DecryptMessage with the id that matches the passed msgId, or undefined |
||||
* if no DecryptMessage has that id. |
||||
* |
||||
*/ |
||||
getMsg (msgId) { |
||||
return this.messages.find((msg) => msg.id === msgId) |
||||
} |
||||
|
||||
/** |
||||
* Approves a DecryptMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise |
||||
* with the message params modified for proper decryption. |
||||
* |
||||
* @param {Object} msgParams The msgParams to be used when eth_decryptMsg is called, plus data added by MetaMask. |
||||
* @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. |
||||
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed. |
||||
* |
||||
*/ |
||||
approveMessage (msgParams) { |
||||
this.setMsgStatusApproved(msgParams.metamaskId) |
||||
return this.prepMsgForDecryption(msgParams) |
||||
} |
||||
|
||||
/** |
||||
* Sets a DecryptMessage status to 'approved' via a call to this._setMsgStatus. |
||||
* |
||||
* @param {number} msgId The id of the DecryptMessage to approve. |
||||
* |
||||
*/ |
||||
setMsgStatusApproved (msgId) { |
||||
this._setMsgStatus(msgId, 'approved') |
||||
} |
||||
|
||||
/** |
||||
* Sets a DecryptMessage status to 'decrypted' via a call to this._setMsgStatus and updates that DecryptMessage in |
||||
* this.messages by adding the raw decryption data of the decryption request to the DecryptMessage |
||||
* |
||||
* @param {number} msgId The id of the DecryptMessage to decrypt. |
||||
* @param {buffer} rawData The raw data of the message request |
||||
* |
||||
*/ |
||||
setMsgStatusDecrypted (msgId, rawData) { |
||||
const msg = this.getMsg(msgId) |
||||
msg.rawData = rawData |
||||
this._updateMsg(msg) |
||||
this._setMsgStatus(msgId, 'decrypted') |
||||
} |
||||
|
||||
/** |
||||
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams |
||||
* |
||||
* @param {Object} msgParams The msgParams to modify |
||||
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed |
||||
* |
||||
*/ |
||||
prepMsgForDecryption (msgParams) { |
||||
delete msgParams.metamaskId |
||||
return Promise.resolve(msgParams) |
||||
} |
||||
|
||||
/** |
||||
* Sets a DecryptMessage status to 'rejected' via a call to this._setMsgStatus. |
||||
* |
||||
* @param {number} msgId The id of the DecryptMessage to reject. |
||||
* |
||||
*/ |
||||
rejectMsg (msgId) { |
||||
this._setMsgStatus(msgId, 'rejected') |
||||
} |
||||
|
||||
/** |
||||
* Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. |
||||
* |
||||
* @param {number} msgId The id of the TypedMessage to error |
||||
* |
||||
*/ |
||||
errorMessage (msgId, error) { |
||||
const msg = this.getMsg(msgId) |
||||
msg.error = error |
||||
this._updateMsg(msg) |
||||
this._setMsgStatus(msgId, 'errored') |
||||
} |
||||
|
||||
/** |
||||
* Updates the status of a DecryptMessage in this.messages via a call to this._updateMsg |
||||
* |
||||
* @private |
||||
* @param {number} msgId The id of the DecryptMessage to update. |
||||
* @param {string} status The new status of the DecryptMessage. |
||||
* @throws A 'DecryptMessageManager - DecryptMessage not found for id: "${msgId}".' if there is no DecryptMessage |
||||
* in this.messages with an id equal to the passed msgId |
||||
* @fires An event with a name equal to `${msgId}:${status}`. The DecryptMessage is also fired. |
||||
* @fires If status is 'rejected' or 'decrypted', an event with a name equal to `${msgId}:finished` is fired along |
||||
* with the DecryptMessage |
||||
* |
||||
*/ |
||||
_setMsgStatus (msgId, status) { |
||||
const msg = this.getMsg(msgId) |
||||
if (!msg) { |
||||
throw new Error('DecryptMessageManager - Message not found for id: "${msgId}".') |
||||
} |
||||
msg.status = status |
||||
this._updateMsg(msg) |
||||
this.emit(`${msgId}:${status}`, msg) |
||||
if (status === 'rejected' || status === 'decrypted' || status === 'errored') { |
||||
this.emit(`${msgId}:finished`, msg) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets a DecryptMessage in this.messages to the passed DecryptMessage if the ids are equal. Then saves the |
||||
* unapprovedDecryptMsgs index to storage via this._saveMsgList |
||||
* |
||||
* @private |
||||
* @param {msg} DecryptMessage A DecryptMessage that will replace an existing DecryptMessage (with the same |
||||
* id) in this.messages |
||||
* |
||||
*/ |
||||
_updateMsg (msg) { |
||||
const index = this.messages.findIndex((message) => message.id === msg.id) |
||||
if (index !== -1) { |
||||
this.messages[index] = msg |
||||
} |
||||
this._saveMsgList() |
||||
} |
||||
|
||||
/** |
||||
* Saves the unapproved DecryptMessages, and their count, to this.memStore |
||||
* |
||||
* @private |
||||
* @fires 'updateBadge' |
||||
* |
||||
*/ |
||||
_saveMsgList () { |
||||
const unapprovedDecryptMsgs = this.getUnapprovedMsgs() |
||||
const unapprovedDecryptMsgCount = Object.keys(unapprovedDecryptMsgs).length |
||||
this.memStore.updateState({ unapprovedDecryptMsgs, unapprovedDecryptMsgCount }) |
||||
this.emit('updateBadge') |
||||
} |
||||
|
||||
/** |
||||
* A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. |
||||
* |
||||
* @param {any} data The buffer data to convert to a hex |
||||
* @returns {string} A hex string conversion of the buffer data |
||||
* |
||||
*/ |
||||
normalizeMsgData (data) { |
||||
try { |
||||
const stripped = ethUtil.stripHexPrefix(data) |
||||
if (stripped.match(hexRe)) { |
||||
return ethUtil.addHexPrefix(stripped) |
||||
} |
||||
} catch (e) { |
||||
log.debug(`Message was not hex encoded, interpreting as utf8.`) |
||||
} |
||||
|
||||
return ethUtil.bufferToHex(Buffer.from(data, 'utf8')) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,286 @@ |
||||
import EventEmitter from 'events' |
||||
import ObservableStore from 'obs-store' |
||||
import { ethErrors } from 'eth-json-rpc-errors' |
||||
import createId from './random-id' |
||||
import log from 'loglevel' |
||||
|
||||
/** |
||||
* Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when |
||||
* an eth_getEncryptionPublicKey call is requested. |
||||
* |
||||
* @typedef {Object} EncryptionPublicKey |
||||
* @property {number} id An id to track and identify the message object |
||||
* @property {Object} msgParams The parameters to pass to the encryptionPublicKey method once the request is |
||||
* approved. |
||||
* @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. |
||||
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the request |
||||
* @property {number} time The epoch time at which the this message was created |
||||
* @property {string} status Indicates whether the request is 'unapproved', 'approved', 'received' or 'rejected' |
||||
* @property {string} type The json-prc method for which a request has been made. A 'Message' will |
||||
* always have a 'eth_getEncryptionPublicKey' type. |
||||
* |
||||
*/ |
||||
|
||||
export default class EncryptionPublicKeyManager extends EventEmitter { |
||||
/** |
||||
* Controller in charge of managing - storing, adding, removing, updating - EncryptionPublicKey. |
||||
* |
||||
* @typedef {Object} EncryptionPublicKeyManager |
||||
* @property {Object} memStore The observable store where EncryptionPublicKey are saved with persistance. |
||||
* @property {Object} memStore.unapprovedEncryptionPublicKeyMsgs A collection of all EncryptionPublicKeys in the 'unapproved' state |
||||
* @property {number} memStore.unapprovedEncryptionPublicKeyMsgCount The count of all EncryptionPublicKeys in this.memStore.unapprobedMsgs |
||||
* @property {array} messages Holds all messages that have been created by this EncryptionPublicKeyManager |
||||
* |
||||
*/ |
||||
constructor () { |
||||
super() |
||||
this.memStore = new ObservableStore({ |
||||
unapprovedEncryptionPublicKeyMsgs: {}, |
||||
unapprovedEncryptionPublicKeyMsgCount: 0, |
||||
}) |
||||
this.messages = [] |
||||
} |
||||
|
||||
/** |
||||
* A getter for the number of 'unapproved' EncryptionPublicKeys in this.messages |
||||
* |
||||
* @returns {number} The number of 'unapproved' EncryptionPublicKeys in this.messages |
||||
* |
||||
*/ |
||||
get unapprovedEncryptionPublicKeyMsgCount () { |
||||
return Object.keys(this.getUnapprovedMsgs()).length |
||||
} |
||||
|
||||
/** |
||||
* A getter for the 'unapproved' EncryptionPublicKeys in this.messages |
||||
* |
||||
* @returns {Object} An index of EncryptionPublicKey ids to EncryptionPublicKeys, for all 'unapproved' EncryptionPublicKeys in |
||||
* this.messages |
||||
* |
||||
*/ |
||||
getUnapprovedMsgs () { |
||||
return this.messages.filter((msg) => msg.status === 'unapproved') |
||||
.reduce((result, msg) => { |
||||
result[msg.id] = msg; return result |
||||
}, {}) |
||||
} |
||||
|
||||
/** |
||||
* Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add |
||||
* the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to |
||||
* this.memStore. |
||||
* |
||||
* @param {Object} address The param for the eth_getEncryptionPublicKey call to be made after the message is approved. |
||||
* @param {Object} req (optional) The original request object possibly containing the origin |
||||
* @returns {Promise<Buffer>} The raw public key contents |
||||
* |
||||
*/ |
||||
addUnapprovedMessageAsync (address, req) { |
||||
return new Promise((resolve, reject) => { |
||||
if (!address) { |
||||
reject(new Error('MetaMask Message for EncryptionPublicKey: address field is required.')) |
||||
} |
||||
const msgId = this.addUnapprovedMessage(address, req) |
||||
this.once(`${msgId}:finished`, (data) => { |
||||
switch (data.status) { |
||||
case 'received': |
||||
return resolve(data.rawData) |
||||
case 'rejected': |
||||
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message for EncryptionPublicKey: User denied message EncryptionPublicKey.')) |
||||
default: |
||||
return reject(new Error(`MetaMask Message for EncryptionPublicKey: Unknown problem: ${JSON.stringify(address)}`)) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add |
||||
* the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to |
||||
* this.memStore. |
||||
* |
||||
* @param {Object} address The param for the eth_getEncryptionPublicKey call to be made after the message is approved. |
||||
* @param {Object} _req (optional) The original request object possibly containing the origin |
||||
* @returns {number} The id of the newly created EncryptionPublicKey. |
||||
* |
||||
*/ |
||||
addUnapprovedMessage (address, _req) { |
||||
log.debug(`EncryptionPublicKeyManager addUnapprovedMessage: address`) |
||||
// create txData obj with parameters and meta data
|
||||
const time = (new Date()).getTime() |
||||
const msgId = createId() |
||||
const msgData = { |
||||
id: msgId, |
||||
msgParams: address, |
||||
time: time, |
||||
status: 'unapproved', |
||||
type: 'eth_getEncryptionPublicKey', |
||||
} |
||||
|
||||
if (_req) { |
||||
msgData.origin = _req.origin |
||||
} |
||||
|
||||
this.addMsg(msgData) |
||||
|
||||
// signal update
|
||||
this.emit('update') |
||||
return msgId |
||||
} |
||||
|
||||
/** |
||||
* Adds a passed EncryptionPublicKey to this.messages, and calls this._saveMsgList() to save the unapproved EncryptionPublicKeys from that |
||||
* list to this.memStore. |
||||
* |
||||
* @param {Message} msg The EncryptionPublicKey to add to this.messages |
||||
* |
||||
*/ |
||||
addMsg (msg) { |
||||
this.messages.push(msg) |
||||
this._saveMsgList() |
||||
} |
||||
|
||||
/** |
||||
* Returns a specified EncryptionPublicKey. |
||||
* |
||||
* @param {number} msgId The id of the EncryptionPublicKey to get |
||||
* @returns {EncryptionPublicKey|undefined} The EncryptionPublicKey with the id that matches the passed msgId, or undefined |
||||
* if no EncryptionPublicKey has that id. |
||||
* |
||||
*/ |
||||
getMsg (msgId) { |
||||
return this.messages.find((msg) => msg.id === msgId) |
||||
} |
||||
|
||||
/** |
||||
* Approves a EncryptionPublicKey. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise |
||||
* with any the message params modified for proper providing. |
||||
* |
||||
* @param {Object} msgParams The msgParams to be used when eth_getEncryptionPublicKey is called, plus data added by MetaMask. |
||||
* @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. |
||||
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed. |
||||
* |
||||
*/ |
||||
approveMessage (msgParams) { |
||||
this.setMsgStatusApproved(msgParams.metamaskId) |
||||
return this.prepMsgForEncryptionPublicKey(msgParams) |
||||
} |
||||
|
||||
/** |
||||
* Sets a EncryptionPublicKey status to 'approved' via a call to this._setMsgStatus. |
||||
* |
||||
* @param {number} msgId The id of the EncryptionPublicKey to approve. |
||||
* |
||||
*/ |
||||
setMsgStatusApproved (msgId) { |
||||
this._setMsgStatus(msgId, 'approved') |
||||
} |
||||
|
||||
/** |
||||
* Sets a EncryptionPublicKey status to 'received' via a call to this._setMsgStatus and updates that EncryptionPublicKey in |
||||
* this.messages by adding the raw data of request to the EncryptionPublicKey |
||||
* |
||||
* @param {number} msgId The id of the EncryptionPublicKey. |
||||
* @param {buffer} rawData The raw data of the message request |
||||
* |
||||
*/ |
||||
setMsgStatusReceived (msgId, rawData) { |
||||
const msg = this.getMsg(msgId) |
||||
msg.rawData = rawData |
||||
this._updateMsg(msg) |
||||
this._setMsgStatus(msgId, 'received') |
||||
} |
||||
|
||||
/** |
||||
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams |
||||
* |
||||
* @param {Object} msgParams The msgParams to modify |
||||
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed |
||||
* |
||||
*/ |
||||
prepMsgForEncryptionPublicKey (msgParams) { |
||||
delete msgParams.metamaskId |
||||
return Promise.resolve(msgParams) |
||||
} |
||||
|
||||
/** |
||||
* Sets a EncryptionPublicKey status to 'rejected' via a call to this._setMsgStatus. |
||||
* |
||||
* @param {number} msgId The id of the EncryptionPublicKey to reject. |
||||
* |
||||
*/ |
||||
rejectMsg (msgId) { |
||||
this._setMsgStatus(msgId, 'rejected') |
||||
} |
||||
|
||||
/** |
||||
* Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. |
||||
* |
||||
* @param {number} msgId The id of the TypedMessage to error |
||||
* |
||||
*/ |
||||
errorMessage (msgId, error) { |
||||
const msg = this.getMsg(msgId) |
||||
msg.error = error |
||||
this._updateMsg(msg) |
||||
this._setMsgStatus(msgId, 'errored') |
||||
} |
||||
|
||||
/** |
||||
* Updates the status of a EncryptionPublicKey in this.messages via a call to this._updateMsg |
||||
* |
||||
* @private |
||||
* @param {number} msgId The id of the EncryptionPublicKey to update. |
||||
* @param {string} status The new status of the EncryptionPublicKey. |
||||
* @throws A 'EncryptionPublicKeyManager - EncryptionPublicKey not found for id: "${msgId}".' if there is no EncryptionPublicKey |
||||
* in this.messages with an id equal to the passed msgId |
||||
* @fires An event with a name equal to `${msgId}:${status}`. The EncryptionPublicKey is also fired. |
||||
* @fires If status is 'rejected' or 'received', an event with a name equal to `${msgId}:finished` is fired along |
||||
* with the EncryptionPublicKey |
||||
* |
||||
*/ |
||||
_setMsgStatus (msgId, status) { |
||||
const msg = this.getMsg(msgId) |
||||
if (!msg) { |
||||
throw new Error('EncryptionPublicKeyManager - Message not found for id: "${msgId}".') |
||||
} |
||||
msg.status = status |
||||
this._updateMsg(msg) |
||||
this.emit(`${msgId}:${status}`, msg) |
||||
if (status === 'rejected' || status === 'received') { |
||||
this.emit(`${msgId}:finished`, msg) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets a EncryptionPublicKey in this.messages to the passed EncryptionPublicKey if the ids are equal. Then saves the |
||||
* unapprovedEncryptionPublicKeyMsgs index to storage via this._saveMsgList |
||||
* |
||||
* @private |
||||
* @param {msg} EncryptionPublicKey A EncryptionPublicKey that will replace an existing EncryptionPublicKey (with the same |
||||
* id) in this.messages |
||||
* |
||||
*/ |
||||
_updateMsg (msg) { |
||||
const index = this.messages.findIndex((message) => message.id === msg.id) |
||||
if (index !== -1) { |
||||
this.messages[index] = msg |
||||
} |
||||
this._saveMsgList() |
||||
} |
||||
|
||||
/** |
||||
* Saves the unapproved EncryptionPublicKeys, and their count, to this.memStore |
||||
* |
||||
* @private |
||||
* @fires 'updateBadge' |
||||
* |
||||
*/ |
||||
_saveMsgList () { |
||||
const unapprovedEncryptionPublicKeyMsgs = this.getUnapprovedMsgs() |
||||
const unapprovedEncryptionPublicKeyMsgCount = Object.keys(unapprovedEncryptionPublicKeyMsgs).length |
||||
this.memStore.updateState({ unapprovedEncryptionPublicKeyMsgs, unapprovedEncryptionPublicKeyMsgCount }) |
||||
this.emit('updateBadge') |
||||
} |
||||
|
||||
} |
@ -0,0 +1,293 @@ |
||||
.request-decrypt-message { |
||||
&__container { |
||||
width: 380px; |
||||
border-radius: 8px; |
||||
background-color: $white; |
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
z-index: 25; |
||||
align-items: center; |
||||
font-family: Roboto; |
||||
position: relative; |
||||
height: 100%; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
width: 100%; |
||||
top: 0; |
||||
box-shadow: none; |
||||
} |
||||
|
||||
@media screen and (min-width: $break-large) { |
||||
height: 620px; |
||||
} |
||||
} |
||||
|
||||
&__typed-container { |
||||
padding: 17px; |
||||
|
||||
h1 { |
||||
font-weight: 900; |
||||
margin-bottom: 5px; |
||||
} |
||||
|
||||
* { |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
> div { |
||||
margin-bottom: 10px; |
||||
} |
||||
} |
||||
|
||||
&__header { |
||||
height: 64px; |
||||
width: 100%; |
||||
position: relative; |
||||
display: flex; |
||||
flex-flow: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
&__header-background { |
||||
position: absolute; |
||||
background-color: $athens-grey; |
||||
z-index: 2; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
&__header__text { |
||||
color: #5B5D67; |
||||
font-family: Roboto; |
||||
font-size: 22px; |
||||
line-height: 29px; |
||||
z-index: 3; |
||||
text-align: center; |
||||
} |
||||
|
||||
&__header__tip-container { |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
} |
||||
|
||||
&__header__tip { |
||||
height: 25px; |
||||
width: 25px; |
||||
background: $athens-grey; |
||||
transform: rotate(45deg); |
||||
position: absolute; |
||||
bottom: -8px; |
||||
z-index: 1; |
||||
} |
||||
|
||||
&__account-info { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
margin-top: 18px; |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
&__account { |
||||
color: $dusty-gray; |
||||
margin-left: 17px; |
||||
} |
||||
|
||||
&__account-text { |
||||
font-size: 14px; |
||||
} |
||||
|
||||
&__account-item { |
||||
height: 22px; |
||||
background-color: $white; |
||||
font-family: Roboto; |
||||
line-height: 16px; |
||||
font-size: 12px; |
||||
width: 124px; |
||||
|
||||
.account-list-item { |
||||
margin-top: 6px; |
||||
} |
||||
|
||||
.account-list-item__account-name { |
||||
text-overflow: ellipsis; |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
width: 80px; |
||||
} |
||||
|
||||
.account-list-item__top-row { |
||||
margin: 0; |
||||
} |
||||
} |
||||
|
||||
&__balance { |
||||
color: $dusty-gray; |
||||
margin-right: 17px; |
||||
width: 124px; |
||||
} |
||||
|
||||
&__balance-text { |
||||
text-align: right; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
&__balance-value { |
||||
text-align: right; |
||||
margin-top: 2.5px; |
||||
} |
||||
|
||||
&__request-icon { |
||||
margin-top: 25px; |
||||
} |
||||
|
||||
&__body { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-flow: column; |
||||
flex: 1 1 auto; |
||||
height: 0; |
||||
} |
||||
|
||||
&__notice { |
||||
font-family: "Avenir Next"; |
||||
font-size: 14px; |
||||
line-height: 19px; |
||||
text-align: center; |
||||
margin-top: 15px; |
||||
margin-bottom: 11px; |
||||
width: 100%; |
||||
} |
||||
|
||||
&__message { |
||||
overflow-wrap: break-word; |
||||
margin: 20px; |
||||
overflow: hidden; |
||||
border: 1px solid #dedede; |
||||
padding: 5px; |
||||
border-radius: 5px; |
||||
position: relative; |
||||
|
||||
&-text { |
||||
font-size: 0.7em; |
||||
height: 115px; |
||||
} |
||||
|
||||
&-cover { |
||||
background-color: white; |
||||
opacity: 0.75; |
||||
position: absolute; |
||||
height: 100%; |
||||
width: 100%; |
||||
top: 0px; |
||||
} |
||||
|
||||
&-lock { |
||||
position: absolute; |
||||
height: 100%; |
||||
width: 100%; |
||||
top: 0px; |
||||
cursor: pointer; |
||||
img { |
||||
padding: 5px; |
||||
background-color: #fff; |
||||
left: calc(50% - 24px); |
||||
position: absolute; |
||||
top: calc(50% - 34px); |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
&--pressed { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
&-lock-text { |
||||
width: 200px; |
||||
font-size: 0.75em; |
||||
position: absolute; |
||||
top: calc(50% + 5px); |
||||
text-align: center; |
||||
left: calc(50% - 100px); |
||||
background-color: white; |
||||
line-height: 1em; |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
&-copy { |
||||
justify-content: space-evenly; |
||||
font-size: 0.75em; |
||||
margin-left: 20px; |
||||
margin-right: 20px; |
||||
display: flex; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&-copy-text { |
||||
margin-right: 10px; |
||||
display: inline; |
||||
} |
||||
|
||||
&-copy-tooltip { |
||||
float: right; |
||||
} |
||||
} |
||||
|
||||
&__footer { |
||||
width: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
font-size: 22px; |
||||
position: relative; |
||||
flex: 0 0 auto; |
||||
border-top: 1px solid $geyser; |
||||
padding: 1.6rem; |
||||
|
||||
button { |
||||
width: 165px; |
||||
} |
||||
|
||||
&__cancel-button { |
||||
margin-right: 1.2rem; |
||||
} |
||||
} |
||||
|
||||
&__visual { |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: space-evenly; |
||||
position: relative; |
||||
margin: 0 20px; |
||||
|
||||
section { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
flex: 1; |
||||
} |
||||
|
||||
&-identicon { |
||||
width: 48px; |
||||
height: 48px; |
||||
|
||||
&--default { |
||||
background-color: #777A87; |
||||
color: white; |
||||
width: 48px; |
||||
height: 48px; |
||||
border-radius: 24px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
font-weight: bold; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,222 @@ |
||||
.request-encryption-public-key { |
||||
&__container { |
||||
width: 380px; |
||||
border-radius: 8px; |
||||
background-color: $white; |
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
z-index: 25; |
||||
align-items: center; |
||||
font-family: Roboto; |
||||
position: relative; |
||||
height: 100%; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
width: 100%; |
||||
top: 0; |
||||
box-shadow: none; |
||||
} |
||||
|
||||
@media screen and (min-width: $break-large) { |
||||
height: 620px; |
||||
} |
||||
} |
||||
|
||||
&__typed-container { |
||||
padding: 17px; |
||||
|
||||
h1 { |
||||
font-weight: 900; |
||||
margin-bottom: 5px; |
||||
} |
||||
|
||||
* { |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
> div { |
||||
margin-bottom: 10px; |
||||
} |
||||
} |
||||
|
||||
&__header { |
||||
height: 64px; |
||||
width: 100%; |
||||
position: relative; |
||||
display: flex; |
||||
flex-flow: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
&__header-background { |
||||
position: absolute; |
||||
background-color: $athens-grey; |
||||
z-index: 2; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
&__header__text { |
||||
color: #5B5D67; |
||||
font-family: Roboto; |
||||
font-size: 22px; |
||||
line-height: 29px; |
||||
z-index: 3; |
||||
text-align: center; |
||||
} |
||||
|
||||
&__header__tip-container { |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
} |
||||
|
||||
&__header__tip { |
||||
height: 25px; |
||||
width: 25px; |
||||
background: $athens-grey; |
||||
transform: rotate(45deg); |
||||
position: absolute; |
||||
bottom: -8px; |
||||
z-index: 1; |
||||
} |
||||
|
||||
&__account-info { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
margin-top: 18px; |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
&__account { |
||||
color: $dusty-gray; |
||||
margin-left: 17px; |
||||
} |
||||
|
||||
&__account-text { |
||||
font-size: 14px; |
||||
} |
||||
|
||||
&__account-item { |
||||
height: 22px; |
||||
background-color: $white; |
||||
font-family: Roboto; |
||||
line-height: 16px; |
||||
font-size: 12px; |
||||
width: 124px; |
||||
|
||||
.account-list-item { |
||||
margin-top: 6px; |
||||
} |
||||
|
||||
.account-list-item__account-name { |
||||
text-overflow: ellipsis; |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
width: 80px; |
||||
} |
||||
|
||||
.account-list-item__top-row { |
||||
margin: 0; |
||||
} |
||||
} |
||||
|
||||
&__balance { |
||||
color: $dusty-gray; |
||||
margin-right: 17px; |
||||
width: 124px; |
||||
} |
||||
|
||||
&__balance-text { |
||||
text-align: right; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
&__balance-value { |
||||
text-align: right; |
||||
margin-top: 2.5px; |
||||
} |
||||
|
||||
&__request-icon { |
||||
margin-top: 25px; |
||||
} |
||||
|
||||
&__body { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-flow: column; |
||||
flex: 1 1 auto; |
||||
height: 0; |
||||
} |
||||
|
||||
&__notice { |
||||
font-family: "Avenir Next"; |
||||
font-size: 14px; |
||||
line-height: 19px; |
||||
text-align: center; |
||||
margin-top: 41px; |
||||
margin-bottom: 11px; |
||||
width: 100%; |
||||
padding-left: 20px; |
||||
padding-right: 20px; |
||||
color: $dusty-gray; |
||||
} |
||||
|
||||
&__footer { |
||||
width: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
font-size: 22px; |
||||
position: relative; |
||||
flex: 0 0 auto; |
||||
border-top: 1px solid $geyser; |
||||
padding: 1.6rem; |
||||
|
||||
button { |
||||
width: 165px; |
||||
} |
||||
|
||||
&__cancel-button { |
||||
margin-right: 1.2rem; |
||||
} |
||||
} |
||||
|
||||
&__visual { |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: space-evenly; |
||||
position: relative; |
||||
margin: 0 20px; |
||||
|
||||
section { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
flex: 1; |
||||
} |
||||
|
||||
&-identicon { |
||||
width: 48px; |
||||
height: 48px; |
||||
|
||||
&--default { |
||||
background-color: #777A87; |
||||
color: white; |
||||
width: 48px; |
||||
height: 48px; |
||||
border-radius: 24px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
font-weight: bold; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,333 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Tooltip from '../../components/ui/tooltip-v2' |
||||
import copyToClipboard from 'copy-to-clipboard' |
||||
import classnames from 'classnames' |
||||
|
||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums' |
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util' |
||||
import Identicon from '../../components/ui/identicon' |
||||
import AccountListItem from '../send/account-list-item/account-list-item.component' |
||||
import { conversionUtil } from '../../helpers/utils/conversion-util' |
||||
import Button from '../../components/ui/button' |
||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes' |
||||
|
||||
export default class ConfirmDecryptMessage extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func.isRequired, |
||||
metricsEvent: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
static propTypes = { |
||||
balance: PropTypes.string, |
||||
clearConfirmTransaction: PropTypes.func.isRequired, |
||||
cancelDecryptMessage: PropTypes.func.isRequired, |
||||
decryptMessage: PropTypes.func.isRequired, |
||||
decryptMessageInline: PropTypes.func.isRequired, |
||||
conversionRate: PropTypes.number, |
||||
history: PropTypes.object.isRequired, |
||||
requesterAddress: PropTypes.string, |
||||
selectedAccount: PropTypes.object, |
||||
txData: PropTypes.object, |
||||
domainMetadata: PropTypes.object, |
||||
} |
||||
|
||||
state = { |
||||
selectedAccount: this.props.selectedAccount, |
||||
hasCopied: false, |
||||
copyToClipboardPressed: false, |
||||
} |
||||
|
||||
componentDidMount = () => { |
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { |
||||
window.addEventListener('beforeunload', this._beforeUnload) |
||||
} |
||||
} |
||||
|
||||
componentWillUnmount = () => { |
||||
this._removeBeforeUnload() |
||||
} |
||||
|
||||
_beforeUnload = (event) => { |
||||
const { clearConfirmTransaction, cancelDecryptMessage } = this.props |
||||
const { metricsEvent } = this.context |
||||
metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Messages', |
||||
action: 'Decrypt Message Request', |
||||
name: 'Cancel Via Notification Close', |
||||
}, |
||||
}) |
||||
clearConfirmTransaction() |
||||
cancelDecryptMessage(event) |
||||
} |
||||
|
||||
_removeBeforeUnload = () => { |
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { |
||||
window.removeEventListener('beforeunload', this._beforeUnload) |
||||
} |
||||
} |
||||
|
||||
copyMessage = () => { |
||||
copyToClipboard(this.state.rawMessage) |
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Messages', |
||||
action: 'Decrypt Message Copy', |
||||
name: 'Copy', |
||||
}, |
||||
}) |
||||
this.setState({ hasCopied: true }) |
||||
setTimeout(() => this.setState({ hasCopied: false }), 3000) |
||||
} |
||||
|
||||
renderHeader = () => { |
||||
return ( |
||||
<div className="request-decrypt-message__header"> |
||||
<div className="request-decrypt-message__header-background" /> |
||||
|
||||
<div className="request-decrypt-message__header__text"> |
||||
{ this.context.t('decryptRequest') } |
||||
</div> |
||||
|
||||
<div className="request-decrypt-message__header__tip-container"> |
||||
<div className="request-decrypt-message__header__tip" /> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderAccount = () => { |
||||
const { selectedAccount } = this.state |
||||
|
||||
return ( |
||||
<div className="request-decrypt-message__account"> |
||||
<div className="request-decrypt-message__account-text"> |
||||
{ `${this.context.t('account')}:` } |
||||
</div> |
||||
|
||||
<div className="request-decrypt-message__account-item"> |
||||
<AccountListItem |
||||
account={selectedAccount} |
||||
displayBalance={false} |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderBalance = () => { |
||||
const { balance, conversionRate } = this.props |
||||
|
||||
const balanceInEther = conversionUtil(balance, { |
||||
fromNumericBase: 'hex', |
||||
toNumericBase: 'dec', |
||||
fromDenomination: 'WEI', |
||||
numberOfDecimals: 6, |
||||
conversionRate, |
||||
}) |
||||
|
||||
return ( |
||||
<div className="request-decrypt-message__balance"> |
||||
<div className="request-decrypt-message__balance-text"> |
||||
{ `${this.context.t('balance')}:` } |
||||
</div> |
||||
<div className="request-decrypt-message__balance-value"> |
||||
{ `${balanceInEther} ETH` } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderRequestIcon = () => { |
||||
const { requesterAddress } = this.props |
||||
|
||||
return ( |
||||
<div className="request-decrypt-message__request-icon"> |
||||
<Identicon |
||||
diameter={40} |
||||
address={requesterAddress} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderAccountInfo = () => { |
||||
return ( |
||||
<div className="request-decrypt-message__account-info"> |
||||
{ this.renderAccount() } |
||||
{ this.renderRequestIcon() } |
||||
{ this.renderBalance() } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderBody = () => { |
||||
const { txData } = this.props |
||||
|
||||
const origin = this.props.domainMetadata[txData.msgParams.origin] |
||||
const notice = this.context.t('decryptMessageNotice', [origin.name]) |
||||
|
||||
const { |
||||
hasCopied, |
||||
hasDecrypted, |
||||
hasError, |
||||
rawMessage, |
||||
errorMessage, |
||||
copyToClipboardPressed, |
||||
} = this.state |
||||
|
||||
return ( |
||||
<div className="request-decrypt-message__body"> |
||||
{ this.renderAccountInfo() } |
||||
<div |
||||
className="request-decrypt-message__visual" |
||||
> |
||||
<section> |
||||
{origin.icon ? ( |
||||
<img |
||||
className="request-decrypt-message__visual-identicon" |
||||
src={origin.icon} |
||||
/> |
||||
) : ( |
||||
<i className="request-decrypt-message__visual-identicon--default"> |
||||
{origin.name.charAt(0).toUpperCase()} |
||||
</i> |
||||
)} |
||||
<div |
||||
className="request-decrypt-message__notice" |
||||
> |
||||
{ notice } |
||||
</div> |
||||
</section> |
||||
</div> |
||||
<div |
||||
className="request-decrypt-message__message" |
||||
> |
||||
<div |
||||
className="request-decrypt-message__message-text" |
||||
> |
||||
{ !hasDecrypted && !hasError ? txData.msgParams.data : rawMessage } |
||||
{ !hasError ? '' : errorMessage } |
||||
</div> |
||||
<div |
||||
className={classnames({ |
||||
'request-decrypt-message__message-cover': true, |
||||
'request-decrypt-message__message-lock--pressed': hasDecrypted || hasError, |
||||
})} |
||||
> |
||||
</div> |
||||
<div |
||||
className={classnames({ |
||||
'request-decrypt-message__message-lock': true, |
||||
'request-decrypt-message__message-lock--pressed': hasDecrypted || hasError, |
||||
})} |
||||
onClick={(event) => { |
||||
this.props.decryptMessageInline(txData, event).then((result) => { |
||||
if (!result.error) { |
||||
this.setState({ hasDecrypted: true, rawMessage: result.rawData }) |
||||
} else { |
||||
this.setState({ hasError: true, errorMessage: this.context.t('decryptInlineError', [result.error]) }) |
||||
} |
||||
}) |
||||
}} |
||||
> |
||||
<img src="images/lock.svg" /> |
||||
<div |
||||
className="request-decrypt-message__message-lock-text" |
||||
> |
||||
{this.context.t('decryptMetamask')} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{ hasDecrypted ? |
||||
( |
||||
<div |
||||
className={classnames({ |
||||
'request-decrypt-message__message-copy': true, |
||||
'request-decrypt-message__message-copy--pressed': copyToClipboardPressed, |
||||
})} |
||||
onClick={() => this.copyMessage()} |
||||
onMouseDown={() => this.setState({ copyToClipboardPressed: true })} |
||||
onMouseUp={() => this.setState({ copyToClipboardPressed: false })} |
||||
> |
||||
<Tooltip |
||||
position="bottom" |
||||
title={hasCopied ? this.context.t('copiedExclamation') : this.context.t('copyToClipboard')} |
||||
wrapperClassName="request-decrypt-message__message-copy-tooltip" |
||||
> |
||||
<div |
||||
className="request-decrypt-message__message-copy-text" |
||||
> |
||||
{this.context.t('decryptCopy')} |
||||
</div> |
||||
<img src="images/copy-to-clipboard.svg" /> |
||||
</Tooltip> |
||||
</div> |
||||
) |
||||
: |
||||
<div></div> |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderFooter = () => { |
||||
const { txData } = this.props |
||||
|
||||
return ( |
||||
<div className="request-decrypt-message__footer"> |
||||
<Button |
||||
type="default" |
||||
large |
||||
className="request-decrypt-message__footer__cancel-button" |
||||
onClick={async (event) => { |
||||
this._removeBeforeUnload() |
||||
await this.props.cancelDecryptMessage(txData, event) |
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Messages', |
||||
action: 'Decrypt Message Request', |
||||
name: 'Cancel', |
||||
}, |
||||
}) |
||||
this.props.clearConfirmTransaction() |
||||
this.props.history.push(DEFAULT_ROUTE) |
||||
}} |
||||
> |
||||
{ this.context.t('cancel') } |
||||
</Button> |
||||
<Button |
||||
type="secondary" |
||||
large |
||||
className="request-decrypt-message__footer__sign-button" |
||||
onClick={async (event) => { |
||||
this._removeBeforeUnload() |
||||
await this.props.decryptMessage(txData, event) |
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Messages', |
||||
action: 'Decrypt Message Request', |
||||
name: 'Confirm', |
||||
}, |
||||
}) |
||||
this.props.clearConfirmTransaction() |
||||
this.props.history.push(DEFAULT_ROUTE) |
||||
}} |
||||
> |
||||
{ this.context.t('decrypt') } |
||||
</Button> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render = () => { |
||||
return ( |
||||
<div className="request-decrypt-message__container"> |
||||
{ this.renderHeader() } |
||||
{ this.renderBody() } |
||||
{ this.renderFooter() } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { goHome, decryptMsg, cancelDecryptMsg, decryptMsgInline } from '../../store/actions' |
||||
|
||||
import { |
||||
getSelectedAccount, |
||||
getCurrentAccountWithSendEtherInfo, |
||||
getSelectedAddress, |
||||
conversionRateSelector, |
||||
} from '../../selectors/selectors.js' |
||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck' |
||||
import ConfirmDecryptMessage from './confirm-decrypt-message.component' |
||||
|
||||
function mapStateToProps (state) { |
||||
const { confirmTransaction, |
||||
metamask: { domainMetadata = {} }, |
||||
} = state |
||||
|
||||
const { |
||||
txData = {}, |
||||
} = confirmTransaction |
||||
|
||||
return { |
||||
txData: txData, |
||||
domainMetadata: domainMetadata, |
||||
balance: getSelectedAccount(state).balance, |
||||
selectedAccount: getCurrentAccountWithSendEtherInfo(state), |
||||
selectedAddress: getSelectedAddress(state), |
||||
requester: null, |
||||
requesterAddress: null, |
||||
conversionRate: conversionRateSelector(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
goHome: () => dispatch(goHome()), |
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), |
||||
decryptMessage: (msgData, event) => { |
||||
const params = msgData.msgParams |
||||
params.metamaskId = msgData.id |
||||
event.stopPropagation(event) |
||||
return dispatch(decryptMsg(params)) |
||||
}, |
||||
cancelDecryptMessage: (msgData, event) => { |
||||
event.stopPropagation(event) |
||||
return dispatch(cancelDecryptMsg(msgData)) |
||||
}, |
||||
decryptMessageInline: (msgData, event) => { |
||||
const params = msgData.msgParams |
||||
params.metamaskId = msgData.id |
||||
event.stopPropagation(event) |
||||
return dispatch(decryptMsgInline(params)) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(ConfirmDecryptMessage) |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-decrypt-message.container' |
@ -0,0 +1,238 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums' |
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util' |
||||
import Identicon from '../../components/ui/identicon' |
||||
import AccountListItem from '../send/account-list-item/account-list-item.component' |
||||
import { conversionUtil } from '../../helpers/utils/conversion-util' |
||||
import Button from '../../components/ui/button' |
||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes' |
||||
|
||||
export default class ConfirmEncryptionPublicKey extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func.isRequired, |
||||
metricsEvent: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
static propTypes = { |
||||
balance: PropTypes.string, |
||||
clearConfirmTransaction: PropTypes.func.isRequired, |
||||
cancelEncryptionPublicKey: PropTypes.func.isRequired, |
||||
encryptionPublicKey: PropTypes.func.isRequired, |
||||
conversionRate: PropTypes.number, |
||||
history: PropTypes.object.isRequired, |
||||
requesterAddress: PropTypes.string, |
||||
selectedAccount: PropTypes.object, |
||||
txData: PropTypes.object, |
||||
domainMetadata: PropTypes.object, |
||||
} |
||||
|
||||
state = { |
||||
selectedAccount: this.props.selectedAccount, |
||||
} |
||||
|
||||
componentDidMount = () => { |
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { |
||||
window.addEventListener('beforeunload', this._beforeUnload) |
||||
} |
||||
} |
||||
|
||||
componentWillUnmount = () => { |
||||
this._removeBeforeUnload() |
||||
} |
||||
|
||||
_beforeUnload = (event) => { |
||||
const { clearConfirmTransaction, cancelEncryptionPublicKey } = this.props |
||||
const { metricsEvent } = this.context |
||||
metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Messages', |
||||
action: 'Encryption public key Request', |
||||
name: 'Cancel Via Notification Close', |
||||
}, |
||||
}) |
||||
clearConfirmTransaction() |
||||
cancelEncryptionPublicKey(event) |
||||
} |
||||
|
||||
_removeBeforeUnload = () => { |
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { |
||||
window.removeEventListener('beforeunload', this._beforeUnload) |
||||
} |
||||
} |
||||
|
||||
renderHeader = () => { |
||||
return ( |
||||
<div className="request-encryption-public-key__header"> |
||||
<div className="request-encryption-public-key__header-background" /> |
||||
|
||||
<div className="request-encryption-public-key__header__text"> |
||||
{ this.context.t('encryptionPublicKeyRequest') } |
||||
</div> |
||||
|
||||
<div className="request-encryption-public-key__header__tip-container"> |
||||
<div className="request-encryption-public-key__header__tip" /> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderAccount = () => { |
||||
const { selectedAccount } = this.state |
||||
|
||||
return ( |
||||
<div className="request-encryption-public-key__account"> |
||||
<div className="request-encryption-public-key__account-text"> |
||||
{ `${this.context.t('account')}:` } |
||||
</div> |
||||
|
||||
<div className="request-encryption-public-key__account-item"> |
||||
<AccountListItem |
||||
account={selectedAccount} |
||||
displayBalance={false} |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderBalance = () => { |
||||
const { balance, conversionRate } = this.props |
||||
|
||||
const balanceInEther = conversionUtil(balance, { |
||||
fromNumericBase: 'hex', |
||||
toNumericBase: 'dec', |
||||
fromDenomination: 'WEI', |
||||
numberOfDecimals: 6, |
||||
conversionRate, |
||||
}) |
||||
|
||||
return ( |
||||
<div className="request-encryption-public-key__balance"> |
||||
<div className="request-encryption-public-key__balance-text"> |
||||
{ `${this.context.t('balance')}:` } |
||||
</div> |
||||
<div className="request-encryption-public-key__balance-value"> |
||||
{ `${balanceInEther} ETH` } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderRequestIcon = () => { |
||||
const { requesterAddress } = this.props |
||||
|
||||
return ( |
||||
<div className="request-encryption-public-key__request-icon"> |
||||
<Identicon |
||||
diameter={40} |
||||
address={requesterAddress} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderAccountInfo = () => { |
||||
return ( |
||||
<div className="request-encryption-public-key__account-info"> |
||||
{ this.renderAccount() } |
||||
{ this.renderRequestIcon() } |
||||
{ this.renderBalance() } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderBody = () => { |
||||
const { txData } = this.props |
||||
|
||||
const origin = this.props.domainMetadata[txData.origin] |
||||
const notice = this.context.t('encryptionPublicKeyNotice', [origin.name]) |
||||
|
||||
return ( |
||||
<div className="request-encryption-public-key__body"> |
||||
{ this.renderAccountInfo() } |
||||
<div |
||||
className="request-encryption-public-key__visual" |
||||
> |
||||
<section> |
||||
{origin.icon ? ( |
||||
<img |
||||
className="request-encryption-public-key__visual-identicon" |
||||
src={origin.icon} |
||||
/> |
||||
) : ( |
||||
<i className="request-encryption-public-key__visual-identicon--default"> |
||||
{origin.name.charAt(0).toUpperCase()} |
||||
</i> |
||||
)} |
||||
<div |
||||
className="request-encryption-public-key__notice" |
||||
> |
||||
{ notice } |
||||
</div> |
||||
</section> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderFooter = () => { |
||||
const { txData } = this.props |
||||
|
||||
return ( |
||||
<div className="request-encryption-public-key__footer"> |
||||
<Button |
||||
type="default" |
||||
large |
||||
className="request-encryption-public-key__footer__cancel-button" |
||||
onClick={async (event) => { |
||||
this._removeBeforeUnload() |
||||
await this.props.cancelEncryptionPublicKey(txData, event) |
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Messages', |
||||
action: 'Encryption public key Request', |
||||
name: 'Cancel', |
||||
}, |
||||
}) |
||||
this.props.clearConfirmTransaction() |
||||
this.props.history.push(DEFAULT_ROUTE) |
||||
}} |
||||
> |
||||
{ this.context.t('cancel') } |
||||
</Button> |
||||
<Button |
||||
type="secondary" |
||||
large |
||||
className="request-encryption-public-key__footer__sign-button" |
||||
onClick={async (event) => { |
||||
this._removeBeforeUnload() |
||||
await this.props.encryptionPublicKey(txData, event) |
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Messages', |
||||
action: 'Encryption public key Request', |
||||
name: 'Confirm', |
||||
}, |
||||
}) |
||||
this.props.clearConfirmTransaction() |
||||
this.props.history.push(DEFAULT_ROUTE) |
||||
}} |
||||
> |
||||
{ this.context.t('provide') } |
||||
</Button> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render = () => { |
||||
return ( |
||||
<div className="request-encryption-public-key__container"> |
||||
{ this.renderHeader() } |
||||
{ this.renderBody() } |
||||
{ this.renderFooter() } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,55 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { goHome, encryptionPublicKeyMsg, cancelEncryptionPublicKeyMsg } from '../../store/actions' |
||||
|
||||
import { |
||||
getSelectedAccount, |
||||
getCurrentAccountWithSendEtherInfo, |
||||
getSelectedAddress, |
||||
conversionRateSelector, |
||||
} from '../../selectors/selectors.js' |
||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck' |
||||
import ConfirmEncryptionPublicKey from './confirm-encryption-public-key.component' |
||||
|
||||
function mapStateToProps (state) { |
||||
const { confirmTransaction, |
||||
metamask: { domainMetadata = {} }, |
||||
} = state |
||||
|
||||
const { |
||||
txData = {}, |
||||
} = confirmTransaction |
||||
|
||||
return { |
||||
txData: txData, |
||||
domainMetadata: domainMetadata, |
||||
balance: getSelectedAccount(state).balance, |
||||
selectedAccount: getCurrentAccountWithSendEtherInfo(state), |
||||
selectedAddress: getSelectedAddress(state), |
||||
requester: null, |
||||
requesterAddress: null, |
||||
conversionRate: conversionRateSelector(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
goHome: () => dispatch(goHome()), |
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), |
||||
encryptionPublicKey: (msgData, event) => { |
||||
const params = { data: msgData.msgParams, metamaskId: msgData.id } |
||||
event.stopPropagation() |
||||
return dispatch(encryptionPublicKeyMsg(params)) |
||||
}, |
||||
cancelEncryptionPublicKey: (msgData, event) => { |
||||
event.stopPropagation() |
||||
return dispatch(cancelEncryptionPublicKeyMsg(msgData)) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(ConfirmEncryptionPublicKey) |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-encryption-public-key.container' |
Loading…
Reference in new issue