Address book send plus contact list (#6914)
* Style Send Header * Move Send to-row to send view and restyle * Add "Recents" group to select recipient view * Rename SendToRow to AddRecipient * Basic UI and Layout * New ENSInput component * wip - fuzzy search for input * small refactor * Add Dialog * contact list initial * initial error on invalid address * clean up edit * Click to open modal * Create AddToAddressBookModal component * Modal styling and layout * modal i18n * Add to Addressbook * ens wip * ens wip * ENS Resolution * Reset input * Send to explicit address * Happy Path Complete * Add back error checking * Reset send-to when emptying input * Add back warning object * Fix linter * Fix unit test #1 - fix import paths * Remove dead tests * One more to go * Fix all unit tests * add unit test for reducers and actions * test rendering AddRecipient * Add tests for dialog boxes in AddRecipient * Add test for validating * Fix linter * Fix e2e tests * Token send e2e fix * Style View Contact * Style edit-contact * Fix e2e * Fix from-import-beta-ui e2e spec * Make section header say "add recipient” by default * Auto-focus add recipient input * Update placeholder text * Update input title font size * Auto advance to next step if user paste a valid address * Ellipsify address when recipient is selected * Fix app header background color on desktop * Give each form row a margin of 16px * Use .container/.component naming pattern for ens-input * Auto-focus on input when add to addressbook modal is opened; Save on Enter * Fix and add unit test * Fix selectors name in e2e tests * Correct e2e test token amount for address-book-send changes * Adds e2e test for editing a transaction * Delete test/integration/lib/send-new-ui.js * Add tests for amount max button and high value error on send screen to test/e2e/metamask-ui.spec.js * lint and revert to address as object keys * add chainId based on current network to address book entry * fix test * only display contacts for the current network * Improve ENS message when not found on current network * Add error to indicate when network does not support ENS * bump gaba * address book, resolve comments * Move contact-list to its own component * De-duplicate getaddressbook selector and refactor name selection logic in contact-list-tab/ * Use contact-list component in contact-list-tab.component (i.e. in settings) * Improve/fix settings headers for popup and browser views * Lint fixes related to address book updates * Add 'My accounts' page to settings address book * Update add new contact button in settings to match floating circular design * Improve styles of view contact page * Improve styles and labels of the add-contact.component * Further lint fixes related to address book updates * Update unit tests as per address book updates * Ensure that contact list groups are sorted alphabetically * Refactor settings component to use a container for connection to redux; allow display of addressbook name in settings header * Decouple ens-input.component from send context * Add ens resolution to add contact screen in settings * Switching networks when an ens address is shown on send form removes the ens address. * Resolve send screen search for ensAddress to matching address book entry if it exists * Show resolved ens icon and address if exists (settings: add-contact.component) * Make the displayed and copied address in view-contact.component the checksummed address * Default alias state prop in AddToAddressBookModal to empty string * Use keyCode to detect enter key in AddToAddressBookModal * Ensure add-contact component properly updates after QR code detection * Fix display of all recents after clicking 'Load More' in contact list * Fix send screen contact searching after network switching * Code cleanup related to address book changes * Update unit tests for address book changes * Update ENS name not found on network message * Add ens registration error message * Cancel on edit mode takes user back to view screen * Adds support for memo to settings contact list view and edit screens * Modify designs of edit and view contact in popup environment * Update settings content list UX to show split columns in fullscreen and proper internal navigation * Correct background address book API usages in UIfeature/default_network_editable
parent
1fd3dc9ecf
commit
e9c7df28ed
After Width: | Height: | Size: 745 B |
After Width: | Height: | Size: 372 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 883 B |
@ -0,0 +1,288 @@ |
||||
const path = require('path') |
||||
const assert = require('assert') |
||||
const webdriver = require('selenium-webdriver') |
||||
const { By, Key, until } = webdriver |
||||
const { |
||||
delay, |
||||
buildChromeWebDriver, |
||||
buildFirefoxWebdriver, |
||||
installWebExt, |
||||
getExtensionIdChrome, |
||||
getExtensionIdFirefox, |
||||
} = require('./func') |
||||
const { |
||||
checkBrowserForConsoleErrors, |
||||
closeAllWindowHandlesExcept, |
||||
verboseReportOnFailure, |
||||
findElement, |
||||
findElements, |
||||
} = require('./helpers') |
||||
const fetchMockResponses = require('./fetch-mocks.js') |
||||
|
||||
|
||||
describe('Using MetaMask with an existing account', function () { |
||||
let extensionId |
||||
let driver |
||||
|
||||
const testSeedPhrase = 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress' |
||||
const tinyDelayMs = 200 |
||||
const regularDelayMs = 1000 |
||||
const largeDelayMs = regularDelayMs * 2 |
||||
|
||||
this.timeout(0) |
||||
this.bail(true) |
||||
|
||||
before(async function () { |
||||
let extensionUrl |
||||
switch (process.env.SELENIUM_BROWSER) { |
||||
case 'chrome': { |
||||
const extensionPath = path.resolve('dist/chrome') |
||||
driver = buildChromeWebDriver(extensionPath) |
||||
extensionId = await getExtensionIdChrome(driver) |
||||
await delay(regularDelayMs) |
||||
extensionUrl = `chrome-extension://${extensionId}/home.html` |
||||
break |
||||
} |
||||
case 'firefox': { |
||||
const extensionPath = path.resolve('dist/firefox') |
||||
driver = buildFirefoxWebdriver() |
||||
await installWebExt(driver, extensionPath) |
||||
await delay(regularDelayMs) |
||||
extensionId = await getExtensionIdFirefox(driver) |
||||
extensionUrl = `moz-extension://${extensionId}/home.html` |
||||
break |
||||
} |
||||
} |
||||
// Depending on the state of the application built into the above directory (extPath) and the value of
|
||||
// METAMASK_DEBUG we will see different post-install behaviour and possibly some extra windows. Here we
|
||||
// are closing any extraneous windows to reset us to a single window before continuing.
|
||||
const [tab1] = await driver.getAllWindowHandles() |
||||
await closeAllWindowHandlesExcept(driver, [tab1]) |
||||
await driver.switchTo().window(tab1) |
||||
await driver.get(extensionUrl) |
||||
}) |
||||
|
||||
beforeEach(async function () { |
||||
await driver.executeScript( |
||||
'window.origFetch = window.fetch.bind(window);' + |
||||
'window.fetch = ' + |
||||
'(...args) => { ' + |
||||
'if (args[0] === "https://ethgasstation.info/json/ethgasAPI.json") { return ' + |
||||
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasBasic + '\')) }); } else if ' + |
||||
'(args[0] === "https://ethgasstation.info/json/predictTable.json") { return ' + |
||||
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } else if ' + |
||||
'(args[0].match(/chromeextensionmm/)) { return ' + |
||||
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.metametrics + '\')) }); } else if ' + |
||||
'(args[0] === "https://dev.blockscale.net/api/gasexpress.json") { return ' + |
||||
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.gasExpress + '\')) }); } ' + |
||||
'return window.origFetch(...args); };' + |
||||
'function cancelInfuraRequest(requestDetails) {' + |
||||
'console.log("Canceling: " + requestDetails.url);' + |
||||
'return {' + |
||||
'cancel: true' + |
||||
'};' + |
||||
' }' + |
||||
'window.chrome && window.chrome.webRequest && window.chrome.webRequest.onBeforeRequest.addListener(' + |
||||
'cancelInfuraRequest,' + |
||||
'{urls: ["https://*.infura.io/*"]},' + |
||||
'["blocking"]' + |
||||
');' |
||||
) |
||||
}) |
||||
|
||||
afterEach(async function () { |
||||
if (process.env.SELENIUM_BROWSER === 'chrome') { |
||||
const errors = await checkBrowserForConsoleErrors(driver) |
||||
if (errors.length) { |
||||
const errorReports = errors.map(err => err.message) |
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}` |
||||
console.error(new Error(errorMessage)) |
||||
} |
||||
} |
||||
if (this.currentTest.state === 'failed') { |
||||
await verboseReportOnFailure(driver, this.currentTest) |
||||
} |
||||
}) |
||||
|
||||
after(async function () { |
||||
await driver.quit() |
||||
}) |
||||
|
||||
describe('First time flow starting from an existing seed phrase', () => { |
||||
it('clicks the continue button on the welcome screen', async () => { |
||||
await findElement(driver, By.css('.welcome-page__header')) |
||||
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) |
||||
welcomeScreenBtn.click() |
||||
await delay(largeDelayMs) |
||||
}) |
||||
|
||||
it('clicks the "Import Wallet" option', async () => { |
||||
const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Import Wallet')]`)) |
||||
customRpcButton.click() |
||||
await delay(largeDelayMs) |
||||
}) |
||||
|
||||
it('clicks the "No thanks" option on the metametrics opt-in screen', async () => { |
||||
const optOutButton = await findElement(driver, By.css('.btn-default')) |
||||
optOutButton.click() |
||||
await delay(largeDelayMs) |
||||
}) |
||||
|
||||
it('imports a seed phrase', async () => { |
||||
const [seedTextArea] = await findElements(driver, By.css('textarea.first-time-flow__textarea')) |
||||
await seedTextArea.sendKeys(testSeedPhrase) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [password] = await findElements(driver, By.id('password')) |
||||
await password.sendKeys('correct horse battery staple') |
||||
const [confirmPassword] = await findElements(driver, By.id('confirm-password')) |
||||
confirmPassword.sendKeys('correct horse battery staple') |
||||
|
||||
const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox')) |
||||
await tosCheckBox.click() |
||||
|
||||
const [importButton] = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`)) |
||||
await importButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('clicks through the success screen', async () => { |
||||
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) |
||||
const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) |
||||
await doneButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Send ETH from inside MetaMask', () => { |
||||
it('starts a send transaction', async function () { |
||||
const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`)) |
||||
await sendButton.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const inputAddress = await findElement(driver, By.css('input[placeholder="Search, public address (0x), or ENS"]')) |
||||
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') |
||||
|
||||
const recipientRow = await findElement(driver, By.css('.send__select-recipient-wrapper__group-item')) |
||||
await recipientRow.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const inputAmount = await findElement(driver, By.css('.unit-input__input')) |
||||
await inputAmount.sendKeys('1') |
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await findElement(driver, By.css('.advanced-gas-options-btn')) |
||||
await configureGas.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const gasModal = await driver.findElement(By.css('span .modal')) |
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) |
||||
await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a')) |
||||
await delay(50) |
||||
|
||||
|
||||
await gasPriceInput.sendKeys(Key.BACK_SPACE) |
||||
await delay(50) |
||||
await gasPriceInput.sendKeys(Key.BACK_SPACE) |
||||
await delay(50) |
||||
await gasPriceInput.sendKeys('10') |
||||
await delay(50) |
||||
await delay(tinyDelayMs) |
||||
await delay(50) |
||||
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) |
||||
await delay(50) |
||||
|
||||
await gasLimitInput.sendKeys('25000') |
||||
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) |
||||
await save.click() |
||||
await driver.wait(until.stalenessOf(gasModal)) |
||||
await delay(regularDelayMs) |
||||
|
||||
// Continue to next screen
|
||||
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('has correct value and fee on the confirm screen the transaction', async function () { |
||||
const transactionAmounts = await findElements(driver, By.css('.currency-display-component__text')) |
||||
const transactionAmount = transactionAmounts[0] |
||||
assert.equal(await transactionAmount.getText(), '1') |
||||
|
||||
const transactionFee = transactionAmounts[1] |
||||
assert.equal(await transactionFee.getText(), '0.00025') |
||||
}) |
||||
|
||||
it('edits the transaction', async function () { |
||||
const editButton = await findElement(driver, By.css('.confirm-page-container-header__back-button')) |
||||
await editButton.click() |
||||
|
||||
await delay(regularDelayMs) |
||||
|
||||
const inputAmount = await findElement(driver, By.css('.unit-input__input')) |
||||
await inputAmount.sendKeys(Key.chord(Key.CONTROL, 'a')) |
||||
await delay(50) |
||||
await inputAmount.sendKeys(Key.BACK_SPACE) |
||||
await delay(50) |
||||
await inputAmount.sendKeys('2.2') |
||||
|
||||
const configureGas = await findElement(driver, By.css('.advanced-gas-options-btn')) |
||||
await configureGas.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const gasModal = await driver.findElement(By.css('span .modal')) |
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) |
||||
await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a')) |
||||
await delay(50) |
||||
|
||||
await gasPriceInput.sendKeys(Key.BACK_SPACE) |
||||
await delay(50) |
||||
await gasPriceInput.sendKeys(Key.BACK_SPACE) |
||||
await delay(50) |
||||
await gasPriceInput.sendKeys('8') |
||||
await delay(50) |
||||
await delay(tinyDelayMs) |
||||
await delay(50) |
||||
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) |
||||
await delay(50) |
||||
|
||||
await gasLimitInput.sendKeys('100000') |
||||
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) |
||||
await save.click() |
||||
await driver.wait(until.stalenessOf(gasModal)) |
||||
await delay(regularDelayMs) |
||||
|
||||
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('has correct updated value on the confirm screen the transaction', async function () { |
||||
const transactionAmounts = await findElements(driver, By.css('.currency-display-component__text')) |
||||
const transactionAmount = transactionAmounts[0] |
||||
assert.equal(await transactionAmount.getText(), '2.2') |
||||
|
||||
const transactionFee = transactionAmounts[1] |
||||
assert.equal(await transactionFee.getText(), '0.0008') |
||||
}) |
||||
|
||||
it('confirms the transaction', async function () { |
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) |
||||
await confirmButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('finds the transaction in the transactions list', async function () { |
||||
const transactions = await findElements(driver, By.css('.transaction-list-item')) |
||||
assert.equal(transactions.length, 1) |
||||
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) |
||||
assert.equal(txValues.length, 1) |
||||
assert.ok(/-2.2\s*ETH/.test(await txValues[0].getText())) |
||||
}) |
||||
}) |
||||
}) |
@ -1,168 +0,0 @@ |
||||
const reactTriggerChange = require('../../lib/react-trigger-change') |
||||
const { |
||||
timeout, |
||||
queryAsync, |
||||
findAsync, |
||||
} = require('../../lib/util') |
||||
const fetchMockResponses = require('../../e2e/fetch-mocks.js') |
||||
|
||||
QUnit.module('new ui send flow') |
||||
|
||||
QUnit.test('successful send flow', (assert) => { |
||||
const done = assert.async() |
||||
runSendFlowTest(assert).then(done).catch((err) => { |
||||
assert.notOk(err, `Error was thrown: ${err.stack}`) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
global.ethQuery = { |
||||
sendTransaction: () => {}, |
||||
} |
||||
|
||||
global.ethereumProvider = {} |
||||
|
||||
async function runSendFlowTest (assert) { |
||||
const tempFetch = global.fetch |
||||
|
||||
const realFetch = window.fetch.bind(window) |
||||
global.fetch = (...args) => { |
||||
if (args[0] === 'https://ethgasstation.info/json/ethgasAPI.json') { |
||||
return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasBasic)) }) |
||||
} else if (args[0] === 'https://ethgasstation.info/json/predictTable.json') { |
||||
return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasPredictTable)) }) |
||||
} else if (args[0] === 'https://dev.blockscale.net/api/gasexpress.json') { |
||||
return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.gasExpress)) }) |
||||
} else if (args[0].match(/chromeextensionmm/)) { |
||||
return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) }) |
||||
} |
||||
return realFetch.fetch(...args) |
||||
} |
||||
|
||||
console.log('*** start runSendFlowTest') |
||||
const selectState = await queryAsync($, 'select') |
||||
selectState.val('send new ui') |
||||
reactTriggerChange(selectState[0]) |
||||
|
||||
const sendScreenButton = await queryAsync($, 'button.btn-secondary.transaction-view-balance__button') |
||||
assert.ok(sendScreenButton[1], 'send screen button present') |
||||
sendScreenButton[1].click() |
||||
|
||||
const sendTitle = await queryAsync($, '.page-container__title') |
||||
assert.equal(sendTitle[0].textContent, 'Send ETH', 'Send screen title is correct') |
||||
|
||||
const sendFromField = await queryAsync($, '.send-v2__form-field') |
||||
assert.ok(sendFromField[0], 'send screen has a from field') |
||||
|
||||
const sendFromFieldItemAddress = await queryAsync($, '.account-list-item__account-name') |
||||
assert.equal(sendFromFieldItemAddress[0].textContent, 'Send Account 2', 'send from field shows correct account name') |
||||
|
||||
const sendToFieldInput = await queryAsync($, '.send-v2__to-autocomplete__input') |
||||
sendToFieldInput[0].focus() |
||||
|
||||
await timeout(1000) |
||||
|
||||
const sendToDropdownList = await queryAsync($, '.send-v2__from-dropdown__list') |
||||
assert.equal(sendToDropdownList.children().length, 5, 'send to dropdown shows all accounts and address book accounts') |
||||
|
||||
sendToDropdownList.children()[2].click() |
||||
|
||||
const sendToAccountAddress = sendToFieldInput.val() |
||||
assert.equal(sendToAccountAddress, '0x2f8D4a878cFA04A6E60D46362f5644DeAb66572D', 'send to dropdown selects the correct address') |
||||
|
||||
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(3)') |
||||
const sendAmountFieldInput = await findAsync(sendAmountField, '.unit-input__input') |
||||
|
||||
const amountMaxButton = await queryAsync($, '.send-v2__amount-max') |
||||
amountMaxButton.click() |
||||
reactTriggerChange(sendAmountField.find('input')[1]) |
||||
assert.equal(sendAmountFieldInput.is(':disabled'), true, 'disabled the send amount input when max mode is on') |
||||
|
||||
const gasPriceButtonGroup = await queryAsync($, '.gas-price-button-group--small') |
||||
const gasPriceButton = await gasPriceButtonGroup.find('button')[0] |
||||
const valueBeforeGasPriceChange = sendAmountFieldInput.prop('value') |
||||
gasPriceButton.click() |
||||
reactTriggerChange(sendAmountField.find('input')[1]) |
||||
|
||||
await timeout(1000) |
||||
|
||||
assert.notEqual(valueBeforeGasPriceChange, sendAmountFieldInput.prop('value'), 'send amount value changes when gas price changes') |
||||
|
||||
amountMaxButton.click() |
||||
reactTriggerChange(sendAmountField.find('input')[1]) |
||||
|
||||
sendAmountField.find('.unit-input').click() |
||||
sendAmountFieldInput.val('5.1') |
||||
reactTriggerChange(sendAmountField.find('input')[1]) |
||||
|
||||
let errorMessage = await queryAsync($, '.send-v2__error') |
||||
assert.equal(errorMessage[0].textContent, 'Insufficient funds.', 'send should render an insufficient fund error message') |
||||
|
||||
sendAmountFieldInput.val('2.0') |
||||
reactTriggerChange(sendAmountFieldInput[0]) |
||||
await timeout() |
||||
errorMessage = $('.send-v2__error') |
||||
assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected') |
||||
|
||||
const sendButton = await queryAsync($, 'button.btn-secondary.btn--large.page-container__footer-button') |
||||
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') |
||||
sendButton[0].click() |
||||
await timeout() |
||||
|
||||
selectState.val('send edit') |
||||
reactTriggerChange(selectState[0]) |
||||
|
||||
const confirmFromName = (await queryAsync($, '.sender-to-recipient__name')).first() |
||||
assert.equal(confirmFromName[0].textContent, 'Send Account 2', 'confirm screen should show correct from name') |
||||
|
||||
const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last() |
||||
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name') |
||||
|
||||
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__secondary') |
||||
const confirmScreenGas = confirmScreenRowFiats[0] |
||||
assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas') |
||||
const confirmScreenTotal = confirmScreenRowFiats[1] |
||||
assert.equal(confirmScreenTotal.textContent, '$2,405.37', 'confirm screen should show correct total') |
||||
|
||||
const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button') |
||||
confirmScreenBackButton[0].click() |
||||
|
||||
const sendToFieldInputInEdit = await queryAsync($, '.send-v2__to-autocomplete__input') |
||||
sendToFieldInputInEdit[0].focus() |
||||
sendToFieldInputInEdit.val('0xd85a4b6a394794842887b8284293d69163007bbb') |
||||
|
||||
const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(3)') |
||||
sendAmountFieldInEdit.find('.unit-input')[0].click() |
||||
|
||||
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.unit-input__input') |
||||
sendAmountFieldInputInEdit.val('1.0') |
||||
reactTriggerChange(sendAmountFieldInputInEdit[0]) |
||||
|
||||
const sendButtonInEdit = await queryAsync($, '.btn-secondary.btn--large.page-container__footer-button') |
||||
assert.equal(sendButtonInEdit[0].textContent, 'Next', 'next button in edit rendered') |
||||
|
||||
selectState.val('send new ui') |
||||
reactTriggerChange(selectState[0]) |
||||
|
||||
const cancelButtonInEdit = await queryAsync($, '.btn-default.btn--large.page-container__footer-button') |
||||
cancelButtonInEdit[0].click() |
||||
|
||||
global.fetch = tempFetch |
||||
// sendButtonInEdit[0].click()
|
||||
|
||||
// // TODO: Need a way to mock background so that we can test correct transition from editing to confirm
|
||||
// selectState.val('confirm new ui')
|
||||
// reactTriggerChange(selectState[0])
|
||||
|
||||
|
||||
// const confirmScreenConfirmButton = await queryAsync($, '.btn-confirm.page-container__footer-button')
|
||||
// console.log(`+++++++++++++++++++++++++++++++= confirmScreenConfirmButton[0]`, confirmScreenConfirmButton[0]);
|
||||
// confirmScreenConfirmButton[0].click()
|
||||
|
||||
// await timeout(10000000)
|
||||
|
||||
// const txView = await queryAsync($, '.tx-view')
|
||||
// console.log(`++++++++++++++++++++++++++++++++ txView[0]`, txView[0]);
|
||||
|
||||
// assert.ok(txView[0], 'Should return to the account details screen after confirming')
|
||||
} |
@ -0,0 +1,114 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import RecipientGroup from './recipient-group/recipient-group.component' |
||||
|
||||
export default class ContactList extends PureComponent { |
||||
static propTypes = { |
||||
searchForContacts: PropTypes.func, |
||||
searchForRecents: PropTypes.func, |
||||
searchForMyAccounts: PropTypes.func, |
||||
selectRecipient: PropTypes.func, |
||||
children: PropTypes.node, |
||||
selectedAddress: PropTypes.string, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
isShowingAllRecent: false, |
||||
} |
||||
|
||||
renderRecents () { |
||||
const { t } = this.context |
||||
const { isShowingAllRecent } = this.state |
||||
const nonContacts = this.props.searchForRecents() |
||||
|
||||
const showLoadMore = !isShowingAllRecent && nonContacts.length > 2 |
||||
|
||||
return ( |
||||
<div className="send__select-recipient-wrapper__recent-group-wrapper"> |
||||
<RecipientGroup |
||||
label={t('recents')} |
||||
items={showLoadMore ? nonContacts.slice(0, 2) : nonContacts} |
||||
onSelect={this.props.selectRecipient} |
||||
selectedAddress={this.props.selectedAddress} |
||||
/> |
||||
{ |
||||
showLoadMore && ( |
||||
<div |
||||
className="send__select-recipient-wrapper__recent-group-wrapper__load-more" |
||||
onClick={() => this.setState({ isShowingAllRecent: true })} |
||||
> |
||||
{t('loadMore')} |
||||
</div> |
||||
) |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderAddressBook () { |
||||
const contacts = this.props.searchForContacts() |
||||
|
||||
const contactGroups = contacts.reduce((acc, contact) => { |
||||
const firstLetter = contact.name.slice(0, 1).toUpperCase() |
||||
acc[firstLetter] = acc[firstLetter] || [] |
||||
const bucket = acc[firstLetter] |
||||
bucket.push(contact) |
||||
return acc |
||||
}, {}) |
||||
|
||||
return Object |
||||
.entries(contactGroups) |
||||
.sort(([letter1], [letter2]) => { |
||||
if (letter1 > letter2) { |
||||
return 1 |
||||
} else if (letter1 === letter2) { |
||||
return 0 |
||||
} else if (letter1 < letter2) { |
||||
return -1 |
||||
} |
||||
}) |
||||
.map(([letter, groupItems]) => ( |
||||
<RecipientGroup |
||||
key={`${letter}-contract-group`} |
||||
label={letter} |
||||
items={groupItems} |
||||
onSelect={this.props.selectRecipient} |
||||
selectedAddress={this.props.selectedAddress} |
||||
/> |
||||
)) |
||||
} |
||||
|
||||
renderMyAccounts () { |
||||
const myAccounts = this.props.searchForMyAccounts() |
||||
|
||||
return ( |
||||
<RecipientGroup |
||||
items={myAccounts} |
||||
onSelect={this.props.selectRecipient} |
||||
selectedAddress={this.props.selectedAddress} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
children, |
||||
searchForRecents, |
||||
searchForContacts, |
||||
searchForMyAccounts, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<div className="send__select-recipient-wrapper__list"> |
||||
{ children || null } |
||||
{ searchForRecents && this.renderRecents() } |
||||
{ searchForContacts && this.renderAddressBook() } |
||||
{ searchForMyAccounts && this.renderMyAccounts() } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './contact-list.component' |
@ -0,0 +1 @@ |
||||
export { default } from './recipient-group.component' |
@ -0,0 +1,59 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Identicon from '../../../ui/identicon' |
||||
import classnames from 'classnames' |
||||
import { ellipsify } from '../../../../pages/send/send.utils' |
||||
|
||||
function addressesEqual (address1, address2) { |
||||
return String(address1).toLowerCase() === String(address2).toLowerCase() |
||||
} |
||||
|
||||
export default function RecipientGroup ({ label, items, onSelect, selectedAddress }) { |
||||
if (!items || !items.length) { |
||||
return null |
||||
} |
||||
|
||||
return ( |
||||
<div className="send__select-recipient-wrapper__group"> |
||||
{label && <div className="send__select-recipient-wrapper__group-label"> |
||||
{label} |
||||
</div>} |
||||
{ |
||||
items.map(({ address, name }) => ( |
||||
<div |
||||
key={address} |
||||
onClick={() => onSelect(address, name)} |
||||
className={classnames({ |
||||
'send__select-recipient-wrapper__group-item': !addressesEqual(address, selectedAddress), |
||||
'send__select-recipient-wrapper__group-item--selected': addressesEqual(address, selectedAddress), |
||||
})} |
||||
> |
||||
<Identicon address={address} diameter={28} /> |
||||
<div className="send__select-recipient-wrapper__group-item__content"> |
||||
<div className="send__select-recipient-wrapper__group-item__title"> |
||||
{name || ellipsify(address)} |
||||
</div> |
||||
{ |
||||
name && ( |
||||
<div className="send__select-recipient-wrapper__group-item__subtitle"> |
||||
{ellipsify(address)} |
||||
</div> |
||||
) |
||||
} |
||||
</div> |
||||
</div> |
||||
)) |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
RecipientGroup.propTypes = { |
||||
label: PropTypes.string, |
||||
items: PropTypes.arrayOf(PropTypes.shape({ |
||||
address: PropTypes.string, |
||||
name: PropTypes.string, |
||||
})), |
||||
onSelect: PropTypes.func.isRequired, |
||||
selectedAddress: PropTypes.string, |
||||
} |
@ -1,181 +0,0 @@ |
||||
const Component = require('react').Component |
||||
const PropTypes = require('prop-types') |
||||
const h = require('react-hyperscript') |
||||
const inherits = require('util').inherits |
||||
const extend = require('xtend') |
||||
const debounce = require('debounce') |
||||
const copyToClipboard = require('copy-to-clipboard') |
||||
const ENS = require('ethjs-ens') |
||||
const networkMap = require('ethjs-ens/lib/network-map.json') |
||||
const ensRE = /.+\..+$/ |
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' |
||||
const connect = require('react-redux').connect |
||||
const ToAutoComplete = require('../../pages/send/to-autocomplete').default |
||||
const log = require('loglevel') |
||||
const { isValidENSAddress } = require('../../helpers/utils/util') |
||||
|
||||
EnsInput.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
module.exports = connect()(EnsInput) |
||||
|
||||
|
||||
inherits(EnsInput, Component) |
||||
function EnsInput () { |
||||
Component.call(this) |
||||
} |
||||
|
||||
EnsInput.prototype.onChange = function (recipient) { |
||||
|
||||
const network = this.props.network |
||||
const networkHasEnsSupport = getNetworkEnsSupport(network) |
||||
|
||||
this.props.onChange({ toAddress: recipient }) |
||||
|
||||
if (!networkHasEnsSupport) return |
||||
|
||||
if (recipient.match(ensRE) === null) { |
||||
return this.setState({ |
||||
loadingEns: false, |
||||
ensResolution: null, |
||||
ensFailure: null, |
||||
toError: null, |
||||
}) |
||||
} |
||||
|
||||
this.setState({ |
||||
loadingEns: true, |
||||
}) |
||||
this.checkName(recipient) |
||||
} |
||||
|
||||
EnsInput.prototype.render = function () { |
||||
const props = this.props |
||||
const opts = extend(props, { |
||||
list: 'addresses', |
||||
onChange: this.onChange.bind(this), |
||||
qrScanner: true, |
||||
}) |
||||
return h('div', { |
||||
style: { width: '100%', position: 'relative' }, |
||||
}, [ |
||||
h(ToAutoComplete, { ...opts }), |
||||
this.ensIcon(), |
||||
]) |
||||
} |
||||
|
||||
EnsInput.prototype.componentDidMount = function () { |
||||
const network = this.props.network |
||||
const networkHasEnsSupport = getNetworkEnsSupport(network) |
||||
this.setState({ ensResolution: ZERO_ADDRESS }) |
||||
|
||||
if (networkHasEnsSupport) { |
||||
const provider = global.ethereumProvider |
||||
this.ens = new ENS({ provider, network }) |
||||
this.checkName = debounce(this.lookupEnsName.bind(this), 200) |
||||
} |
||||
} |
||||
|
||||
EnsInput.prototype.lookupEnsName = function (recipient) { |
||||
const { ensResolution } = this.state |
||||
|
||||
log.info(`ENS attempting to resolve name: ${recipient}`) |
||||
this.ens.lookup(recipient.trim()) |
||||
.then((address) => { |
||||
if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName')) |
||||
if (address !== ensResolution) { |
||||
this.setState({ |
||||
loadingEns: false, |
||||
ensResolution: address, |
||||
nickname: recipient.trim(), |
||||
hoverText: address + '\n' + this.context.t('clickCopy'), |
||||
ensFailure: false, |
||||
toError: null, |
||||
}) |
||||
} |
||||
}) |
||||
.catch((reason) => { |
||||
const setStateObj = { |
||||
loadingEns: false, |
||||
ensResolution: recipient, |
||||
ensFailure: true, |
||||
toError: null, |
||||
} |
||||
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') { |
||||
setStateObj.hoverText = this.context.t('ensNameNotFound') |
||||
setStateObj.toError = 'ensNameNotFound' |
||||
setStateObj.ensFailure = false |
||||
} else { |
||||
log.error(reason) |
||||
setStateObj.hoverText = reason.message |
||||
} |
||||
|
||||
return this.setState(setStateObj) |
||||
}) |
||||
} |
||||
|
||||
EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { |
||||
const state = this.state || {} |
||||
const ensResolution = state.ensResolution |
||||
// If an address is sent without a nickname, meaning not from ENS or from
|
||||
// the user's own accounts, a default of a one-space string is used.
|
||||
const nickname = state.nickname || ' ' |
||||
if (prevProps.network !== this.props.network) { |
||||
const provider = global.ethereumProvider |
||||
this.ens = new ENS({ provider, network: this.props.network }) |
||||
this.onChange(ensResolution) |
||||
} |
||||
if (prevState && ensResolution && this.props.onChange && |
||||
ensResolution !== prevState.ensResolution) { |
||||
this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning }) |
||||
} |
||||
} |
||||
|
||||
EnsInput.prototype.ensIcon = function (recipient) { |
||||
const { hoverText } = this.state || {} |
||||
return h('span.#ensIcon', { |
||||
title: hoverText, |
||||
style: { |
||||
position: 'absolute', |
||||
top: '16px', |
||||
left: '-25px', |
||||
}, |
||||
}, this.ensIconContents(recipient)) |
||||
} |
||||
|
||||
EnsInput.prototype.ensIconContents = function () { |
||||
const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } |
||||
|
||||
if (toError) return |
||||
|
||||
if (loadingEns) { |
||||
return h('img', { |
||||
src: 'images/loading.svg', |
||||
style: { |
||||
width: '30px', |
||||
height: '30px', |
||||
transform: 'translateY(-6px)', |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
if (ensFailure) { |
||||
return h('i.fa.fa-warning.fa-lg.warning') |
||||
} |
||||
|
||||
if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { |
||||
return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { |
||||
style: { color: 'green' }, |
||||
onClick: (event) => { |
||||
event.preventDefault() |
||||
event.stopPropagation() |
||||
copyToClipboard(ensResolution) |
||||
}, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function getNetworkEnsSupport (network) { |
||||
return Boolean(networkMap[network]) |
||||
} |
@ -0,0 +1,79 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Button from '../../../ui/button/button.component' |
||||
|
||||
export default class AddToAddressBookModal extends Component { |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
hideModal: PropTypes.func.isRequired, |
||||
addToAddressBook: PropTypes.func.isRequired, |
||||
recipient: PropTypes.string.isRequired, |
||||
} |
||||
|
||||
state = { |
||||
alias: '', |
||||
} |
||||
|
||||
onSave = () => { |
||||
const { recipient, addToAddressBook, hideModal } = this.props |
||||
addToAddressBook(recipient, this.state.alias) |
||||
hideModal() |
||||
} |
||||
|
||||
onChange = e => { |
||||
this.setState({ |
||||
alias: e.target.value, |
||||
}) |
||||
} |
||||
|
||||
onKeyPress = e => { |
||||
if (e.keyCode === 13 && this.state.alias) { |
||||
this.onSave() |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
|
||||
return ( |
||||
<div className="add-to-address-book-modal"> |
||||
<div className="add-to-address-book-modal__content"> |
||||
<div className="add-to-address-book-modal__content__header"> |
||||
{t('addToAddressBook')} |
||||
</div> |
||||
<div className="add-to-address-book-modal__input-label"> |
||||
{t('enterAnAlias')} |
||||
</div> |
||||
<input |
||||
type="text" |
||||
className="add-to-address-book-modal__input" |
||||
placeholder={t('addToAddressBookModalPlaceholder')} |
||||
onChange={this.onChange} |
||||
onKeyPress={this.onKeyPress} |
||||
value={this.state.alias} |
||||
autoFocus |
||||
/> |
||||
</div> |
||||
<div className="add-to-address-book-modal__footer"> |
||||
<Button |
||||
type="secondary" |
||||
onClick={this.props.hideModal} |
||||
> |
||||
{t('cancel')} |
||||
</Button> |
||||
<Button |
||||
type="primary" |
||||
onClick={this.onSave} |
||||
disabled={!this.state.alias} |
||||
> |
||||
{t('save')} |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
import { connect } from 'react-redux' |
||||
import AddToAddressBookModal from './add-to-addressbook-modal.component' |
||||
import actions from '../../../../store/actions' |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
...state.appState.modal.modalState.props || {}, |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
hideModal: () => dispatch(actions.hideModal()), |
||||
addToAddressBook: (recipient, nickname) => dispatch(actions.addToAddressBook(recipient, nickname)), |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AddToAddressBookModal) |
@ -0,0 +1 @@ |
||||
export { default } from './add-to-addressbook-modal.container' |
@ -0,0 +1,37 @@ |
||||
.add-to-address-book-modal { |
||||
@extend %col-nowrap; |
||||
@extend %modal; |
||||
|
||||
&__content { |
||||
@extend %col-nowrap; |
||||
padding: 1.5rem; |
||||
border-bottom: 1px solid $Grey-100; |
||||
|
||||
&__header { |
||||
@extend %h3; |
||||
} |
||||
} |
||||
|
||||
&__input-label { |
||||
color: $Grey-600; |
||||
margin-top: 1.25rem; |
||||
} |
||||
|
||||
&__input { |
||||
@extend %input; |
||||
margin-top: 0.75rem; |
||||
|
||||
&::placeholder { |
||||
color: $Grey-300; |
||||
} |
||||
} |
||||
|
||||
&__footer { |
||||
@extend %row-nowrap; |
||||
padding: 1rem; |
||||
|
||||
button + button { |
||||
margin-left: 1rem; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
.dialog { |
||||
font-size: .75rem; |
||||
line-height: 1rem; |
||||
padding: 1rem; |
||||
border: 1px solid $black; |
||||
box-sizing: border-box; |
||||
border-radius: 8px; |
||||
|
||||
&--message { |
||||
border-color: $Blue-200; |
||||
color: $Blue-600; |
||||
background-color: $Blue-000; |
||||
} |
||||
|
||||
&--error { |
||||
border-color: $Red-300; |
||||
color: $Red-600; |
||||
background-color: $Red-000; |
||||
} |
||||
|
||||
&--warning { |
||||
border-color: $Orange-300; |
||||
color: $Orange-600; |
||||
background-color: $Orange-000; |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import c from 'classnames' |
||||
|
||||
export default function Dialog (props) { |
||||
const { children, type, className, onClick } = props |
||||
return ( |
||||
<div |
||||
className={c('dialog', className, { |
||||
'dialog--message': type === 'message', |
||||
'dialog--error': type === 'error', |
||||
'dialog--warning': type === 'warning', |
||||
})} |
||||
onClick={onClick} |
||||
> |
||||
{ children } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
Dialog.propTypes = { |
||||
className: PropTypes.string, |
||||
children: PropTypes.node, |
||||
type: PropTypes.oneOf(['message', 'error', 'warning']), |
||||
onClick: PropTypes.func, |
||||
} |
@ -0,0 +1,243 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Fuse from 'fuse.js' |
||||
import Identicon from '../../../../components/ui/identicon' |
||||
import {isValidAddress} from '../../../../helpers/utils/util' |
||||
import Dialog from '../../../../components/ui/dialog' |
||||
import ContactList from '../../../../components/app/contact-list' |
||||
import RecipientGroup from '../../../../components/app/contact-list/recipient-group/recipient-group.component' |
||||
import {ellipsify} from '../../send.utils' |
||||
|
||||
export default class AddRecipient extends Component { |
||||
|
||||
static propTypes = { |
||||
className: PropTypes.string, |
||||
query: PropTypes.string, |
||||
ownedAccounts: PropTypes.array, |
||||
addressBook: PropTypes.array, |
||||
updateGas: PropTypes.func, |
||||
updateSendTo: PropTypes.func, |
||||
ensResolution: PropTypes.string, |
||||
toError: PropTypes.string, |
||||
toWarning: PropTypes.string, |
||||
ensResolutionError: PropTypes.string, |
||||
selectedToken: PropTypes.object, |
||||
hasHexData: PropTypes.bool, |
||||
tokens: PropTypes.array, |
||||
addressBookEntryName: PropTypes.string, |
||||
contacts: PropTypes.array, |
||||
nonContacts: PropTypes.array, |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
this.recentFuse = new Fuse(props.nonContacts, { |
||||
shouldSort: true, |
||||
threshold: 0.45, |
||||
location: 0, |
||||
distance: 100, |
||||
maxPatternLength: 32, |
||||
minMatchCharLength: 1, |
||||
keys: [ |
||||
{ name: 'address', weight: 0.5 }, |
||||
], |
||||
}) |
||||
|
||||
this.contactFuse = new Fuse(props.contacts, { |
||||
shouldSort: true, |
||||
threshold: 0.45, |
||||
location: 0, |
||||
distance: 100, |
||||
maxPatternLength: 32, |
||||
minMatchCharLength: 1, |
||||
keys: [ |
||||
{ name: 'name', weight: 0.5 }, |
||||
{ name: 'address', weight: 0.5 }, |
||||
], |
||||
}) |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
metricsEvent: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
isShowingTransfer: false, |
||||
isShowingAllRecent: false, |
||||
} |
||||
|
||||
selectRecipient = (to, nickname = '') => { |
||||
const { updateSendTo, updateGas } = this.props |
||||
|
||||
updateSendTo(to, nickname) |
||||
updateGas({ to }) |
||||
} |
||||
|
||||
searchForContacts = () => { |
||||
const { query, contacts } = this.props |
||||
|
||||
let _contacts = contacts |
||||
|
||||
if (query) { |
||||
this.contactFuse.setCollection(contacts) |
||||
_contacts = this.contactFuse.search(query) |
||||
} |
||||
|
||||
return _contacts |
||||
} |
||||
|
||||
searchForRecents = () => { |
||||
const { query, nonContacts } = this.props |
||||
|
||||
let _nonContacts = nonContacts |
||||
|
||||
if (query) { |
||||
this.recentFuse.setCollection(nonContacts) |
||||
_nonContacts = this.recentFuse.search(query) |
||||
} |
||||
|
||||
return _nonContacts |
||||
} |
||||
|
||||
render () { |
||||
const { ensResolution, query, addressBookEntryName } = this.props |
||||
const { isShowingTransfer } = this.state |
||||
|
||||
let content |
||||
|
||||
if (isValidAddress(query)) { |
||||
content = this.renderExplicitAddress(query) |
||||
} else if (ensResolution) { |
||||
content = this.renderExplicitAddress(ensResolution, addressBookEntryName || query) |
||||
} else if (isShowingTransfer) { |
||||
content = this.renderTransfer() |
||||
} |
||||
|
||||
return ( |
||||
<div className="send__select-recipient-wrapper"> |
||||
{ this.renderDialogs() } |
||||
{ content || this.renderMain() } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderExplicitAddress (address, name) { |
||||
return ( |
||||
<div |
||||
key={address} |
||||
className="send__select-recipient-wrapper__group-item" |
||||
onClick={() => this.selectRecipient(address, name)} |
||||
> |
||||
<Identicon address={address} diameter={28} /> |
||||
<div className="send__select-recipient-wrapper__group-item__content"> |
||||
<div className="send__select-recipient-wrapper__group-item__title"> |
||||
{name || ellipsify(address)} |
||||
</div> |
||||
{ |
||||
name && ( |
||||
<div className="send__select-recipient-wrapper__group-item__subtitle"> |
||||
{ellipsify(address)} |
||||
</div> |
||||
) |
||||
} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderTransfer () { |
||||
const { ownedAccounts } = this.props |
||||
const { t } = this.context |
||||
|
||||
return ( |
||||
<div className="send__select-recipient-wrapper__list"> |
||||
<div |
||||
className="send__select-recipient-wrapper__list__link" |
||||
onClick={() => this.setState({ isShowingTransfer: false })} |
||||
> |
||||
<div className="send__select-recipient-wrapper__list__back-caret"/> |
||||
{ t('backToAll') } |
||||
</div> |
||||
<RecipientGroup |
||||
label={t('myAccounts')} |
||||
items={ownedAccounts} |
||||
onSelect={this.selectRecipient} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderMain () { |
||||
const { t } = this.context |
||||
const { query, ownedAccounts = [], addressBook } = this.props |
||||
|
||||
return ( |
||||
<div className="send__select-recipient-wrapper__list"> |
||||
<ContactList |
||||
addressBook={addressBook} |
||||
searchForContacts={this.searchForContacts.bind(this)} |
||||
searchForRecents={this.searchForRecents.bind(this)} |
||||
selectRecipient={this.selectRecipient.bind(this)} |
||||
> |
||||
{ |
||||
(ownedAccounts && ownedAccounts.length > 1) && !query && ( |
||||
<div |
||||
className="send__select-recipient-wrapper__list__link" |
||||
onClick={() => this.setState({ isShowingTransfer: true })} |
||||
> |
||||
{ t('transferBetweenAccounts') } |
||||
</div> |
||||
) |
||||
} |
||||
</ContactList> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderDialogs () { |
||||
const { toError, toWarning, ensResolutionError, ensResolution } = this.props |
||||
const { t } = this.context |
||||
const contacts = this.searchForContacts() |
||||
const recents = this.searchForRecents() |
||||
|
||||
if (contacts.length || recents.length) { |
||||
return null |
||||
} |
||||
|
||||
if (ensResolutionError) { |
||||
return ( |
||||
<Dialog |
||||
type="error" |
||||
className="send__error-dialog" |
||||
> |
||||
{ensResolutionError} |
||||
</Dialog> |
||||
) |
||||
} |
||||
|
||||
if (toError && toError !== 'required' && !ensResolution) { |
||||
return ( |
||||
<Dialog |
||||
type="error" |
||||
className="send__error-dialog" |
||||
> |
||||
{t(toError)} |
||||
</Dialog> |
||||
) |
||||
} |
||||
|
||||
|
||||
if (toWarning) { |
||||
return ( |
||||
<Dialog |
||||
type="warning" |
||||
className="send__error-dialog" |
||||
> |
||||
{t(toWarning)} |
||||
</Dialog> |
||||
) |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,44 @@ |
||||
import { connect } from 'react-redux' |
||||
import { |
||||
accountsWithSendEtherInfoSelector, |
||||
getSendEnsResolution, |
||||
getSendEnsResolutionError, |
||||
} from '../../send.selectors.js' |
||||
import { |
||||
getAddressBook, |
||||
getAddressBookEntry, |
||||
} from '../../../../selectors/selectors' |
||||
import { |
||||
updateSendTo, |
||||
} from '../../../../store/actions' |
||||
import AddRecipient from './add-recipient.component' |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AddRecipient) |
||||
|
||||
function mapStateToProps (state) { |
||||
const ensResolution = getSendEnsResolution(state) |
||||
|
||||
let addressBookEntryName = '' |
||||
if (ensResolution) { |
||||
const addressBookEntry = getAddressBookEntry(state, ensResolution) || {} |
||||
addressBookEntryName = addressBookEntry.name |
||||
} |
||||
|
||||
const addressBook = getAddressBook(state) |
||||
|
||||
return { |
||||
ownedAccounts: accountsWithSendEtherInfoSelector(state), |
||||
addressBook, |
||||
ensResolution, |
||||
addressBookEntryName, |
||||
ensResolutionError: getSendEnsResolutionError(state), |
||||
contacts: addressBook.filter(({ name }) => !!name), |
||||
nonContacts: addressBook.filter(({ name }) => !name), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), |
||||
} |
||||
} |
@ -0,0 +1,268 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import c from 'classnames' |
||||
import { isValidENSAddress, isValidAddress } from '../../../../helpers/utils/util' |
||||
import {ellipsify} from '../../send.utils' |
||||
|
||||
import debounce from 'debounce' |
||||
import copyToClipboard from 'copy-to-clipboard/index' |
||||
import ENS from 'ethjs-ens' |
||||
import networkMap from 'ethjs-ens/lib/network-map.json' |
||||
import log from 'loglevel' |
||||
|
||||
|
||||
// Local Constants
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' |
||||
const ZERO_X_ERROR_ADDRESS = '0x' |
||||
|
||||
export default class EnsInput extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
className: PropTypes.string, |
||||
network: PropTypes.string, |
||||
selectedAddress: PropTypes.string, |
||||
selectedName: PropTypes.string, |
||||
onChange: PropTypes.func, |
||||
updateSendTo: PropTypes.func, |
||||
updateEnsResolution: PropTypes.func, |
||||
scanQrCode: PropTypes.func, |
||||
updateEnsResolutionError: PropTypes.func, |
||||
addressBook: PropTypes.array, |
||||
onPaste: PropTypes.func, |
||||
onReset: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
recipient: null, |
||||
input: '', |
||||
toError: null, |
||||
toWarning: null, |
||||
} |
||||
|
||||
componentDidMount () { |
||||
const network = this.props.network |
||||
const networkHasEnsSupport = getNetworkEnsSupport(network) |
||||
this.setState({ ensResolution: ZERO_ADDRESS }) |
||||
|
||||
if (networkHasEnsSupport) { |
||||
const provider = global.ethereumProvider |
||||
this.ens = new ENS({ provider, network }) |
||||
this.checkName = debounce(this.lookupEnsName, 200) |
||||
} |
||||
} |
||||
|
||||
// If an address is sent without a nickname, meaning not from ENS or from
|
||||
// the user's own accounts, a default of a one-space string is used.
|
||||
componentDidUpdate (prevProps) { |
||||
const { |
||||
input, |
||||
} = this.state |
||||
const { |
||||
network, |
||||
} = this.props |
||||
|
||||
if (prevProps.network !== network) { |
||||
const provider = global.ethereumProvider |
||||
this.ens = new ENS({ provider, network }) |
||||
this.onChange({ target: { value: input } }) |
||||
} |
||||
} |
||||
|
||||
resetInput = () => { |
||||
const { updateEnsResolution, updateEnsResolutionError, onReset } = this.props |
||||
this.onChange({ target: { value: '' } }) |
||||
onReset() |
||||
updateEnsResolution('') |
||||
updateEnsResolutionError('') |
||||
} |
||||
|
||||
lookupEnsName = (recipient) => { |
||||
recipient = recipient.trim() |
||||
|
||||
log.info(`ENS attempting to resolve name: ${recipient}`) |
||||
this.ens.lookup(recipient) |
||||
.then((address) => { |
||||
if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName')) |
||||
if (address === ZERO_X_ERROR_ADDRESS) throw new Error(this.context.t('ensRegistrationError')) |
||||
this.props.updateEnsResolution(address) |
||||
}) |
||||
.catch((reason) => { |
||||
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') { |
||||
this.props.updateEnsResolutionError(this.context.t('ensNotFoundOnCurrentNetwork')) |
||||
} else { |
||||
log.error(reason) |
||||
this.props.updateEnsResolutionError(reason.message) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
onPaste = event => { |
||||
event.clipboardData.items[0].getAsString(text => { |
||||
if (isValidAddress(text)) { |
||||
this.props.onPaste(text) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
onChange = e => { |
||||
const { network, onChange, updateEnsResolution, updateEnsResolutionError } = this.props |
||||
const input = e.target.value |
||||
const networkHasEnsSupport = getNetworkEnsSupport(network) |
||||
|
||||
this.setState({ input }, () => onChange(input)) |
||||
|
||||
// Empty ENS state if input is empty
|
||||
// maybe scan ENS
|
||||
if (!input || isValidAddress(input) || !networkHasEnsSupport) { |
||||
updateEnsResolution('') |
||||
updateEnsResolutionError(!networkHasEnsSupport ? 'Network does not support ENS' : '') |
||||
return |
||||
} |
||||
|
||||
if (isValidENSAddress(input)) { |
||||
this.lookupEnsName(input) |
||||
} else { |
||||
updateEnsResolution('') |
||||
updateEnsResolutionError('') |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { className, selectedAddress } = this.props |
||||
const { input } = this.state |
||||
|
||||
if (selectedAddress) { |
||||
return this.renderSelected() |
||||
} |
||||
|
||||
return ( |
||||
<div className={c('ens-input', className)}> |
||||
<div |
||||
className={c('ens-input__wrapper', { |
||||
'ens-input__wrapper__status-icon--error': false, |
||||
'ens-input__wrapper__status-icon--valid': false, |
||||
})} |
||||
> |
||||
<div className="ens-input__wrapper__status-icon" /> |
||||
<input |
||||
className="ens-input__wrapper__input" |
||||
type="text" |
||||
placeholder={t('recipientAddressPlaceholder')} |
||||
onChange={this.onChange} |
||||
onPaste={this.onPaste} |
||||
value={selectedAddress || input} |
||||
autoFocus |
||||
/> |
||||
<div |
||||
className={c('ens-input__wrapper__action-icon', { |
||||
'ens-input__wrapper__action-icon--erase': input, |
||||
'ens-input__wrapper__action-icon--qrcode': !input, |
||||
})} |
||||
onClick={() => { |
||||
if (input) { |
||||
this.resetInput() |
||||
} else { |
||||
this.props.scanQrCode() |
||||
} |
||||
}} |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderSelected () { |
||||
const { t } = this.context |
||||
const { className, selectedAddress, selectedName, addressBook } = this.props |
||||
const contact = addressBook.filter(item => item.address === selectedAddress)[0] || {} |
||||
const name = contact.name || selectedName |
||||
|
||||
|
||||
return ( |
||||
<div className={c('ens-input', className)}> |
||||
<div |
||||
className="ens-input__wrapper ens-input__wrapper--valid" |
||||
> |
||||
<div className="ens-input__wrapper__status-icon ens-input__wrapper__status-icon--valid" /> |
||||
<div |
||||
className="ens-input__wrapper__input ens-input__wrapper__input--selected" |
||||
placeholder={t('recipientAddress')} |
||||
onChange={this.onChange} |
||||
> |
||||
<div className="ens-input__selected-input__title"> |
||||
{name || ellipsify(selectedAddress)} |
||||
</div> |
||||
{ name && <div className="ens-input__selected-input__subtitle">{selectedAddress}</div> } |
||||
</div> |
||||
<div |
||||
className="ens-input__wrapper__action-icon ens-input__wrapper__action-icon--erase" |
||||
onClick={this.resetInput} |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
ensIcon (recipient) { |
||||
const { hoverText } = this.state |
||||
|
||||
return ( |
||||
<span |
||||
className="#ensIcon" |
||||
title={hoverText} |
||||
style={{ |
||||
position: 'absolute', |
||||
top: '16px', |
||||
left: '-25px', |
||||
}} |
||||
> |
||||
{ this.ensIconContents(recipient) } |
||||
</span> |
||||
) |
||||
} |
||||
|
||||
ensIconContents () { |
||||
const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } |
||||
|
||||
if (toError) return |
||||
|
||||
if (loadingEns) { |
||||
return ( |
||||
<img |
||||
src="images/loading.svg" |
||||
style={{ |
||||
width: '30px', |
||||
height: '30px', |
||||
transform: 'translateY(-6px)', |
||||
}} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
if (ensFailure) { |
||||
return <i className="fa fa-warning fa-lg warning'" /> |
||||
} |
||||
|
||||
if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { |
||||
return ( |
||||
<i |
||||
className="fa fa-check-circle fa-lg cursor-pointer" |
||||
style={{ color: 'green' }} |
||||
onClick={event => { |
||||
event.preventDefault() |
||||
event.stopPropagation() |
||||
copyToClipboard(ensResolution) |
||||
}} |
||||
/> |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
function getNetworkEnsSupport (network) { |
||||
return Boolean(networkMap[network]) |
||||
} |
@ -0,0 +1,20 @@ |
||||
import EnsInput from './ens-input.component' |
||||
import { |
||||
getCurrentNetwork, |
||||
getSendTo, |
||||
getSendToNickname, |
||||
} from '../../send.selectors' |
||||
import { |
||||
getAddressBook, |
||||
} from '../../../../selectors/selectors' |
||||
const connect = require('react-redux').connect |
||||
|
||||
|
||||
export default connect( |
||||
state => ({ |
||||
network: getCurrentNetwork(state), |
||||
selectedAddress: getSendTo(state), |
||||
selectedName: getSendToNickname(state), |
||||
addressBook: getAddressBook(state), |
||||
}) |
||||
)(EnsInput) |
@ -0,0 +1 @@ |
||||
export { default } from './ens-input.container' |
@ -0,0 +1 @@ |
||||
export { default } from './add-recipient.container' |
@ -0,0 +1,202 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import AddRecipient from '../add-recipient.component' |
||||
import Dialog from '../../../../../components/ui/dialog' |
||||
|
||||
const propsMethodSpies = { |
||||
closeToDropdown: sinon.spy(), |
||||
openToDropdown: sinon.spy(), |
||||
updateGas: sinon.spy(), |
||||
updateSendTo: sinon.spy(), |
||||
updateSendToError: sinon.spy(), |
||||
updateSendToWarning: sinon.spy(), |
||||
} |
||||
|
||||
describe('AddRecipient Component', function () { |
||||
let wrapper |
||||
let instance |
||||
|
||||
beforeEach(() => { |
||||
wrapper = shallow(<AddRecipient |
||||
closeToDropdown={propsMethodSpies.closeToDropdown} |
||||
inError={false} |
||||
inWarning={false} |
||||
network={'mockNetwork'} |
||||
openToDropdown={propsMethodSpies.openToDropdown} |
||||
to={'mockTo'} |
||||
toAccounts={['mockAccount']} |
||||
toDropdownOpen={false} |
||||
updateGas={propsMethodSpies.updateGas} |
||||
updateSendTo={propsMethodSpies.updateSendTo} |
||||
updateSendToError={propsMethodSpies.updateSendToError} |
||||
updateSendToWarning={propsMethodSpies.updateSendToWarning} |
||||
addressBook={[{ address: '0x80F061544cC398520615B5d3e7A3BedD70cd4510', name: 'Fav 5' }]} |
||||
nonContacts={[{ address: '0x70F061544cC398520615B5d3e7A3BedD70cd4510', name: 'Fav 7' }]} |
||||
contacts={[{ address: '0x60F061544cC398520615B5d3e7A3BedD70cd4510', name: 'Fav 6' }]} |
||||
/>, { context: { t: str => str + '_t' } }) |
||||
instance = wrapper.instance() |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
propsMethodSpies.closeToDropdown.resetHistory() |
||||
propsMethodSpies.openToDropdown.resetHistory() |
||||
propsMethodSpies.updateSendTo.resetHistory() |
||||
propsMethodSpies.updateSendToError.resetHistory() |
||||
propsMethodSpies.updateSendToWarning.resetHistory() |
||||
propsMethodSpies.updateGas.resetHistory() |
||||
}) |
||||
|
||||
describe('selectRecipient', () => { |
||||
|
||||
it('should call updateSendTo', () => { |
||||
assert.equal(propsMethodSpies.updateSendTo.callCount, 0) |
||||
instance.selectRecipient('mockTo2', 'mockNickname') |
||||
assert.equal(propsMethodSpies.updateSendTo.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.updateSendTo.getCall(0).args, |
||||
['mockTo2', 'mockNickname'] |
||||
) |
||||
}) |
||||
|
||||
it('should call updateGas if there is no to error', () => { |
||||
assert.equal(propsMethodSpies.updateGas.callCount, 0) |
||||
instance.selectRecipient(false) |
||||
assert.equal(propsMethodSpies.updateGas.callCount, 1) |
||||
}) |
||||
}) |
||||
|
||||
describe('render', () => { |
||||
it('should render a component', () => { |
||||
assert.equal(wrapper.find('.send__select-recipient-wrapper').length, 1) |
||||
}) |
||||
|
||||
it('should render no content if there are no recents, transfers, and contacts', () => { |
||||
wrapper.setProps({ |
||||
ownedAccounts: [], |
||||
addressBook: [], |
||||
}) |
||||
|
||||
assert.equal(wrapper.find('.send__select-recipient-wrapper__list__link').length, 0) |
||||
assert.equal(wrapper.find('.send__select-recipient-wrapper__group').length, 0) |
||||
}) |
||||
|
||||
it('should render transfer', () => { |
||||
wrapper.setProps({ |
||||
ownedAccounts: [{ address: '0x123', name: '123' }, { address: '0x124', name: '124' }], |
||||
addressBook: [{ address: '0x456', name: 'test-name' }], |
||||
}) |
||||
wrapper.setState({ isShowingTransfer: true }) |
||||
|
||||
const xferLink = wrapper.find('.send__select-recipient-wrapper__list__link') |
||||
assert.equal(xferLink.length, 1) |
||||
|
||||
|
||||
const groups = wrapper.find('RecipientGroup') |
||||
assert.equal(groups.shallow().find('.send__select-recipient-wrapper__group').length, 1) |
||||
}) |
||||
|
||||
it('should render ContactList', () => { |
||||
wrapper.setProps({ |
||||
ownedAccounts: [{ address: '0x123', name: '123' }, { address: '0x124', name: '124' }], |
||||
addressBook: [{ address: '0x125' }], |
||||
}) |
||||
|
||||
const contactList = wrapper.find('ContactList') |
||||
|
||||
assert.equal(contactList.length, 1) |
||||
}) |
||||
|
||||
it('should render contacts', () => { |
||||
wrapper.setProps({ |
||||
addressBook: [ |
||||
{ address: '0x125', name: 'alice' }, |
||||
{ address: '0x126', name: 'alex' }, |
||||
{ address: '0x127', name: 'catherine' }, |
||||
], |
||||
}) |
||||
wrapper.setState({ isShowingTransfer: false }) |
||||
|
||||
const xferLink = wrapper.find('.send__select-recipient-wrapper__list__link') |
||||
assert.equal(xferLink.length, 0) |
||||
|
||||
const groups = wrapper.find('ContactList') |
||||
assert.equal(groups.length, 1) |
||||
|
||||
assert.equal(groups.find('.send__select-recipient-wrapper__group-item').length, 0) |
||||
}) |
||||
|
||||
it('should render error when query has no results', () => { |
||||
wrapper.setProps({ |
||||
addressBook: [], |
||||
toError: 'bad', |
||||
contacts: [], |
||||
nonContacts: [], |
||||
}) |
||||
|
||||
const dialog = wrapper.find(Dialog) |
||||
|
||||
assert.equal(dialog.props().type, 'error') |
||||
assert.equal(dialog.props().children, 'bad_t') |
||||
assert.equal(dialog.length, 1) |
||||
}) |
||||
|
||||
it('should render error when query has ens does not resolve', () => { |
||||
wrapper.setProps({ |
||||
addressBook: [], |
||||
toError: 'bad', |
||||
ensResolutionError: 'very bad', |
||||
contacts: [], |
||||
nonContacts: [], |
||||
}) |
||||
|
||||
const dialog = wrapper.find(Dialog) |
||||
|
||||
assert.equal(dialog.props().type, 'error') |
||||
assert.equal(dialog.props().children, 'very bad') |
||||
assert.equal(dialog.length, 1) |
||||
}) |
||||
|
||||
it('should render warning', () => { |
||||
wrapper.setProps({ |
||||
addressBook: [], |
||||
query: 'yo', |
||||
toWarning: 'watchout', |
||||
}) |
||||
|
||||
const dialog = wrapper.find(Dialog) |
||||
|
||||
assert.equal(dialog.props().type, 'warning') |
||||
assert.equal(dialog.props().children, 'watchout_t') |
||||
assert.equal(dialog.length, 1) |
||||
}) |
||||
|
||||
it('should not render error when ens resolved', () => { |
||||
wrapper.setProps({ |
||||
addressBook: [], |
||||
toError: 'bad', |
||||
ensResolution: '0x128', |
||||
}) |
||||
|
||||
const dialog = wrapper.find(Dialog) |
||||
|
||||
assert.equal(dialog.length, 0) |
||||
}) |
||||
|
||||
it('should not render error when query has results', () => { |
||||
wrapper.setProps({ |
||||
addressBook: [ |
||||
{ address: '0x125', name: 'alice' }, |
||||
{ address: '0x126', name: 'alex' }, |
||||
{ address: '0x127', name: 'catherine' }, |
||||
], |
||||
toError: 'bad', |
||||
}) |
||||
|
||||
const dialog = wrapper.find(Dialog) |
||||
|
||||
assert.equal(dialog.length, 0) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,72 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
import sinon from 'sinon' |
||||
|
||||
let mapStateToProps |
||||
let mapDispatchToProps |
||||
|
||||
const actionSpies = { |
||||
updateSendTo: sinon.spy(), |
||||
} |
||||
|
||||
proxyquire('../add-recipient.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md) => { |
||||
mapStateToProps = ms |
||||
mapDispatchToProps = md |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
'../../send.selectors.js': { |
||||
getSendEnsResolution: (s) => `mockSendEnsResolution:${s}`, |
||||
getSendEnsResolutionError: (s) => `mockSendEnsResolutionError:${s}`, |
||||
accountsWithSendEtherInfoSelector: (s) => `mockAccountsWithSendEtherInfoSelector:${s}`, |
||||
}, |
||||
'../../../../selectors/selectors': { |
||||
getAddressBook: (s) => [{ name: `mockAddressBook:${s}` }], |
||||
getAddressBookEntry: (s) => `mockAddressBookEntry:${s}`, |
||||
}, |
||||
'../../../../store/actions': actionSpies, |
||||
}) |
||||
|
||||
describe('add-recipient container', () => { |
||||
|
||||
describe('mapStateToProps()', () => { |
||||
|
||||
it('should map the correct properties to props', () => { |
||||
assert.deepEqual(mapStateToProps('mockState'), { |
||||
addressBook: [{ name: 'mockAddressBook:mockState' }], |
||||
contacts: [{ name: 'mockAddressBook:mockState' }], |
||||
ensResolution: 'mockSendEnsResolution:mockState', |
||||
ensResolutionError: 'mockSendEnsResolutionError:mockState', |
||||
ownedAccounts: 'mockAccountsWithSendEtherInfoSelector:mockState', |
||||
addressBookEntryName: undefined, |
||||
nonContacts: [], |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('mapDispatchToProps()', () => { |
||||
let dispatchSpy |
||||
let mapDispatchToPropsObject |
||||
|
||||
beforeEach(() => { |
||||
dispatchSpy = sinon.spy() |
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) |
||||
}) |
||||
|
||||
describe('updateSendTo()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname') |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(actionSpies.updateSendTo.calledOnce) |
||||
assert.deepEqual( |
||||
actionSpies.updateSendTo.getCall(0).args, |
||||
['mockTo', 'mockNickname'] |
||||
) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -1 +1 @@ |
||||
export { default } from './send-content.component' |
||||
export { default } from './send-content.container' |
||||
|
@ -0,0 +1,38 @@ |
||||
import { connect } from 'react-redux' |
||||
import SendContent from './send-content.component' |
||||
import { |
||||
accountsWithSendEtherInfoSelector, |
||||
getSendTo, |
||||
} from '../send.selectors' |
||||
import { |
||||
getAddressBook, |
||||
} from '../../../selectors/selectors' |
||||
import actions from '../../../store/actions' |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
to: getSendTo(state), |
||||
addressBook: getAddressBook(state), |
||||
ownedAccounts: accountsWithSendEtherInfoSelector(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
showAddToAddressBookModal: (recipient) => dispatch(actions.showModal({ |
||||
name: 'ADD_TO_ADDRESSBOOK', |
||||
recipient, |
||||
})), |
||||
} |
||||
} |
||||
|
||||
function mergeProps (stateProps, dispatchProps, ownProps) { |
||||
return { |
||||
...ownProps, |
||||
...stateProps, |
||||
...dispatchProps, |
||||
showAddToAddressBookModal: () => dispatchProps.showAddToAddressBookModal(stateProps.to), |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendContent) |
@ -1 +0,0 @@ |
||||
export { default } from './send-to-row.container' |
@ -1,91 +0,0 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import SendRowWrapper from '../send-row-wrapper' |
||||
import EnsInput from '../../../../components/app/ens-input' |
||||
import { getToErrorObject, getToWarningObject } from './send-to-row.utils.js' |
||||
|
||||
export default class SendToRow extends Component { |
||||
|
||||
static propTypes = { |
||||
closeToDropdown: PropTypes.func, |
||||
hasHexData: PropTypes.bool.isRequired, |
||||
inError: PropTypes.bool, |
||||
inWarning: PropTypes.bool, |
||||
network: PropTypes.string, |
||||
openToDropdown: PropTypes.func, |
||||
selectedToken: PropTypes.object, |
||||
to: PropTypes.string, |
||||
toAccounts: PropTypes.array, |
||||
toDropdownOpen: PropTypes.bool, |
||||
tokens: PropTypes.array, |
||||
updateGas: PropTypes.func, |
||||
updateSendTo: PropTypes.func, |
||||
updateSendToError: PropTypes.func, |
||||
updateSendToWarning: PropTypes.func, |
||||
scanQrCode: PropTypes.func, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
metricsEvent: PropTypes.func, |
||||
} |
||||
|
||||
handleToChange (to, nickname = '', toError, toWarning, network) { |
||||
const { hasHexData, updateSendTo, updateSendToError, updateGas, tokens, selectedToken, updateSendToWarning } = this.props |
||||
const toErrorObject = getToErrorObject(to, toError, hasHexData, tokens, selectedToken, network) |
||||
const toWarningObject = getToWarningObject(to, toWarning, tokens, selectedToken) |
||||
updateSendTo(to, nickname) |
||||
updateSendToError(toErrorObject) |
||||
updateSendToWarning(toWarningObject) |
||||
if (toErrorObject.to === null) { |
||||
updateGas({ to }) |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
closeToDropdown, |
||||
inError, |
||||
inWarning, |
||||
network, |
||||
openToDropdown, |
||||
to, |
||||
toAccounts, |
||||
toDropdownOpen, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<SendRowWrapper |
||||
errorType={'to'} |
||||
label={`${this.context.t('to')}: `} |
||||
showError={inError} |
||||
showWarning={inWarning} |
||||
warningType={'to'} |
||||
> |
||||
<EnsInput |
||||
scanQrCode={_ => { |
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Transactions', |
||||
action: 'Edit Screen', |
||||
name: 'Used QR scanner', |
||||
}, |
||||
}) |
||||
this.props.scanQrCode() |
||||
}} |
||||
accounts={toAccounts} |
||||
closeDropdown={() => closeToDropdown()} |
||||
dropdownOpen={toDropdownOpen} |
||||
inError={inError} |
||||
name={'address'} |
||||
network={network} |
||||
onChange={({ toAddress, nickname, toError, toWarning }) => this.handleToChange(toAddress, nickname, toError, toWarning, this.props.network)} |
||||
openDropdown={() => openToDropdown()} |
||||
placeholder={this.context.t('recipientAddress')} |
||||
to={to} |
||||
/> |
||||
</SendRowWrapper> |
||||
) |
||||
} |
||||
|
||||
} |
@ -1,54 +0,0 @@ |
||||
import { connect } from 'react-redux' |
||||
import { |
||||
getCurrentNetwork, |
||||
getSelectedToken, |
||||
getSendTo, |
||||
getSendToAccounts, |
||||
getSendHexData, |
||||
} from '../../send.selectors.js' |
||||
import { |
||||
getToDropdownOpen, |
||||
getTokens, |
||||
sendToIsInError, |
||||
sendToIsInWarning, |
||||
} from './send-to-row.selectors.js' |
||||
import { |
||||
updateSendTo, |
||||
} from '../../../../store/actions' |
||||
import { |
||||
updateSendErrors, |
||||
updateSendWarnings, |
||||
openToDropdown, |
||||
closeToDropdown, |
||||
} from '../../../../ducks/send/send.duck' |
||||
import SendToRow from './send-to-row.component' |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
hasHexData: Boolean(getSendHexData(state)), |
||||
inError: sendToIsInError(state), |
||||
inWarning: sendToIsInWarning(state), |
||||
network: getCurrentNetwork(state), |
||||
selectedToken: getSelectedToken(state), |
||||
to: getSendTo(state), |
||||
toAccounts: getSendToAccounts(state), |
||||
toDropdownOpen: getToDropdownOpen(state), |
||||
tokens: getTokens(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
closeToDropdown: () => dispatch(closeToDropdown()), |
||||
openToDropdown: () => dispatch(openToDropdown()), |
||||
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), |
||||
updateSendToError: (toErrorObject) => { |
||||
dispatch(updateSendErrors(toErrorObject)) |
||||
}, |
||||
updateSendToWarning: (toWarningObject) => { |
||||
dispatch(updateSendWarnings(toWarningObject)) |
||||
}, |
||||
} |
||||
} |
@ -1,166 +0,0 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import proxyquire from 'proxyquire' |
||||
|
||||
const SendToRow = proxyquire('../send-to-row.component.js', { |
||||
'./send-to-row.utils.js': { |
||||
getToErrorObject: (to, toError) => ({ |
||||
to: to === false ? null : `mockToErrorObject:${to}${toError}`, |
||||
}), |
||||
getToWarningObject: (to, toWarning) => ({ |
||||
to: to === false ? null : `mockToWarningObject:${to}${toWarning}`, |
||||
}), |
||||
}, |
||||
}).default |
||||
|
||||
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' |
||||
import EnsInput from '../../../../../components/app/ens-input' |
||||
|
||||
const propsMethodSpies = { |
||||
closeToDropdown: sinon.spy(), |
||||
openToDropdown: sinon.spy(), |
||||
updateGas: sinon.spy(), |
||||
updateSendTo: sinon.spy(), |
||||
updateSendToError: sinon.spy(), |
||||
updateSendToWarning: sinon.spy(), |
||||
} |
||||
|
||||
sinon.spy(SendToRow.prototype, 'handleToChange') |
||||
|
||||
describe('SendToRow Component', function () { |
||||
let wrapper |
||||
let instance |
||||
|
||||
beforeEach(() => { |
||||
wrapper = shallow(<SendToRow |
||||
closeToDropdown={propsMethodSpies.closeToDropdown} |
||||
inError={false} |
||||
inWarning={false} |
||||
network={'mockNetwork'} |
||||
openToDropdown={propsMethodSpies.openToDropdown} |
||||
to={'mockTo'} |
||||
toAccounts={['mockAccount']} |
||||
toDropdownOpen={false} |
||||
updateGas={propsMethodSpies.updateGas} |
||||
updateSendTo={propsMethodSpies.updateSendTo} |
||||
updateSendToError={propsMethodSpies.updateSendToError} |
||||
updateSendToWarning={propsMethodSpies.updateSendToWarning} |
||||
/>, { context: { t: str => str + '_t' } }) |
||||
instance = wrapper.instance() |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
propsMethodSpies.closeToDropdown.resetHistory() |
||||
propsMethodSpies.openToDropdown.resetHistory() |
||||
propsMethodSpies.updateSendTo.resetHistory() |
||||
propsMethodSpies.updateSendToError.resetHistory() |
||||
propsMethodSpies.updateSendToWarning.resetHistory() |
||||
SendToRow.prototype.handleToChange.resetHistory() |
||||
}) |
||||
|
||||
describe('handleToChange', () => { |
||||
|
||||
it('should call updateSendTo', () => { |
||||
assert.equal(propsMethodSpies.updateSendTo.callCount, 0) |
||||
instance.handleToChange('mockTo2', 'mockNickname') |
||||
assert.equal(propsMethodSpies.updateSendTo.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.updateSendTo.getCall(0).args, |
||||
['mockTo2', 'mockNickname'] |
||||
) |
||||
}) |
||||
|
||||
it('should call updateSendToError', () => { |
||||
assert.equal(propsMethodSpies.updateSendToError.callCount, 0) |
||||
instance.handleToChange('mockTo2', '', 'mockToError') |
||||
assert.equal(propsMethodSpies.updateSendToError.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.updateSendToError.getCall(0).args, |
||||
[{ to: 'mockToErrorObject:mockTo2mockToError' }] |
||||
) |
||||
}) |
||||
|
||||
it('should call updateSendToWarning', () => { |
||||
assert.equal(propsMethodSpies.updateSendToWarning.callCount, 0) |
||||
instance.handleToChange('mockTo2', '', '', 'mockToWarning') |
||||
assert.equal(propsMethodSpies.updateSendToWarning.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.updateSendToWarning.getCall(0).args, |
||||
[{ to: 'mockToWarningObject:mockTo2mockToWarning' }] |
||||
) |
||||
}) |
||||
|
||||
it('should not call updateGas if there is a to error', () => { |
||||
assert.equal(propsMethodSpies.updateGas.callCount, 0) |
||||
instance.handleToChange('mockTo2') |
||||
assert.equal(propsMethodSpies.updateGas.callCount, 0) |
||||
}) |
||||
|
||||
it('should call updateGas if there is no to error', () => { |
||||
assert.equal(propsMethodSpies.updateGas.callCount, 0) |
||||
instance.handleToChange(false) |
||||
assert.equal(propsMethodSpies.updateGas.callCount, 1) |
||||
}) |
||||
}) |
||||
|
||||
describe('render', () => { |
||||
it('should render a SendRowWrapper component', () => { |
||||
assert.equal(wrapper.find(SendRowWrapper).length, 1) |
||||
}) |
||||
|
||||
it('should pass the correct props to SendRowWrapper', () => { |
||||
const { |
||||
errorType, |
||||
label, |
||||
showError, |
||||
} = wrapper.find(SendRowWrapper).props() |
||||
|
||||
assert.equal(errorType, 'to') |
||||
|
||||
assert.equal(label, 'to_t: ') |
||||
|
||||
assert.equal(showError, false) |
||||
}) |
||||
|
||||
it('should render an EnsInput as a child of the SendRowWrapper', () => { |
||||
assert(wrapper.find(SendRowWrapper).childAt(0).is(EnsInput)) |
||||
}) |
||||
|
||||
it('should render the EnsInput with the correct props', () => { |
||||
const { |
||||
accounts, |
||||
closeDropdown, |
||||
dropdownOpen, |
||||
inError, |
||||
name, |
||||
network, |
||||
onChange, |
||||
openDropdown, |
||||
placeholder, |
||||
to, |
||||
} = wrapper.find(SendRowWrapper).childAt(0).props() |
||||
assert.deepEqual(accounts, ['mockAccount']) |
||||
assert.equal(dropdownOpen, false) |
||||
assert.equal(inError, false) |
||||
assert.equal(name, 'address') |
||||
assert.equal(network, 'mockNetwork') |
||||
assert.equal(placeholder, 'recipientAddress_t') |
||||
assert.equal(to, 'mockTo') |
||||
assert.equal(propsMethodSpies.closeToDropdown.callCount, 0) |
||||
closeDropdown() |
||||
assert.equal(propsMethodSpies.closeToDropdown.callCount, 1) |
||||
assert.equal(propsMethodSpies.openToDropdown.callCount, 0) |
||||
openDropdown() |
||||
assert.equal(propsMethodSpies.openToDropdown.callCount, 1) |
||||
assert.equal(SendToRow.prototype.handleToChange.callCount, 0) |
||||
onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError', toWarning: 'mockToWarning' }) |
||||
assert.equal(SendToRow.prototype.handleToChange.callCount, 1) |
||||
assert.deepEqual( |
||||
SendToRow.prototype.handleToChange.getCall(0).args, |
||||
['mockNewTo', 'mockNewNickname', 'mockToError', 'mockToWarning', 'mockNetwork' ] |
||||
) |
||||
}) |
||||
}) |
||||
}) |
@ -1,134 +0,0 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
import sinon from 'sinon' |
||||
|
||||
let mapStateToProps |
||||
let mapDispatchToProps |
||||
|
||||
const actionSpies = { |
||||
updateSendTo: sinon.spy(), |
||||
} |
||||
const duckActionSpies = { |
||||
closeToDropdown: sinon.spy(), |
||||
openToDropdown: sinon.spy(), |
||||
updateSendErrors: sinon.spy(), |
||||
updateSendWarnings: sinon.spy(), |
||||
} |
||||
|
||||
proxyquire('../send-to-row.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md) => { |
||||
mapStateToProps = ms |
||||
mapDispatchToProps = md |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
'../../send.selectors.js': { |
||||
getCurrentNetwork: (s) => `mockNetwork:${s}`, |
||||
getSelectedToken: (s) => `mockSelectedToken:${s}`, |
||||
getSendHexData: (s) => s, |
||||
getSendTo: (s) => `mockTo:${s}`, |
||||
getSendToAccounts: (s) => `mockToAccounts:${s}`, |
||||
}, |
||||
'./send-to-row.selectors.js': { |
||||
getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`, |
||||
sendToIsInError: (s) => `mockInError:${s}`, |
||||
sendToIsInWarning: (s) => `mockInWarning:${s}`, |
||||
getTokens: (s) => `mockTokens:${s}`, |
||||
}, |
||||
'../../../../store/actions': actionSpies, |
||||
'../../../../ducks/send/send.duck': duckActionSpies, |
||||
}) |
||||
|
||||
describe('send-to-row container', () => { |
||||
|
||||
describe('mapStateToProps()', () => { |
||||
|
||||
it('should map the correct properties to props', () => { |
||||
assert.deepEqual(mapStateToProps('mockState'), { |
||||
hasHexData: true, |
||||
inError: 'mockInError:mockState', |
||||
inWarning: 'mockInWarning:mockState', |
||||
network: 'mockNetwork:mockState', |
||||
selectedToken: 'mockSelectedToken:mockState', |
||||
to: 'mockTo:mockState', |
||||
toAccounts: 'mockToAccounts:mockState', |
||||
toDropdownOpen: 'mockToDropdownOpen:mockState', |
||||
tokens: 'mockTokens:mockState', |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('mapDispatchToProps()', () => { |
||||
let dispatchSpy |
||||
let mapDispatchToPropsObject |
||||
|
||||
beforeEach(() => { |
||||
dispatchSpy = sinon.spy() |
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) |
||||
}) |
||||
|
||||
describe('closeToDropdown()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.closeToDropdown() |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(duckActionSpies.closeToDropdown.calledOnce) |
||||
assert.equal( |
||||
duckActionSpies.closeToDropdown.getCall(0).args[0], |
||||
undefined |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('openToDropdown()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.openToDropdown() |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(duckActionSpies.openToDropdown.calledOnce) |
||||
assert.equal( |
||||
duckActionSpies.openToDropdown.getCall(0).args[0], |
||||
undefined |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('updateSendTo()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname') |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(actionSpies.updateSendTo.calledOnce) |
||||
assert.deepEqual( |
||||
actionSpies.updateSendTo.getCall(0).args, |
||||
['mockTo', 'mockNickname'] |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('updateSendToError()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.updateSendToError('mockToErrorObject') |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(duckActionSpies.updateSendErrors.calledOnce) |
||||
assert.equal( |
||||
duckActionSpies.updateSendErrors.getCall(0).args[0], |
||||
'mockToErrorObject' |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('updateSendToWarning()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.updateSendToWarning('mockToWarningObject') |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(duckActionSpies.updateSendWarnings.calledOnce) |
||||
assert.equal( |
||||
duckActionSpies.updateSendWarnings.getCall(0).args[0], |
||||
'mockToWarningObject' |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,233 @@ |
||||
.send { |
||||
&__header { |
||||
position: relative; |
||||
background-color: $Grey-000; |
||||
border-bottom: none; |
||||
padding: 14px 0 3px 0; |
||||
|
||||
.page-container__title { |
||||
@extend %h4; |
||||
text-align: center; |
||||
} |
||||
|
||||
.page-container__header-close-text { |
||||
@extend %link; |
||||
font-size: 1rem; |
||||
line-height: 1.1875rem; |
||||
position: absolute; |
||||
right: 1rem; |
||||
} |
||||
} |
||||
|
||||
&__dialog { |
||||
margin: 1rem; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&__error-dialog { |
||||
margin: 1rem; |
||||
} |
||||
|
||||
&__to-row { |
||||
margin: 0; |
||||
padding: .5rem; |
||||
flex: 0 0 auto; |
||||
background-color: $Grey-000; |
||||
border-bottom: 1px solid $alto; |
||||
} |
||||
|
||||
&__select-recipient-wrapper { |
||||
@extend %col-nowrap; |
||||
flex: 1 1 auto; |
||||
height: 0; |
||||
|
||||
&__list { |
||||
overflow-y: auto; |
||||
|
||||
&__link { |
||||
@extend %link; |
||||
@extend %row-nowrap; |
||||
padding: 1rem; |
||||
font-size: 1rem; |
||||
border-bottom: 1px solid $alto; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__back-caret { |
||||
@extend %bg-contain; |
||||
display: block; |
||||
background-image: url('/images/caret-left.svg'); |
||||
width: 18px; |
||||
height: 18px; |
||||
margin-right: .5rem; |
||||
} |
||||
} |
||||
|
||||
&__recent-group-wrapper { |
||||
@extend %col-nowrap; |
||||
|
||||
&__load-more { |
||||
@extend %link; |
||||
font-size: .75rem; |
||||
line-height: 1.0625rem; |
||||
padding: .5rem; |
||||
text-align: center; |
||||
border-bottom: 1px solid $alto; |
||||
} |
||||
} |
||||
|
||||
&__group { |
||||
@extend %col-nowrap; |
||||
} |
||||
|
||||
&__group-label { |
||||
@extend %h8; |
||||
background-color: $Grey-000; |
||||
color: $Grey-600; |
||||
line-height: .875rem; |
||||
padding: .5rem 1rem; |
||||
border-bottom: 1px solid $alto; |
||||
|
||||
&:first-of-type { |
||||
border-top: 1px solid $alto; |
||||
} |
||||
} |
||||
|
||||
&__group-item, &__group-item--selected { |
||||
@extend %row-nowrap; |
||||
padding: .75rem 1rem; |
||||
align-items: center; |
||||
border-bottom: 1px solid $alto; |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
background-color: rgba($alto, 0.2); |
||||
} |
||||
|
||||
.identicon { |
||||
margin-right: 1rem; |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
&__content { |
||||
@extend %col-nowrap; |
||||
flex: 1 1 auto; |
||||
width: 0; |
||||
} |
||||
|
||||
&__title { |
||||
font-size: .875rem; |
||||
line-height: 1.25rem; |
||||
color: $black; |
||||
} |
||||
|
||||
&__subtitle { |
||||
@extend %h8; |
||||
color: $Grey-500; |
||||
} |
||||
} |
||||
|
||||
&__group-item--selected { |
||||
border: 2px solid #2b7cd6; |
||||
border-radius: 8px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.ens-input { |
||||
@extend %row-nowrap; |
||||
|
||||
&__wrapper { |
||||
@extend %row-nowrap; |
||||
flex: 1 1 auto; |
||||
width: 0; |
||||
align-items: center; |
||||
background: $white; |
||||
border-radius: .5rem; |
||||
padding: .75rem .5rem; |
||||
border: 1px solid $Grey-100; |
||||
transition: border-color 150ms ease-in-out; |
||||
|
||||
&:focus-within { |
||||
border-color: $Grey-500; |
||||
} |
||||
|
||||
&__status-icon { |
||||
@extend %bg-contain; |
||||
background-image: url("/images/search-black.svg"); |
||||
width: 1.125rem; |
||||
height: 1.125rem; |
||||
margin: .25rem .5rem .25rem .25rem; |
||||
|
||||
&--error { |
||||
|
||||
} |
||||
|
||||
&--valid { |
||||
background-image: url("/images/check-green-solid.svg"); |
||||
} |
||||
} |
||||
|
||||
&__input { |
||||
@extend %h6; |
||||
flex: 1 1 auto; |
||||
width: 0; |
||||
border: 0; |
||||
outline: none; |
||||
|
||||
&::placeholder { |
||||
color: $Grey-200; |
||||
} |
||||
} |
||||
|
||||
&__action-icon { |
||||
@extend %bg-contain; |
||||
cursor: pointer; |
||||
|
||||
&--erase { |
||||
background-image: url("/images/close-gray.svg"); |
||||
width: .75rem; |
||||
height: .75rem; |
||||
margin: 0 .25rem; |
||||
} |
||||
|
||||
&--qrcode { |
||||
background-image: url("/images/qr-blue.svg"); |
||||
width: 1.5rem; |
||||
height: 1.5rem; |
||||
margin: 0 .25rem; |
||||
} |
||||
} |
||||
|
||||
&--valid { |
||||
border-color: $Blue-500; |
||||
|
||||
.ens-input__wrapper { |
||||
&__status-icon { |
||||
background-image: url("/images/check-green-solid.svg"); |
||||
} |
||||
|
||||
&__input { |
||||
@extend %col-nowrap; |
||||
font-size: .75rem; |
||||
line-height: .75rem; |
||||
font-weight: 400; |
||||
color: $Blue-500; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
&__selected-input { |
||||
&__title { |
||||
@extend %ellipsify; |
||||
font-size: .875rem; |
||||
} |
||||
|
||||
&__subtitle { |
||||
font-size: 0.75rem; |
||||
color: $Grey-500; |
||||
margin-top: .25rem; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,131 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Identicon from '../../../../components/ui/identicon' |
||||
import TextField from '../../../../components/ui/text-field' |
||||
import { CONTACT_LIST_ROUTE } from '../../../../helpers/constants/routes' |
||||
import { isValidAddress, isValidENSAddress } from '../../../../helpers/utils/util' |
||||
import EnsInput from '../../../../pages/send/send-content/add-recipient/ens-input' |
||||
import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' |
||||
import debounce from 'lodash.debounce' |
||||
|
||||
export default class AddContact extends PureComponent { |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
addToAddressBook: PropTypes.func, |
||||
history: PropTypes.object, |
||||
scanQrCode: PropTypes.func, |
||||
qrCodeData: PropTypes.object, |
||||
qrCodeDetected: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
nickname: '', |
||||
ethAddress: '', |
||||
ensAddress: '', |
||||
error: '', |
||||
ensError: '', |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
this.dValidate = debounce(this.validate, 1000) |
||||
} |
||||
|
||||
componentWillReceiveProps (nextProps) { |
||||
if (nextProps.qrCodeData) { |
||||
if (nextProps.qrCodeData.type === 'address') { |
||||
const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase() |
||||
const currentAddress = this.state.ensAddress || this.state.ethAddress |
||||
if (currentAddress.toLowerCase() !== scannedAddress) { |
||||
this.setState({ ethAddress: scannedAddress, ensAddress: '' }) |
||||
// Clean up QR code data after handling
|
||||
this.props.qrCodeDetected(null) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
validate = address => { |
||||
const valid = isValidAddress(address) |
||||
const validEnsAddress = isValidENSAddress(address) |
||||
if (valid || validEnsAddress || address === '') { |
||||
this.setState({ error: '', ethAddress: address }) |
||||
} else { |
||||
this.setState({ error: 'Invalid Address' }) |
||||
} |
||||
} |
||||
|
||||
renderInput () { |
||||
return ( |
||||
<EnsInput |
||||
className="send__to-row" |
||||
scanQrCode={_ => { this.props.scanQrCode() }} |
||||
onChange={this.dValidate} |
||||
onPaste={text => this.setState({ ethAddress: text })} |
||||
onReset={() => this.setState({ ethAddress: '', ensAddress: '' })} |
||||
updateEnsResolution={address => { |
||||
this.setState({ ensAddress: address, error: '', ensError: '' }) |
||||
}} |
||||
updateEnsResolutionError={message => this.setState({ ensError: message })} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { history, addToAddressBook } = this.props |
||||
|
||||
const errorToRender = this.state.ensError || this.state.error |
||||
|
||||
return ( |
||||
<div className="settings-page__content-row address-book__add-contact"> |
||||
{this.state.ensAddress && <div className="address-book__view-contact__group"> |
||||
<Identicon address={this.state.ensAddress} diameter={60} /> |
||||
<div className="address-book__view-contact__group__value"> |
||||
{ this.state.ensAddress } |
||||
</div> |
||||
</div>} |
||||
<div className="address-book__add-contact__content"> |
||||
<div className="address-book__view-contact__group"> |
||||
<div className="address-book__view-contact__group__label"> |
||||
{ t('userName') } |
||||
</div> |
||||
<TextField |
||||
type="text" |
||||
id="nickname" |
||||
value={this.state.newName} |
||||
onChange={e => this.setState({ newName: e.target.value })} |
||||
fullWidth |
||||
margin="dense" |
||||
/> |
||||
</div> |
||||
|
||||
<div className="address-book__view-contact__group"> |
||||
<div className="address-book__view-contact__group__label"> |
||||
{ t('ethereumPublicAddress') } |
||||
</div> |
||||
{ this.renderInput() } |
||||
{ errorToRender && <div className="address-book__add-contact__error">{errorToRender}</div>} |
||||
</div> |
||||
</div> |
||||
<PageContainerFooter |
||||
cancelText={this.context.t('cancel')} |
||||
disabled={Boolean(this.state.error)} |
||||
onSubmit={() => { |
||||
addToAddressBook(this.state.ensAddress || this.state.ethAddress, this.state.newName) |
||||
history.push(CONTACT_LIST_ROUTE) |
||||
}} |
||||
onCancel={() => { |
||||
history.push(CONTACT_LIST_ROUTE) |
||||
}} |
||||
submitText={this.context.t('save')} |
||||
submitButtonType={'confirm'} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
import AddContact from './add-contact.component' |
||||
import { compose } from 'recompose' |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { addToAddressBook, showQrScanner, qrCodeDetected } from '../../../../store/actions' |
||||
import { |
||||
CONTACT_ADD_ROUTE, |
||||
} from '../../../../helpers/constants/routes' |
||||
import { |
||||
getQrCodeData, |
||||
} from '../../../../pages/send/send.selectors' |
||||
|
||||
const mapStateToProps = state => { |
||||
return { |
||||
qrCodeData: getQrCodeData(state), |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
addToAddressBook: (recipient, nickname) => dispatch(addToAddressBook(recipient, nickname)), |
||||
scanQrCode: () => dispatch(showQrScanner(CONTACT_ADD_ROUTE)), |
||||
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(AddContact) |
@ -0,0 +1 @@ |
||||
export { default } from './add-contact.container' |
@ -0,0 +1,132 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ContactList from '../../../components/app/contact-list' |
||||
import EditContact from './edit-contact' |
||||
import AddContact from './add-contact' |
||||
import ViewContact from './view-contact' |
||||
import MyAccounts from './my-accounts' |
||||
import { |
||||
CONTACT_ADD_ROUTE, |
||||
CONTACT_VIEW_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_ROUTE, |
||||
} from '../../../helpers/constants/routes' |
||||
|
||||
export default class ContactListTab extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
addressBook: PropTypes.array, |
||||
history: PropTypes.object, |
||||
selectedAddress: PropTypes.string, |
||||
viewingContact: PropTypes.bool, |
||||
editingContact: PropTypes.bool, |
||||
addingContact: PropTypes.bool, |
||||
showContactContent: PropTypes.bool, |
||||
hideAddressBook: PropTypes.bool, |
||||
showingMyAccounts: PropTypes.bool, |
||||
} |
||||
|
||||
renderAddresses () { |
||||
const { addressBook, history, selectedAddress } = this.props |
||||
const contacts = addressBook.filter(({ name }) => !!name) |
||||
const nonContacts = addressBook.filter(({ name }) => !name) |
||||
|
||||
return ( |
||||
<div> |
||||
<ContactList |
||||
searchForContacts={() => contacts} |
||||
searchForRecents={() => nonContacts} |
||||
selectRecipient={(address) => { |
||||
history.push(`${CONTACT_VIEW_ROUTE}/${address}`) |
||||
}} |
||||
selectedAddress={selectedAddress} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderAddButton () { |
||||
const { history } = this.props |
||||
return <div |
||||
className="address-book-add-button__button" |
||||
onClick={() => { |
||||
history.push(CONTACT_ADD_ROUTE) |
||||
}}> |
||||
<img |
||||
className="account-menu__item-icon" |
||||
src="images/plus-btn-white.svg" |
||||
/> |
||||
</div> |
||||
} |
||||
|
||||
renderMyAccountsButton () { |
||||
const { history } = this.props |
||||
const { t } = this.context |
||||
return ( |
||||
<div |
||||
className="address-book__my-accounts-button" |
||||
onClick={() => { |
||||
history.push(CONTACT_MY_ACCOUNTS_ROUTE) |
||||
}} |
||||
> |
||||
<div className="address-book__my-accounts-button__header">{t('myWalletAccounts')}</div> |
||||
<div className="address-book__my-accounts-button__content"> |
||||
<div className="address-book__my-accounts-button__text"> |
||||
{ t('myWalletAccountsDescription') } |
||||
</div> |
||||
<div className="address-book__my-accounts-button__caret" /> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderContactContent () { |
||||
const { viewingContact, editingContact, addingContact, showContactContent } = this.props |
||||
|
||||
if (!showContactContent) { |
||||
return null |
||||
} |
||||
|
||||
let ContactContentComponent = null |
||||
if (viewingContact) { |
||||
ContactContentComponent = ViewContact |
||||
} else if (editingContact) { |
||||
ContactContentComponent = EditContact |
||||
} else if (addingContact) { |
||||
ContactContentComponent = AddContact |
||||
} |
||||
|
||||
return (ContactContentComponent && <div className="address-book-contact-content"> |
||||
<ContactContentComponent /> |
||||
</div>) |
||||
} |
||||
|
||||
renderAddressBookContent () { |
||||
const { hideAddressBook, showingMyAccounts } = this.props |
||||
|
||||
if (!hideAddressBook && !showingMyAccounts) { |
||||
return (<div className="address-book"> |
||||
{ this.renderMyAccountsButton() } |
||||
{ this.renderAddresses() } |
||||
</div>) |
||||
} else if (!hideAddressBook && showingMyAccounts) { |
||||
return (<MyAccounts />) |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { addingContact } = this.props |
||||
|
||||
return ( |
||||
<div className="address-book-wrapper"> |
||||
{ this.renderAddressBookContent() } |
||||
{ this.renderContactContent() } |
||||
{!addingContact && <div className="address-book-add-button"> |
||||
{ this.renderAddButton() } |
||||
</div>} |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,54 @@ |
||||
import ContactListTab from './contact-list-tab.component' |
||||
import { compose } from 'recompose' |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { getAddressBook } from '../../../selectors/selectors' |
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' |
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util' |
||||
|
||||
import { |
||||
CONTACT_ADD_ROUTE, |
||||
CONTACT_EDIT_ROUTE, |
||||
CONTACT_VIEW_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_VIEW_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_EDIT_ROUTE, |
||||
} from '../../../helpers/constants/routes' |
||||
|
||||
|
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { location } = ownProps |
||||
const { pathname } = location |
||||
|
||||
const pathNameTail = pathname.match(/[^/]+$/)[0] |
||||
const pathNameTailIsAddress = pathNameTail.includes('0x') |
||||
|
||||
const viewingContact = Boolean(pathname.match(CONTACT_VIEW_ROUTE) || pathname.match(CONTACT_MY_ACCOUNTS_VIEW_ROUTE)) |
||||
const editingContact = Boolean(pathname.match(CONTACT_EDIT_ROUTE) || pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE)) |
||||
const addingContact = Boolean(pathname.match(CONTACT_ADD_ROUTE)) |
||||
const showingMyAccounts = Boolean( |
||||
pathname.match(CONTACT_MY_ACCOUNTS_ROUTE) || |
||||
pathname.match(CONTACT_MY_ACCOUNTS_VIEW_ROUTE) || |
||||
pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE) |
||||
) |
||||
const envIsPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP |
||||
|
||||
const hideAddressBook = envIsPopup && (viewingContact || editingContact || addingContact) |
||||
|
||||
return { |
||||
viewingContact, |
||||
editingContact, |
||||
addingContact, |
||||
showingMyAccounts, |
||||
addressBook: getAddressBook(state), |
||||
selectedAddress: pathNameTailIsAddress ? pathNameTail : '', |
||||
hideAddressBook, |
||||
envIsPopup, |
||||
showContactContent: !envIsPopup || hideAddressBook, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps) |
||||
)(ContactListTab) |
@ -0,0 +1,135 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Identicon from '../../../../components/ui/identicon' |
||||
import Button from '../../../../components/ui/button/button.component' |
||||
import TextField from '../../../../components/ui/text-field' |
||||
import { isValidAddress } from '../../../../helpers/utils/util' |
||||
import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' |
||||
|
||||
export default class EditContact extends PureComponent { |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
addToAddressBook: PropTypes.func, |
||||
removeFromAddressBook: PropTypes.func, |
||||
history: PropTypes.object, |
||||
name: PropTypes.string, |
||||
address: PropTypes.string, |
||||
memo: PropTypes.string, |
||||
viewRoute: PropTypes.string, |
||||
listRoute: PropTypes.string, |
||||
setAccountLabel: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
newName: '', |
||||
newAddress: '', |
||||
newMemo: '', |
||||
error: '', |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { history, name, addToAddressBook, removeFromAddressBook, address, memo, viewRoute, listRoute, setAccountLabel } = this.props |
||||
|
||||
return ( |
||||
<div className="settings-page__content-row address-book__edit-contact"> |
||||
<div className="settings-page__header address-book__header--edit"> |
||||
<Identicon address={address} diameter={60}/> |
||||
<Button |
||||
type="link" |
||||
className="settings-page__address-book-button" |
||||
onClick={() => { |
||||
removeFromAddressBook(address) |
||||
history.push(listRoute) |
||||
}} |
||||
> |
||||
{t('deleteAccount')} |
||||
</Button> |
||||
</div> |
||||
<div className="address-book__edit-contact__content"> |
||||
<div className="address-book__view-contact__group"> |
||||
<div className="address-book__view-contact__group__label"> |
||||
{ t('userName') } |
||||
</div> |
||||
<TextField |
||||
type="text" |
||||
id="nickname" |
||||
placeholder={this.context.t('addAlias')} |
||||
value={this.state.newName || name} |
||||
onChange={e => this.setState({ newName: e.target.value })} |
||||
fullWidth |
||||
margin="dense" |
||||
/> |
||||
</div> |
||||
|
||||
<div className="address-book__view-contact__group"> |
||||
<div className="address-book__view-contact__group__label"> |
||||
{ t('ethereumPublicAddress') } |
||||
</div> |
||||
<TextField |
||||
type="text" |
||||
id="address" |
||||
placeholder={address} |
||||
value={this.state.newAddress || address} |
||||
error={this.state.error} |
||||
onChange={e => this.setState({ newAddress: e.target.value })} |
||||
fullWidth |
||||
margin="dense" |
||||
/> |
||||
</div> |
||||
|
||||
<div className="address-book__view-contact__group"> |
||||
<div className="address-book__view-contact__group__label--capitalized"> |
||||
{ t('memo') } |
||||
</div> |
||||
<TextField |
||||
type="text" |
||||
id="memo" |
||||
placeholder={memo} |
||||
value={this.state.newMemo || memo} |
||||
onChange={e => this.setState({ newMemo: e.target.value })} |
||||
fullWidth |
||||
margin="dense" |
||||
multiline={true} |
||||
rows={3} |
||||
classes={{ |
||||
inputMultiline: 'address-book__view-contact__text-area', |
||||
inputRoot: 'address-book__view-contact__text-area-wrapper', |
||||
}} |
||||
/> |
||||
</div> |
||||
</div> |
||||
<PageContainerFooter |
||||
cancelText={this.context.t('cancel')} |
||||
onSubmit={() => { |
||||
if (this.state.newAddress !== '' && this.state.newAddress !== address) { |
||||
// if the user makes a valid change to the address field, remove the original address
|
||||
if (isValidAddress(this.state.newAddress)) { |
||||
removeFromAddressBook(address) |
||||
addToAddressBook(this.state.newAddress, this.state.newName || name, this.state.newMemo || memo) |
||||
setAccountLabel(this.state.newAddress, this.state.newName || name) |
||||
history.push(listRoute) |
||||
} else { |
||||
this.setState({ error: 'invalid address' }) |
||||
} |
||||
} else { |
||||
// update name
|
||||
addToAddressBook(address, this.state.newName || name, this.state.newMemo || memo) |
||||
setAccountLabel(address, this.state.newName || name) |
||||
history.push(listRoute) |
||||
} |
||||
}} |
||||
onCancel={() => { |
||||
history.push(`${viewRoute}/${address}`) |
||||
}} |
||||
submitText={this.context.t('save')} |
||||
submitButtonType={'confirm'} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
import EditContact from './edit-contact.component' |
||||
import { compose } from 'recompose' |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { getAddressBookEntry } from '../../../../selectors/selectors' |
||||
import { |
||||
CONTACT_VIEW_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_VIEW_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_EDIT_ROUTE, |
||||
CONTACT_LIST_ROUTE, |
||||
} from '../../../../helpers/constants/routes' |
||||
import { addToAddressBook, removeFromAddressBook, setAccountLabel } from '../../../../store/actions' |
||||
|
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { location } = ownProps |
||||
const { pathname } = location |
||||
const pathNameTail = pathname.match(/[^/]+$/)[0] |
||||
const pathNameTailIsAddress = pathNameTail.includes('0x') |
||||
const address = pathNameTailIsAddress ? pathNameTail.toLowerCase() : ownProps.match.params.id |
||||
|
||||
const { memo, name } = getAddressBookEntry(state, address) || state.metamask.identities[address] |
||||
|
||||
const showingMyAccounts = Boolean(pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE)) |
||||
|
||||
return { |
||||
address, |
||||
name, |
||||
memo, |
||||
viewRoute: showingMyAccounts ? CONTACT_MY_ACCOUNTS_VIEW_ROUTE : CONTACT_VIEW_ROUTE, |
||||
listRoute: showingMyAccounts ? CONTACT_MY_ACCOUNTS_ROUTE : CONTACT_LIST_ROUTE, |
||||
showingMyAccounts, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
addToAddressBook: (recipient, nickname, memo) => dispatch(addToAddressBook(recipient, nickname, memo)), |
||||
removeFromAddressBook: (addressToRemove) => dispatch(removeFromAddressBook(addressToRemove)), |
||||
setAccountLabel: (address, label) => dispatch(setAccountLabel(address, label)), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(EditContact) |
@ -0,0 +1 @@ |
||||
export { default } from './edit-contact.container' |
@ -0,0 +1 @@ |
||||
export { default } from './contact-list-tab.container' |
@ -0,0 +1,234 @@ |
||||
.address-book-wrapper { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
height: 100%; |
||||
} |
||||
|
||||
.address-book { |
||||
flex: 0.4 1 40%; |
||||
max-width: 40%; |
||||
|
||||
@media screen and (max-width: 576px) { |
||||
flex: 1; |
||||
max-width: 100%; |
||||
} |
||||
|
||||
&__entry { |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
padding: 16px 14px; |
||||
flex: 0 0 auto; |
||||
border-bottom: 1px solid #dedede; |
||||
|
||||
&:hover { |
||||
border: 1px solid #037DD6; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
|
||||
&__name { |
||||
padding: 3px; |
||||
} |
||||
|
||||
&__header, &__header--edit { |
||||
&__name { |
||||
font-family: Roboto; |
||||
font-style: normal; |
||||
font-weight: normal; |
||||
font-size: 24px; |
||||
line-height: 34px; |
||||
margin-left: 24px; |
||||
} |
||||
} |
||||
|
||||
&__header--edit { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
|
||||
.button { |
||||
justify-content: flex-end; |
||||
color: #D73A49; |
||||
font-size: 14px; |
||||
} |
||||
} |
||||
|
||||
&__input { |
||||
@extend %input-2; |
||||
margin-top: .25rem; |
||||
|
||||
&--address { |
||||
font-size: 0.875rem; |
||||
} |
||||
} |
||||
|
||||
&__view-contact { |
||||
&__text-area-wrapper { |
||||
height: 96px !important; |
||||
} |
||||
|
||||
&__text-area { |
||||
line-height: initial !important; |
||||
} |
||||
|
||||
&__group { |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
padding: 1.5rem 1.5rem 0 1.5rem; |
||||
|
||||
&__label, &__label--capitalized { |
||||
font-size: .75rem; |
||||
color: $Grey-500; |
||||
margin-bottom: .25rem; |
||||
} |
||||
|
||||
&__label--capitalized { |
||||
text-transform: capitalize; |
||||
} |
||||
|
||||
&__value, &__static-address { |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
font-size: 1.125rem; |
||||
color: $Grey-800; |
||||
word-break: break-word; |
||||
|
||||
&--address { |
||||
font-size: 0.875rem; |
||||
} |
||||
|
||||
&--copy-icon { |
||||
padding-left: 4px; |
||||
} |
||||
} |
||||
|
||||
&__static-address { |
||||
font-size: 0.875rem; |
||||
&--copy-icon { |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
color: black; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.unit-input__input { |
||||
max-width: 100%; |
||||
width: 100%; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&__edit-contact { |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
padding-bottom: 0 !important; |
||||
height: 100%; |
||||
|
||||
&__content { |
||||
flex: 1 1 auto; |
||||
|
||||
> div { |
||||
padding-top: 0; |
||||
} |
||||
|
||||
} |
||||
|
||||
.page-container__footer { |
||||
border-top: none; |
||||
} |
||||
} |
||||
|
||||
&__add-contact { |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
padding-bottom: 0 !important; |
||||
height: 100%; |
||||
|
||||
&__content { |
||||
flex: 1 1 auto; |
||||
height: 100%; |
||||
} |
||||
|
||||
&__error { |
||||
font-size: 12px; |
||||
line-height: 12px; |
||||
left: 8px; |
||||
color: $red; |
||||
} |
||||
} |
||||
|
||||
&__my-accounts-button { |
||||
display: flex; |
||||
flex-flow: column; |
||||
cursor: pointer; |
||||
padding: 15px; |
||||
|
||||
&:hover { |
||||
background-color: rgba(222, 222, 222, 0.2); |
||||
} |
||||
|
||||
&__header { |
||||
font-family: Roboto; |
||||
font-style: normal; |
||||
font-weight: normal; |
||||
font-size: 18px; |
||||
line-height: 25px; |
||||
color: #000000; |
||||
} |
||||
|
||||
&__content { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
&__text { |
||||
font-family: Roboto; |
||||
font-style: normal; |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
color: #6A737D; |
||||
} |
||||
|
||||
&__caret { |
||||
display: block; |
||||
background-image: url(/images/caret-right.svg); |
||||
width: 30px; |
||||
opacity: .5; |
||||
background-repeat: no-repeat; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.address-book-add-button { |
||||
&__button { |
||||
position: absolute; |
||||
top: 10px; |
||||
right: 16px; |
||||
height: 56px; |
||||
width: 56px; |
||||
border-radius: 18px; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
border-radius: 50%; |
||||
border-width: 2px; |
||||
background: #037DD6; |
||||
margin-right: 5px; |
||||
cursor: pointer; |
||||
box-shadow: 0px 2px 16px rgba(0, 0, 0, 0.25); |
||||
} |
||||
} |
||||
|
||||
.address-book--hidden { |
||||
display: none; |
||||
} |
||||
|
||||
.address-book-contact-content { |
||||
flex: 0.4 1 40%; |
||||
|
||||
@media screen and (max-width: 576px) { |
||||
flex: 1 |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './my-accounts.container' |
@ -0,0 +1,39 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ContactList from '../../../../components/app/contact-list' |
||||
import { CONTACT_MY_ACCOUNTS_VIEW_ROUTE } from '../../../../helpers/constants/routes' |
||||
|
||||
export default class ViewContact extends PureComponent { |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
myAccounts: PropTypes.array, |
||||
history: PropTypes.object, |
||||
} |
||||
|
||||
renderMyAccounts () { |
||||
const { myAccounts, history } = this.props |
||||
|
||||
return ( |
||||
<div> |
||||
<ContactList |
||||
searchForMyAccounts={() => myAccounts} |
||||
selectRecipient={(address) => { |
||||
history.push(`${CONTACT_MY_ACCOUNTS_VIEW_ROUTE}/${address}`) |
||||
}} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
return ( |
||||
<div className="address-book"> |
||||
{ this.renderMyAccounts() } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
import ViewContact from './my-accounts.component' |
||||
import { compose } from 'recompose' |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { accountsWithSendEtherInfoSelector } from '../../../../selectors/selectors' |
||||
|
||||
const mapStateToProps = (state,) => { |
||||
const myAccounts = accountsWithSendEtherInfoSelector(state) |
||||
|
||||
return { |
||||
myAccounts, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps) |
||||
)(ViewContact) |
@ -0,0 +1 @@ |
||||
export { default } from './view-contact.container' |
@ -0,0 +1,78 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Identicon from '../../../../components/ui/identicon' |
||||
|
||||
import Button from '../../../../components/ui/button/button.component' |
||||
import copyToClipboard from 'copy-to-clipboard' |
||||
|
||||
function quadSplit (address) { |
||||
return '0x ' + address.slice(2).match(/.{1,4}/g).join(' ') |
||||
} |
||||
|
||||
export default class ViewContact extends PureComponent { |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
removeFromAddressBook: PropTypes.func, |
||||
name: PropTypes.string, |
||||
address: PropTypes.string, |
||||
history: PropTypes.object, |
||||
checkSummedAddress: PropTypes.string, |
||||
memo: PropTypes.string, |
||||
editRoute: PropTypes.string, |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { history, name, address, checkSummedAddress, memo, editRoute } = this.props |
||||
|
||||
return ( |
||||
<div className="settings-page__content-row"> |
||||
<div className="settings-page__content-item"> |
||||
<div className="settings-page__header address-book__header"> |
||||
<Identicon address={address} diameter={60} /> |
||||
<div className="address-book__header__name">{ name }</div> |
||||
</div> |
||||
<div className="address-book__view-contact__group"> |
||||
<Button |
||||
type="secondary" |
||||
onClick={() => { |
||||
history.push(`${editRoute}/${address}`) |
||||
}} |
||||
> |
||||
{t('edit')} |
||||
</Button> |
||||
</div> |
||||
<div className="address-book__view-contact__group"> |
||||
<div className="address-book__view-contact__group__label"> |
||||
{ t('ethereumPublicAddress') } |
||||
</div> |
||||
<div className="address-book__view-contact__group__value"> |
||||
<div |
||||
className="address-book__view-contact__group__static-address" |
||||
> |
||||
{ quadSplit(checkSummedAddress) } |
||||
</div> |
||||
<img |
||||
className="address-book__view-contact__group__static-address--copy-icon" |
||||
onClick={() => copyToClipboard(checkSummedAddress)} |
||||
src="/images/copy-to-clipboard.svg" |
||||
/> |
||||
</div> |
||||
</div> |
||||
<div className="address-book__view-contact__group"> |
||||
<div className="address-book__view-contact__group__label--capitalized"> |
||||
{ t('memo') } |
||||
</div> |
||||
<div className="address-book__view-contact__group__static-address"> |
||||
{ memo } |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
import ViewContact from './view-contact.component' |
||||
import { compose } from 'recompose' |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { getAddressBookEntry } from '../../../../selectors/selectors' |
||||
import { removeFromAddressBook } from '../../../../store/actions' |
||||
import { checksumAddress } from '../../../../helpers/utils/util' |
||||
import { |
||||
CONTACT_EDIT_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_EDIT_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_VIEW_ROUTE, |
||||
} from '../../../../helpers/constants/routes' |
||||
|
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { location } = ownProps |
||||
const { pathname } = location |
||||
const pathNameTail = pathname.match(/[^/]+$/)[0] |
||||
const pathNameTailIsAddress = pathNameTail.includes('0x') |
||||
const address = pathNameTailIsAddress ? pathNameTail.toLowerCase() : ownProps.match.params.id |
||||
|
||||
const { memo, name } = getAddressBookEntry(state, address) || state.metamask.identities[address] |
||||
|
||||
const showingMyAccounts = Boolean(pathname.match(CONTACT_MY_ACCOUNTS_VIEW_ROUTE)) |
||||
|
||||
return { |
||||
name, |
||||
address, |
||||
checkSummedAddress: checksumAddress(address), |
||||
memo, |
||||
editRoute: showingMyAccounts ? CONTACT_MY_ACCOUNTS_EDIT_ROUTE : CONTACT_EDIT_ROUTE, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
removeFromAddressBook: (addressToRemove) => dispatch(removeFromAddressBook(addressToRemove)), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(ViewContact) |
@ -1 +1 @@ |
||||
export { default } from './settings.component' |
||||
export { default } from './settings.container' |
||||
|
@ -0,0 +1,92 @@ |
||||
import Settings from './settings.component' |
||||
import { compose } from 'recompose' |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { getAddressBookEntryName } from '../../selectors/selectors' |
||||
import { isValidAddress } from '../../helpers/utils/util' |
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums' |
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util' |
||||
|
||||
import { |
||||
ADVANCED_ROUTE, |
||||
SECURITY_ROUTE, |
||||
GENERAL_ROUTE, |
||||
ABOUT_US_ROUTE, |
||||
SETTINGS_ROUTE, |
||||
CONTACT_LIST_ROUTE, |
||||
CONTACT_ADD_ROUTE, |
||||
CONTACT_EDIT_ROUTE, |
||||
CONTACT_VIEW_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_EDIT_ROUTE, |
||||
CONTACT_MY_ACCOUNTS_VIEW_ROUTE, |
||||
} from '../../helpers/constants/routes' |
||||
|
||||
const ROUTES_TO_I18N_KEYS = { |
||||
[GENERAL_ROUTE]: 'general', |
||||
[ADVANCED_ROUTE]: 'advanced', |
||||
[SECURITY_ROUTE]: 'securityAndPrivacy', |
||||
[ABOUT_US_ROUTE]: 'about', |
||||
[CONTACT_LIST_ROUTE]: 'contactList', |
||||
[CONTACT_ADD_ROUTE]: 'newContact', |
||||
[CONTACT_EDIT_ROUTE]: 'editContact', |
||||
[CONTACT_VIEW_ROUTE]: 'viewContact', |
||||
[CONTACT_MY_ACCOUNTS_ROUTE]: 'myAccounts', |
||||
} |
||||
|
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { location } = ownProps |
||||
const { pathname } = location |
||||
const pathNameTail = pathname.match(/[^/]+$/)[0] |
||||
|
||||
const isAddressEntryPage = pathNameTail.includes('0x') |
||||
const isMyAccountsPage = pathname.match('my-accounts') |
||||
const isAddContactPage = Boolean(pathname.match(CONTACT_ADD_ROUTE)) |
||||
const isEditContactPage = Boolean(pathname.match(CONTACT_EDIT_ROUTE)) |
||||
const isEditMyAccountsContactPage = Boolean(pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE)) |
||||
|
||||
const isPopupView = getEnvironmentType(location.href) === ENVIRONMENT_TYPE_POPUP |
||||
const pathnameI18nKey = ROUTES_TO_I18N_KEYS[pathname] |
||||
|
||||
let backRoute |
||||
if (isMyAccountsPage && isAddressEntryPage) { |
||||
backRoute = CONTACT_MY_ACCOUNTS_ROUTE |
||||
} else if (isEditContactPage) { |
||||
backRoute = `${CONTACT_VIEW_ROUTE}/${pathNameTail}` |
||||
} else if (isEditMyAccountsContactPage) { |
||||
backRoute = `${CONTACT_MY_ACCOUNTS_VIEW_ROUTE}/${pathNameTail}` |
||||
} else if (isAddressEntryPage || isMyAccountsPage || isAddContactPage) { |
||||
backRoute = CONTACT_LIST_ROUTE |
||||
} else { |
||||
backRoute = SETTINGS_ROUTE |
||||
} |
||||
|
||||
let initialBreadCrumbRoute |
||||
let breadCrumbTextKey |
||||
let initialBreadCrumbKey |
||||
if (isMyAccountsPage) { |
||||
initialBreadCrumbRoute = CONTACT_LIST_ROUTE |
||||
breadCrumbTextKey = 'myWalletAccounts' |
||||
initialBreadCrumbKey = ROUTES_TO_I18N_KEYS[initialBreadCrumbRoute] |
||||
} |
||||
|
||||
const addressName = getAddressBookEntryName(state, isValidAddress(pathNameTail) ? pathNameTail : '') |
||||
|
||||
return { |
||||
isAddressEntryPage, |
||||
isMyAccountsPage, |
||||
backRoute, |
||||
currentPath: pathname, |
||||
isPopupView, |
||||
pathnameI18nKey, |
||||
addressName, |
||||
initialBreadCrumbRoute, |
||||
breadCrumbTextKey, |
||||
initialBreadCrumbKey, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps) |
||||
)(Settings) |
@ -0,0 +1,232 @@ |
||||
module.exports = { |
||||
'metamask': { |
||||
'isInitialized': true, |
||||
'isUnlocked': true, |
||||
'featureFlags': {'sendHexData': true}, |
||||
'rpcTarget': 'https://rawtestrpc.metamask.io/', |
||||
'identities': { |
||||
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { |
||||
'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', |
||||
'name': 'Send Account 1', |
||||
}, |
||||
'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { |
||||
'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', |
||||
'name': 'Send Account 2', |
||||
}, |
||||
'0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { |
||||
'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', |
||||
'name': 'Send Account 3', |
||||
}, |
||||
'0xd85a4b6a394794842887b8284293d69163007bbb': { |
||||
'address': '0xd85a4b6a394794842887b8284293d69163007bbb', |
||||
'name': 'Send Account 4', |
||||
}, |
||||
}, |
||||
'cachedBalances': {}, |
||||
'currentBlockGasLimit': '0x4c1878', |
||||
'currentCurrency': 'USD', |
||||
'conversionRate': 1200.88200327, |
||||
'conversionDate': 1489013762, |
||||
'nativeCurrency': 'ETH', |
||||
'frequentRpcList': [], |
||||
'network': '3', |
||||
'accounts': { |
||||
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { |
||||
'code': '0x', |
||||
'balance': '0x47c9d71831c76efe', |
||||
'nonce': '0x1b', |
||||
'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', |
||||
}, |
||||
'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { |
||||
'code': '0x', |
||||
'balance': '0x37452b1315889f80', |
||||
'nonce': '0xa', |
||||
'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', |
||||
}, |
||||
'0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { |
||||
'code': '0x', |
||||
'balance': '0x30c9d71831c76efe', |
||||
'nonce': '0x1c', |
||||
'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', |
||||
}, |
||||
'0xd85a4b6a394794842887b8284293d69163007bbb': { |
||||
'code': '0x', |
||||
'balance': '0x0', |
||||
'nonce': '0x0', |
||||
'address': '0xd85a4b6a394794842887b8284293d69163007bbb', |
||||
}, |
||||
}, |
||||
'addressBook': [ |
||||
{ |
||||
'address': '0x06195827297c7a80a443b6894d3bdb8824b43896', |
||||
'name': 'Address Book Account 1', |
||||
'chainId': '3', |
||||
}, |
||||
], |
||||
'tokens': [ |
||||
{ |
||||
'address': '0x1a195821297c7a80a433b6894d3bdb8824b43896', |
||||
'decimals': 18, |
||||
'symbol': 'ABC', |
||||
}, |
||||
{ |
||||
'address': '0x8d6b81208414189a58339873ab429b6c47ab92d3', |
||||
'decimals': 4, |
||||
'symbol': 'DEF', |
||||
}, |
||||
{ |
||||
'address': '0xa42084c8d1d9a2198631988579bb36b48433a72b', |
||||
'decimals': 18, |
||||
'symbol': 'GHI', |
||||
}, |
||||
], |
||||
'tokenExchangeRates': { |
||||
'def_eth': { |
||||
rate: 2.0, |
||||
}, |
||||
'ghi_eth': { |
||||
rate: 31.01, |
||||
}, |
||||
}, |
||||
'transactions': {}, |
||||
'selectedAddressTxList': [ |
||||
{ |
||||
'id': 'mockTokenTx1', |
||||
'txParams': { |
||||
'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3', |
||||
}, |
||||
'time': 1700000000000, |
||||
}, |
||||
{ |
||||
'id': 'mockTokenTx2', |
||||
'txParams': { |
||||
'to': '0xafaketokenaddress', |
||||
}, |
||||
'time': 1600000000000, |
||||
}, |
||||
{ |
||||
'id': 'mockTokenTx3', |
||||
'txParams': { |
||||
'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3', |
||||
}, |
||||
'time': 1500000000000, |
||||
}, |
||||
{ |
||||
'id': 'mockEthTx1', |
||||
'txParams': { |
||||
'to': '0xd85a4b6a394794842887b8284293d69163007bbb', |
||||
}, |
||||
'time': 1400000000000, |
||||
}, |
||||
], |
||||
'selectedTokenAddress': '0x8d6b81208414189a58339873ab429b6c47ab92d3', |
||||
'unapprovedMsgs': { |
||||
'0xabc': { id: 'unapprovedMessage1', 'time': 1650000000000 }, |
||||
'0xdef': { id: 'unapprovedMessage2', 'time': 1550000000000 }, |
||||
'0xghi': { id: 'unapprovedMessage3', 'time': 1450000000000 }, |
||||
}, |
||||
'unapprovedMsgCount': 0, |
||||
'unapprovedPersonalMsgs': {}, |
||||
'unapprovedPersonalMsgCount': 0, |
||||
'keyringTypes': [ |
||||
'Simple Key Pair', |
||||
'HD Key Tree', |
||||
], |
||||
'keyrings': [ |
||||
{ |
||||
'type': 'HD Key Tree', |
||||
'accounts': [ |
||||
'fdea65c8e26263f6d9a1b5de9555d2931a33b825', |
||||
'c5b8dbac4c1d3f152cdeb400e2313f309c410acb', |
||||
'2f8d4a878cfa04a6e60d46362f5644deab66572d', |
||||
], |
||||
}, |
||||
{ |
||||
'type': 'Simple Key Pair', |
||||
'accounts': [ |
||||
'0xd85a4b6a394794842887b8284293d69163007bbb', |
||||
], |
||||
}, |
||||
], |
||||
'selectedAddress': '0xd85a4b6a394794842887b8284293d69163007bbb', |
||||
'provider': { |
||||
'type': 'testnet', |
||||
}, |
||||
'shapeShiftTxList': [ |
||||
{ id: 'shapeShiftTx1', 'time': 1675000000000 }, |
||||
{ id: 'shapeShiftTx2', 'time': 1575000000000 }, |
||||
{ id: 'shapeShiftTx3', 'time': 1475000000000 }, |
||||
], |
||||
'lostAccounts': [], |
||||
'send': { |
||||
'gasLimit': '0xFFFF', |
||||
'gasPrice': '0xaa', |
||||
'gasTotal': '0xb451dc41b578', |
||||
'tokenBalance': 3434, |
||||
'from': { |
||||
'address': '0xabcdefg', |
||||
'balance': '0x5f4e3d2c1', |
||||
}, |
||||
'to': '0x987fedabc', |
||||
'amount': '0x080', |
||||
'memo': '', |
||||
'errors': { |
||||
'someError': null, |
||||
}, |
||||
'maxModeOn': false, |
||||
'editingTransactionId': 97531, |
||||
'forceGasMin': true, |
||||
}, |
||||
'unapprovedTxs': { |
||||
'4768706228115573': { |
||||
'id': 4768706228115573, |
||||
'time': 1487363153561, |
||||
'status': 'unapproved', |
||||
'gasMultiplier': 1, |
||||
'metamaskNetworkId': '3', |
||||
'txParams': { |
||||
'from': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', |
||||
'to': '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', |
||||
'value': '0xde0b6b3a7640000', |
||||
'metamaskId': 4768706228115573, |
||||
'metamaskNetworkId': '3', |
||||
'gas': '0x5209', |
||||
}, |
||||
'gasLimitSpecified': false, |
||||
'estimatedGas': '0x5209', |
||||
'txFee': '17e0186e60800', |
||||
'txValue': 'de0b6b3a7640000', |
||||
'maxCost': 'de234b52e4a0800', |
||||
'gasPrice': '4a817c800', |
||||
}, |
||||
}, |
||||
'currentLocale': 'en', |
||||
recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'], |
||||
}, |
||||
'appState': { |
||||
'menuOpen': false, |
||||
'currentView': { |
||||
'name': 'accountDetail', |
||||
'detailView': null, |
||||
'context': '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', |
||||
}, |
||||
'accountDetail': { |
||||
'subview': 'transactions', |
||||
}, |
||||
'modal': { |
||||
'modalState': {}, |
||||
'previousModalState': {}, |
||||
}, |
||||
'transForward': true, |
||||
'isLoading': false, |
||||
'warning': null, |
||||
'scrollToBottom': false, |
||||
'forgottenPassword': null, |
||||
}, |
||||
'identities': {}, |
||||
'send': { |
||||
'fromDropdownOpen': false, |
||||
'toDropdownOpen': false, |
||||
'errors': { 'someError': null }, |
||||
}, |
||||
} |
@ -0,0 +1,25 @@ |
||||
import assert from 'assert' |
||||
import selectors from '../selectors.js' |
||||
const { |
||||
getAddressBook, |
||||
} = selectors |
||||
import mockState from './selectors-test-data' |
||||
|
||||
describe('selectors', () => { |
||||
|
||||
describe('getAddressBook()', () => { |
||||
it('should return the address book', () => { |
||||
assert.deepEqual( |
||||
getAddressBook(mockState), |
||||
[ |
||||
{ |
||||
address: '0x06195827297c7a80a443b6894d3bdb8824b43896', |
||||
name: 'Address Book Account 1', |
||||
chainId: '3', |
||||
}, |
||||
], |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
}) |
Loading…
Reference in new issue