Metametrics (#6171)

* Add metametrics provider and util.

* Add backend api and state for participating in metametrics.

* Add frontend action for participating in metametrics.

* Add metametrics opt-in screen.

* Add metametrics events to first time flow.

* Add metametrics events for route changes

* Add metametrics events for send and confirm screens

* Add metametrics events to dropdowns, transactions, log in and out, settings, sig requests and main screen

* Ensures each log in is measured as a new visit by metametrics.

* Ensure metametrics is called with an empty string for dimensions params if specified

* Adds opt in metametrics modal after unlock for existing users

* Adds settings page toggle for opting in and out of MetaMetrics

* Switch metametrics dimensions to page level scope

* Lint, test and translation fixes for metametrics.

* Update design for metametrics opt-in screen

* Complete responsive styling of metametrics-opt-in modal

* Use new chart image on metrics opt in screens

* Incorporate the metametrics opt-in screen into the new onboarding flow

* Update e2e tests to accomodate metametrics changes

* Mock out metametrics network requests in integration tests

* Fix tx-list integration test to support metametrics provider.

* Send number of tokens and accounts data with every metametrics event.

* Update metametrics event descriptor schema and add new events.

* Fix import tos bug and send gas button bug due to metametrics changes.

* Various small fixes on the metametrics branch.

* Add origin custom variable type to metametrics.util

* Fix names of onboarding complete actions (metametrics).

* Fix names of Metrics Options actions (metametrics).

* Clean up code related to metametrics.

* Fix bad merge conflict resolution and improve promise handling in sendMetaMetrics event and confrim tx base

* Don't send a second metrics event if user has gone back during first time flow.

* Collect metametrics on going back from onboarding create/import.

* Add missing custom variable constants for metametrics

* Fix metametrics provider

* Make height of opt-in modal responsive.

* Adjust text content for opt-in modal.

* Update metametrics event names and clean up code in opt-in-modal

* Put phishing warning step next to last in onboarding flow

* Link terms of service on create and import screens of first time flow

* Add subtext to options on the onboarding select action screen.

* Fix styling of bullet points on end of onboarding screen.

* Combine phishing warning and congratulations screens.

* Fix placement of users if unlocking after an incomplete onboarding import flow.

* Fix capitalization in opt-in screen

* Fix last onboarding screen translations

* Add link to 'Learn More' on the last screen of onboarding

* Code clean up: metametrics branch

* Update e2e tests for phishing warning step removal

* e2e tests passing on metametrics branch

* Different tracking urls for metametrics on development and prod
feature/default_network_editable
Dan J Miller 6 years ago committed by GitHub
parent 1765864e40
commit c757366355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      app/_locales/en/messages.json
  2. 8
      app/images/metrics-chart.svg
  3. 44
      app/scripts/controllers/preferences.js
  4. 41
      app/scripts/metamask-controller.js
  5. 3
      development/states/tx-list-items.json
  6. 1
      test/e2e/beta/fetch-mocks.js
  7. 15
      test/e2e/beta/from-import-beta-ui.spec.js
  8. 31
      test/e2e/beta/metamask-beta-responsive-ui.spec.js
  9. 23
      test/e2e/beta/metamask-beta-ui.spec.js
  10. 8
      test/integration/lib/confirm-sig-requests.js
  11. 9
      test/integration/lib/currency-localization.js
  12. 2
      test/integration/lib/send-new-ui.js
  13. 8
      test/integration/lib/tx-list-items.js
  14. 66
      ui/app/actions.js
  15. 29
      ui/app/app.js
  16. 13
      ui/app/components/account-dropdowns.js
  17. 42
      ui/app/components/account-menu/account-menu.component.js
  18. 32
      ui/app/components/app-header/app-header.component.js
  19. 2
      ui/app/components/app-header/app-header.container.js
  20. 22
      ui/app/components/customize-gas-modal/index.js
  21. 22
      ui/app/components/dropdowns/account-details-dropdown.js
  22. 28
      ui/app/components/dropdowns/network-dropdown.js
  23. 12
      ui/app/components/menu-bar/menu-bar.component.js
  24. 3
      ui/app/components/menu-bar/menu-bar.container.js
  25. 25
      ui/app/components/modals/customize-gas/customize-gas.component.js
  26. 2
      ui/app/components/modals/index.scss
  27. 1
      ui/app/components/modals/metametrics-opt-in-modal/index.js
  28. 30
      ui/app/components/modals/metametrics-opt-in-modal/index.scss
  29. 135
      ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js
  30. 24
      ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js
  31. 19
      ui/app/components/modals/modal.js
  32. 83
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
  33. 5
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
  34. 18
      ui/app/components/pages/create-account/connect-hardware/index.js
  35. 15
      ui/app/components/pages/create-account/import-account/json.js
  36. 15
      ui/app/components/pages/create-account/import-account/private-key.js
  37. 24
      ui/app/components/pages/create-account/new-account.js
  38. 17
      ui/app/components/pages/first-time-flow/create-password/create-password.component.js
  39. 37
      ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
  40. 39
      ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js
  41. 17
      ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js
  42. 37
      ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js
  43. 16
      ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js
  44. 12
      ui/app/components/pages/first-time-flow/end-of-flow/index.scss
  45. 7
      ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
  46. 2
      ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
  47. 12
      ui/app/components/pages/first-time-flow/first-time-flow.component.js
  48. 2
      ui/app/components/pages/first-time-flow/first-time-flow.container.js
  49. 26
      ui/app/components/pages/first-time-flow/first-time-flow.selectors.js
  50. 7
      ui/app/components/pages/first-time-flow/index.scss
  51. 1
      ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js
  52. 136
      ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss
  53. 169
      ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
  54. 27
      ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
  55. 13
      ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
  56. 20
      ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
  57. 2
      ui/app/components/pages/first-time-flow/select-action/index.js
  58. 3
      ui/app/components/pages/first-time-flow/select-action/index.scss
  59. 22
      ui/app/components/pages/first-time-flow/select-action/select-action.component.js
  60. 23
      ui/app/components/pages/first-time-flow/select-action/select-action.container.js
  61. 12
      ui/app/components/pages/first-time-flow/welcome/welcome.component.js
  62. 3
      ui/app/components/pages/first-time-flow/welcome/welcome.container.js
  63. 16
      ui/app/components/pages/keychains/restore-vault.js
  64. 67
      ui/app/components/pages/settings/settings-tab/settings-tab.component.js
  65. 4
      ui/app/components/pages/settings/settings-tab/settings-tab.container.js
  66. 31
      ui/app/components/pages/unlock-page/unlock-page.component.js
  67. 4
      ui/app/components/pages/unlock-page/unlock-page.container.js
  68. 25
      ui/app/components/provider-page-container/provider-page-container.component.js
  69. 7
      ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
  70. 26
      ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
  71. 2
      ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
  72. 12
      ui/app/components/send/send-content/send-to-row/send-to-row.component.js
  73. 41
      ui/app/components/send/send-footer/send-footer.component.js
  74. 2
      ui/app/components/send/send-footer/send-footer.container.js
  75. 5
      ui/app/components/send/send-footer/tests/send-footer-component.test.js
  76. 2
      ui/app/components/send/send-footer/tests/send-footer-container.test.js
  77. 8
      ui/app/components/sender-to-recipient/sender-to-recipient.component.js
  78. 11
      ui/app/components/sidebars/sidebar.component.js
  79. 15
      ui/app/components/signature-request.js
  80. 12
      ui/app/components/token-cell.js
  81. 1
      ui/app/components/transaction-activity-log/transaction-activity-log.component.js
  82. 35
      ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
  83. 14
      ui/app/components/transaction-list-item/transaction-list-item.component.js
  84. 3
      ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
  85. 25
      ui/app/components/transaction-view-balance/transaction-view-balance.component.js
  86. 16
      ui/app/components/wallet-view.js
  87. 1
      ui/app/ducks/confirm-transaction.duck.js
  88. 106
      ui/app/metametrics/metametrics.provider.js
  89. 188
      ui/app/metametrics/metametrics.util.js
  90. 19
      ui/app/reducers/metamask.js
  91. 3
      ui/app/root.js
  92. 2
      ui/app/routes.js
  93. 56
      ui/app/selectors.js
  94. 2
      ui/app/selectors/confirm-transaction.js

@ -470,15 +470,21 @@
"message": "Tips on storing it safely"
},
"endOfFlowMessage3": {
"message": "Save a backup in multiple places"
"message": "Save a backup in multiple places."
},
"endOfFlowMessage4": {
"message": "Never tell anyone"
"message": "Never share the phrase with anyone."
},
"endOfFlowMessage5": {
"message": "If you need to back your seed phrase again, you can find it in Settings -> Security."
"message": "Be careful of phishing! MetaMask will never spontaneously ask for your seed phrase."
},
"endOfFlowMessage6": {
"message": "If you need to back your up seed phrase again, you can find it in Settings -> Security."
},
"endOfFlowMessage7": {
"message": "If you ever have questions or see something fishy, email support@metamask.io."
},
"endOfFlowMessage8": {
"message": "MetaMask cannot recover your seedphrase. Learn more."
},
"ensNameNotFound": {
@ -689,6 +695,9 @@
"importWallet": {
"message": "Import Wallet"
},
"importYourExisting": {
"message": "Import your existing wallet using a 12 word seed phrase"
},
"imported": {
"message": "Imported",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1000,6 +1009,12 @@
"originalTotal": {
"message": "Original Total"
},
"participateInMetaMetrics": {
"message": "Participate in MetaMetrics"
},
"participateInMetaMetricsDesciption": {
"message": "Participate in MetaMetrics to help us make MetaMask better"
},
"password": {
"message": "Password"
},
@ -1435,6 +1450,9 @@
"testFaucet": {
"message": "Test Faucet"
},
"thisWillCreate": {
"message": "This will create a new wallet and seed phrase"
},
"tips": {
"message": "Tips"
},

@ -0,0 +1,8 @@
<svg width="95" height="82" viewBox="0 0 95 82" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.848145" y="35.7778" width="17.7707" height="27.2222" rx="4" fill="#C4C4C4"/>
<rect x="24.542" y="17.8889" width="17.7707" height="45.1111" rx="4" fill="#C4C4C4"/>
<rect x="48.2363" width="17.7707" height="63" rx="4" fill="#C4C4C4"/>
<ellipse cx="74.5222" cy="61.0554" rx="19.6218" ry="20.6111" fill="white"/>
<path d="M74.2257 45.1111C66.0108 45.1111 59.3428 52.1153 59.3428 60.7444C59.3428 69.3736 66.0108 76.3777 74.2257 76.3777C82.4407 76.3777 89.1086 69.3736 89.1086 60.7444C89.1086 52.1153 82.4407 45.1111 74.2257 45.1111ZM82.4407 70.4382C81.8005 70.046 81.1071 69.7098 80.4136 69.4296C79.8268 69.2055 79.2934 68.9253 78.7066 68.7012C77.1063 67.9167 76.8929 67.3564 76.8929 67.3003C76.8929 67.2443 76.8929 67.1883 76.8929 67.1322V67.0762V66.3478C76.8929 66.3478 79.8802 66.4598 81.3204 64.9469C81.3204 64.9469 79.24 64.3306 79.6134 60.3522C79.9868 56.3738 79.1333 52.8997 76.1461 53.2359C76.1461 53.2359 74.8125 51.5549 72.1986 52.6196C71.2918 52.9558 68.8913 53.9083 68.998 59.3436C69.1047 64.8349 67.131 64.8349 67.131 64.8349C67.131 64.8349 68.1445 66.4038 71.6119 66.3478V66.684C71.6119 66.796 71.6119 66.9641 71.5585 67.0762C71.5585 67.1322 71.5585 67.1883 71.5585 67.2443C71.5585 67.3564 71.3451 67.9167 69.7448 68.6451C69.158 68.9253 68.6246 69.1494 68.0378 69.3736C67.3443 69.6537 66.6509 69.9899 66.0108 70.3822C63.4503 68.0288 61.8499 64.5547 61.8499 60.6884C61.8499 53.5161 67.3977 47.6886 74.2257 47.6886C81.0537 47.6886 86.6015 53.5161 86.6015 60.6884C86.6015 64.6107 85.0012 68.0848 82.4407 70.4382Z" fill="#C4C4C4"/>
<path d="M74.5518 64.0034H76.1523V65.5557H74.5518V64.0034ZM72.5913 60.1362C72.6343 59.1157 72.9906 58.3924 73.6602 57.9663C74.0827 57.6942 74.6019 57.5581 75.2178 57.5581C76.027 57.5581 76.6984 57.7515 77.2319 58.1382C77.769 58.5249 78.0376 59.0978 78.0376 59.8569C78.0376 60.3224 77.9212 60.7145 77.6885 61.0332C77.5524 61.2266 77.291 61.4736 76.9043 61.7744L76.5229 62.0698C76.3153 62.231 76.1774 62.4189 76.1094 62.6338C76.0664 62.7699 76.0431 62.9811 76.0396 63.2676H74.5894C74.6108 62.6624 74.6681 62.2453 74.7612 62.0161C74.8543 61.7834 75.0942 61.5166 75.481 61.2158L75.873 60.9097C76.002 60.813 76.1058 60.7074 76.1846 60.5928C76.3278 60.3958 76.3994 60.1792 76.3994 59.9429C76.3994 59.6707 76.3188 59.4237 76.1577 59.2017C76.0002 58.9761 75.7101 58.8633 75.2876 58.8633C74.8722 58.8633 74.5768 59.0011 74.4014 59.2769C74.2295 59.5526 74.1436 59.839 74.1436 60.1362H72.5913Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -1,6 +1,6 @@
const ObservableStore = require('obs-store')
const normalizeAddress = require('eth-sig-util').normalize
const { isValidAddress } = require('ethereumjs-util')
const { isValidAddress, sha3, bufferToHex } = require('ethereumjs-util')
const extend = require('xtend')
@ -42,6 +42,8 @@ class PreferencesController {
// perform sensitive operations.
featureFlags: {},
knownMethodData: {},
participateInMetaMetrics: null,
firstTimeFlowType: null,
currentLocale: opts.initLangCode,
identities: {},
lostIdentities: {},
@ -52,6 +54,8 @@ class PreferencesController {
},
completedOnboarding: false,
completedUiMigration: true,
metaMetricsId: null,
metaMetricsSendCount: 0,
}, opts.initState)
this.diagnostics = opts.diagnostics
@ -92,6 +96,44 @@ class PreferencesController {
this.store.updateState({ useBlockie: val })
}
/**
* Setter for the `participateInMetaMetrics` property
*
* @param {boolean} bool Whether or not the user wants to participate in MetaMetrics
* @returns {string|null} the string of the new metametrics id, or null if not set
*
*/
setParticipateInMetaMetrics (bool) {
this.store.updateState({ participateInMetaMetrics: bool })
let metaMetricsId = null
if (bool && !this.store.getState().metaMetricsId) {
metaMetricsId = bufferToHex(sha3(String(Date.now()) + String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER))))
this.store.updateState({ metaMetricsId })
} else if (bool === false) {
this.store.updateState({ metaMetricsId })
}
return metaMetricsId
}
setMetaMetricsSendCount (val) {
this.store.updateState({ metaMetricsSendCount: val })
}
getMetaMetricsSendCount () {
return this.store.getState().metaMetricsSendCount
}
/**
* Setter for the `firstTimeFlowType` property
*
* @param {String} type Indicates the type of first time flow - create or import - the user wishes to follow
*
*/
setFirstTimeFlowType (type) {
this.store.updateState({ firstTimeFlowType: type })
}
getSuggestedTokens () {
return this.store.getState().suggestedTokens
}

@ -385,6 +385,9 @@ module.exports = class MetamaskController extends EventEmitter {
getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this),
setUseBlockie: this.setUseBlockie.bind(this),
setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this),
setMetaMetricsSendCount: this.setMetaMetricsSendCount.bind(this),
setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this),
setCurrentLocale: this.setCurrentLocale.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
markPasswordForgotten: this.markPasswordForgotten.bind(this),
@ -1624,6 +1627,44 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
/**
* Sets whether or not the user will have usage data tracked with MetaMetrics
* @param {boolean} bool - True for users that wish to opt-in, false for users that wish to remain out.
* @param {Function} cb - A callback function called when complete.
*/
setParticipateInMetaMetrics (bool, cb) {
try {
const metaMetricsId = this.preferencesController.setParticipateInMetaMetrics(bool)
cb(null, metaMetricsId)
} catch (err) {
cb(err)
}
}
setMetaMetricsSendCount (val, cb) {
try {
this.preferencesController.setMetaMetricsSendCount(val)
cb(null)
} catch (err) {
cb(err)
}
}
/**
* Sets the type of first time flow the user wishes to follow: create or import
* @param {String} type - Indicates the type of first time flow the user wishes to follow
* @param {Function} cb - A callback function called when complete.
*/
setFirstTimeFlowType (type, cb) {
try {
this.preferencesController.setFirstTimeFlowType(type)
cb(null)
} catch (err) {
cb(err)
}
}
/**
* A method for setting a user's current locale, affecting the language rendered.
* @param {string} key - Locale identifier.

@ -1403,5 +1403,6 @@
],
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {}
}
},
"confirmTransaction": {}
}

File diff suppressed because one or more lines are too long

@ -72,6 +72,8 @@ describe('Using MetaMask with an existing account', function () {
'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); }'
@ -110,6 +112,12 @@ describe('Using MetaMask with an existing account', function () {
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)
@ -128,13 +136,6 @@ describe('Using MetaMask with an existing account', function () {
await delay(regularDelayMs)
})
it('clicks through the security warning screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Protect Your Keys!')]`))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.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'))

@ -18,6 +18,7 @@ const {
loadExtension,
verboseReportOnFailure,
} = require('./helpers')
const fetchMockResponses = require('./fetch-mocks.js')
describe('MetaMask', function () {
let extensionId
@ -61,6 +62,23 @@ describe('MetaMask', function () {
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); }'
)
})
afterEach(async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
const errors = await checkBrowserForConsoleErrors(driver)
@ -93,6 +111,12 @@ describe('MetaMask', function () {
await delay(largeDelayMs)
})
it('clicks the "I agree" option on the metametrics opt-in screen', async () => {
const optOutButton = await findElement(driver, By.css('.btn-confirm'))
optOutButton.click()
await delay(largeDelayMs)
})
it('accepts a secure password', async () => {
const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password'))
const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password'))
@ -108,13 +132,6 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('clicks through the security warning screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Protect Your Keys!')]`))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
let seedPhrase
it('reveals the seed phrase', async () => {

@ -76,6 +76,8 @@ describe('MetaMask', function () {
'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); }'
@ -114,6 +116,12 @@ describe('MetaMask', function () {
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('accepts a secure password', async () => {
const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password'))
const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password'))
@ -129,13 +137,6 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('clicks through the security warning screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Protect Your Keys!')]`))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
let seedPhrase
it('reveals the seed phrase', async () => {
@ -688,12 +689,16 @@ describe('MetaMask', function () {
})
it('rejects a transaction', async () => {
await delay(tinyDelayMs / 2)
const rejectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Reject')]`), 10000)
await delay(tinyDelayMs / 2)
await rejectButton.click()
await delay(regularDelayMs)
const navigationElement = await findElement(driver, By.css('.confirm-page-container-navigation'))
await delay(tinyDelayMs / 2)
const navigationText = await navigationElement.getText()
await delay(tinyDelayMs / 2)
assert.equal(navigationText.includes('3'), true, 'transaction rejected')
})
@ -1124,7 +1129,7 @@ describe('MetaMask', function () {
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/), 10000)
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
await walletBalance.click()
@ -1137,7 +1142,7 @@ describe('MetaMask', function () {
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElements(driver, By.css('.transaction-view-balance__primary-balance'))
await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /43\s*TST/))
await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /43\s*TST/), 10000)
}
})
})

@ -3,6 +3,7 @@ const {
timeout,
queryAsync,
} = require('../../lib/util')
const fetchMockResponses = require('../../e2e/beta/fetch-mocks.js')
QUnit.module('confirm sig requests')
@ -19,6 +20,13 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests')
reactTriggerChange(selectState[0])
global.fetch = (...args) => {
if (args[0].match(/chromeextensionmm/)) {
return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) })
}
return window.fetch(...args)
}
const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid')
if (pendingRequestItem[2]) {

@ -4,6 +4,7 @@ const {
queryAsync,
findAsync,
} = require('../../lib/util')
const fetchMockResponses = require('../../e2e/beta/fetch-mocks.js')
QUnit.module('currency localization')
@ -19,6 +20,14 @@ async function runCurrencyLocalizationTest (assert, done) {
console.log('*** start runCurrencyLocalizationTest')
const selectState = await queryAsync($, 'select')
selectState.val('currency localization')
global.fetch = (...args) => {
if (args[0].match(/chromeextensionmm/)) {
return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) })
}
return window.fetch(...args)
}
await timeout(1000)
reactTriggerChange(selectState[0])
await timeout(1000)

@ -32,6 +32,8 @@ async function runSendFlowTest (assert, done) {
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 window.fetch(...args)
}

@ -3,6 +3,7 @@ const {
queryAsync,
findAsync,
} = require('../../lib/util')
const fetchMockResponses = require('../../e2e/beta/fetch-mocks.js')
QUnit.module('tx list items')
@ -25,6 +26,13 @@ async function runTxListItemsTest (assert, done) {
selectState.val('tx list items')
reactTriggerChange(selectState[0])
global.fetch = (...args) => {
if (args[0].match(/chromeextensionmm/)) {
return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) })
}
return window.fetch(...args)
}
const metamaskLogo = await queryAsync($, '.app-header__logo-container')
assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click()

@ -300,6 +300,11 @@ var actions = {
SET_USE_BLOCKIE: 'SET_USE_BLOCKIE',
setUseBlockie,
SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS',
SET_METAMETRICS_SEND_COUNT: 'SET_METAMETRICS_SEND_COUNT',
setParticipateInMetaMetrics,
setMetaMetricsSendCount,
// locale
SET_CURRENT_LOCALE: 'SET_CURRENT_LOCALE',
SET_LOCALE_MESSAGES: 'SET_LOCALE_MESSAGES',
@ -348,6 +353,9 @@ var actions = {
approveProviderRequest,
rejectProviderRequest,
clearApprovedOrigins,
setFirstTimeFlowType,
SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE',
}
module.exports = actions
@ -2607,6 +2615,49 @@ function toggleAccountMenu () {
}
}
function setParticipateInMetaMetrics (val) {
return (dispatch) => {
log.debug(`background.setParticipateInMetaMetrics`)
return new Promise((resolve, reject) => {
background.setParticipateInMetaMetrics(val, (err, metaMetricsId) => {
log.debug(err)
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch({
type: actions.SET_PARTICIPATE_IN_METAMETRICS,
value: val,
})
resolve([val, metaMetricsId])
})
})
}
}
function setMetaMetricsSendCount (val) {
return (dispatch) => {
log.debug(`background.setMetaMetricsSendCount`)
return new Promise((resolve, reject) => {
background.setMetaMetricsSendCount(val, (err) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch({
type: actions.SET_METAMETRICS_SEND_COUNT,
value: val,
})
resolve(val)
})
})
}
}
function setUseBlockie (val) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@ -2693,3 +2744,18 @@ function clearApprovedOrigins () {
background.clearApprovedOrigins()
}
}
function setFirstTimeFlowType (type) {
return (dispatch) => {
log.debug(`background.setFirstTimeFlowType`)
background.setFirstTimeFlowType(type, (err) => {
if (err) {
return dispatch(actions.displayWarning(err.message))
}
})
dispatch({
type: actions.SET_FIRST_TIME_FLOW_TYPE,
value: type,
})
}
}

@ -15,6 +15,7 @@ const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu
const Sidebar = require('./components/sidebars').default
const { WALLET_VIEW_SIDEBAR } = require('./components/sidebars/sidebar.constants')
// other views
import Home from './components/pages/home'
@ -82,6 +83,20 @@ class App extends Component {
if (!currentCurrency) {
setCurrentCurrencyToUSD()
}
this.props.history.listen((locationObj, action) => {
if (action === 'PUSH') {
const url = `&url=${encodeURIComponent('http://www.metamask.io/metametrics' + locationObj.pathname)}`
this.context.metricsEvent({}, {
currentPath: '',
pathname: locationObj.pathname,
url,
pageOpts: {
hideDimensions: true,
},
})
}
})
}
renderRoutes () {
@ -159,6 +174,18 @@ class App extends Component {
this.getConnectingLabel(loadingMessage) : null
log.debug('Main ui render function')
const sidebarOnOverlayClose = sidebarType === WALLET_VIEW_SIDEBAR
? () => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Wallet Sidebar',
name: 'Closed Sidebare Via Overlay',
},
})
}
: null
const {
isOpen: sidebarIsOpen,
transitionName: sidebarTransitionName,
@ -198,6 +225,7 @@ class App extends Component {
transitionName={sidebarTransitionName}
type={sidebarType}
sidebarProps={sidebar.props}
onOverlayClose={sidebarOnOverlayClose}
/>
<NetworkDropdown
provider={provider}
@ -406,6 +434,7 @@ function mapDispatchToProps (dispatch, ownProps) {
App.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = compose(

@ -233,6 +233,7 @@ class AccountDropdowns extends Component {
}
render () {
const { metricsEvent } = this.context
const { style, enableAccountsSelector, enableAccountOptions } = this.props
const { optionsMenuActive, accountSelectorActive } = this.state
@ -272,6 +273,17 @@ class AccountDropdowns extends Component {
fontSize: '1.8em',
},
onClick: (event) => {
metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'userClick',
name: 'accountsOpenedMenu',
},
pageOpts: {
section: 'header',
component: 'accountDropdownIcon',
},
})
event.stopPropagation()
this.setState({
accountSelectorActive: false,
@ -318,6 +330,7 @@ const mapDispatchToProps = (dispatch) => {
AccountDropdowns.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = {

@ -20,6 +20,7 @@ import {
export default class AccountMenu extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -73,7 +74,16 @@ export default class AccountMenu extends PureComponent {
return (
<div
className="account-menu__account menu__item--clickable"
onClick={() => showAccountDetail(identity.address)}
onClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Main Menu',
name: 'Switched Account',
},
})
showAccountDetail(identity.address)
}}
key={identity.address}
>
<div className="account-menu__check-mark">
@ -197,6 +207,7 @@ export default class AccountMenu extends PureComponent {
lockMetamask,
history,
} = this.props
const { metricsEvent } = this.context
return (
<Menu
@ -230,6 +241,13 @@ export default class AccountMenu extends PureComponent {
<Item
onClick={() => {
toggleAccountMenu()
metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Main Menu',
name: 'Clicked Create Account',
},
})
history.push(NEW_ACCOUNT_ROUTE)
}}
icon={
@ -243,6 +261,13 @@ export default class AccountMenu extends PureComponent {
<Item
onClick={() => {
toggleAccountMenu()
metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Main Menu',
name: 'Clicked Import Account',
},
})
history.push(IMPORT_ACCOUNT_ROUTE)
}}
icon={
@ -256,7 +281,13 @@ export default class AccountMenu extends PureComponent {
<Item
onClick={() => {
toggleAccountMenu()
metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Main Menu',
name: 'Clicked Connect Hardware',
},
})
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
} else {
@ -286,6 +317,13 @@ export default class AccountMenu extends PureComponent {
onClick={() => {
toggleAccountMenu()
history.push(SETTINGS_ROUTE)
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Main Menu',
name: 'Opened Settings',
},
})
}}
icon={
<img

@ -18,10 +18,12 @@ export default class AppHeader extends PureComponent {
isUnlocked: PropTypes.bool,
hideNetworkIndicator: PropTypes.bool,
disabled: PropTypes.bool,
isAccountMenuOpen: PropTypes.bool,
}
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
handleNetworkIndicatorClick (event) {
@ -30,20 +32,40 @@ export default class AppHeader extends PureComponent {
const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props
return networkDropdownOpen === false
? showNetworkDropdown()
: hideNetworkDropdown()
if (networkDropdownOpen === false) {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Opened Network Menu',
},
})
showNetworkDropdown()
} else {
hideNetworkDropdown()
}
}
renderAccountMenu () {
const { isUnlocked, toggleAccountMenu, selectedAddress, disabled } = this.props
const { isUnlocked, toggleAccountMenu, selectedAddress, disabled, isAccountMenuOpen } = this.props
return isUnlocked && (
<div
className={classnames('account-menu__icon', {
'account-menu__icon--disabled': disabled,
})}
onClick={() => disabled || toggleAccountMenu()}
onClick={() => {
if (!disabled) {
!isAccountMenuOpen && this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Opened Main Menu',
},
})
toggleAccountMenu()
}
}}
>
<Identicon
address={selectedAddress}

@ -13,6 +13,7 @@ const mapStateToProps = state => {
provider,
selectedAddress,
isUnlocked,
isAccountMenuOpen,
} = metamask
return {
@ -21,6 +22,7 @@ const mapStateToProps = state => {
provider,
selectedAddress,
isUnlocked,
isAccountMenuOpen,
}
}

@ -3,6 +3,7 @@ const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const BigNumber = require('bignumber.js')
const actions = require('../../actions')
const GasModalCard = require('./gas-modal-card')
import Button from '../button'
@ -112,6 +113,7 @@ function CustomizeGasModal (props) {
CustomizeGasModal.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
@ -148,6 +150,7 @@ CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) {
}
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
const { metricsEvent } = this.context
const {
setGasPrice,
setGasLimit,
@ -159,6 +162,9 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
updateSendAmount,
updateSendErrors,
} = this.props
const {
originalState,
} = this.state
if (maxModeOn && !selectedToken) {
const maxAmount = subtractCurrencies(
@ -169,6 +175,22 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
updateSendAmount(maxAmount)
}
metricsEvent({
eventOpts: {
category: 'Activation',
action: 'userCloses',
name: 'closeCustomizeGas',
},
pageOpts: {
section: 'customizeGasModal',
component: 'customizeGasSaveButton',
},
customVariables: {
gasPriceChange: (new BigNumber(ethUtil.addHexPrefix(gasPrice))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasPrice))).toString(10),
gasLimitChange: (new BigNumber(ethUtil.addHexPrefix(gasLimit))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasLimit))).toString(10),
},
})
setGasPrice(ethUtil.addHexPrefix(gasPrice))
setGasLimit(ethUtil.addHexPrefix(gasLimit))
setGasTotal(ethUtil.addHexPrefix(gasTotal))

@ -10,6 +10,7 @@ const { Menu, Item, CloseArea } = require('./components/menu')
AccountDetailsDropdown.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsDropdown)
@ -72,6 +73,13 @@ AccountDetailsDropdown.prototype.render = function () {
h(Item, {
onClick: (e) => {
e.stopPropagation()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked Expand View',
},
})
global.platform.openExtensionInBrowser()
this.props.onClose()
},
@ -82,6 +90,13 @@ AccountDetailsDropdown.prototype.render = function () {
onClick: (e) => {
e.stopPropagation()
showAccountDetailModal()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Viewed Account Details',
},
})
this.props.onClose()
},
text: this.context.t('accountDetails'),
@ -90,6 +105,13 @@ AccountDetailsDropdown.prototype.render = function () {
h(Item, {
onClick: (e) => {
e.stopPropagation()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked View on Etherscan',
},
})
viewOnEtherscan(address, network)
this.props.onClose()
},

@ -60,6 +60,7 @@ function NetworkDropdown () {
NetworkDropdown.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = compose(
@ -120,7 +121,7 @@ NetworkDropdown.prototype.render = function () {
{
key: 'main',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('mainnet'),
onClick: () => this.handleClick('mainnet'),
style: { ...dropdownMenuItemStyle, borderColor: '#038789' },
},
[
@ -142,7 +143,7 @@ NetworkDropdown.prototype.render = function () {
{
key: 'ropsten',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('ropsten'),
onClick: () => this.handleClick('ropsten'),
style: dropdownMenuItemStyle,
},
[
@ -164,7 +165,7 @@ NetworkDropdown.prototype.render = function () {
{
key: 'kovan',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('kovan'),
onClick: () => this.handleClick('kovan'),
style: dropdownMenuItemStyle,
},
[
@ -186,7 +187,7 @@ NetworkDropdown.prototype.render = function () {
{
key: 'rinkeby',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('rinkeby'),
onClick: () => this.handleClick('rinkeby'),
style: dropdownMenuItemStyle,
},
[
@ -208,7 +209,7 @@ NetworkDropdown.prototype.render = function () {
{
key: 'default',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('localhost'),
onClick: () => this.handleClick('localhost'),
style: dropdownMenuItemStyle,
},
[
@ -252,6 +253,23 @@ NetworkDropdown.prototype.render = function () {
])
}
NetworkDropdown.prototype.handleClick = function (newProviderType) {
const { providerType, setProviderType } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Opened Network Menu',
},
customVariables: {
fromNetwork: providerType,
toNetwork: newProviderType,
},
})
setProviderType(newProviderType)
}
NetworkDropdown.prototype.getNetworkName = function () {
const { provider } = this.props

@ -7,6 +7,7 @@ import AccountDetailsDropdown from '../dropdowns/account-details-dropdown.js'
export default class MenuBar extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -31,7 +32,16 @@ export default class MenuBar extends PureComponent {
>
<div
className="fa fa-bars menu-bar__sidebar-button"
onClick={() => sidebarOpen ? hideSidebar() : showSidebar()}
onClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Copied Address',
},
})
sidebarOpen ? hideSidebar() : showSidebar()
}}
/>
</Tooltip>
<SelectedAccount />

@ -1,4 +1,5 @@
import { connect } from 'react-redux'
import { WALLET_VIEW_SIDEBAR } from '../sidebars/sidebar.constants'
import MenuBar from './menu-bar.component'
import { showSidebar, hideSidebar } from '../../actions'
@ -16,7 +17,7 @@ const mapDispatchToProps = dispatch => {
showSidebar: () => {
dispatch(showSidebar({
transitionName: 'sidebar-right',
type: 'wallet-view',
type: WALLET_VIEW_SIDEBAR,
}))
},
hideSidebar: () => dispatch(hideSidebar()),

@ -1,5 +1,6 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import BigNumber from 'bignumber.js'
import GasModalCard from '../../customize-gas-modal/gas-modal-card'
import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
import Button from '../../button'
@ -14,6 +15,7 @@ import {
export default class CustomizeGas extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -73,9 +75,9 @@ export default class CustomizeGas extends Component {
}
render () {
const { t } = this.context
const { t, metricsEvent } = this.context
const { hideModal } = this.props
const { gasPrice, gasLimit } = this.state
const { gasPrice, gasLimit, originalGasPrice, originalGasLimit } = this.state
const { valid, errorKey } = this.validate()
return (
@ -128,7 +130,24 @@ export default class CustomizeGas extends Component {
<Button
type="primary"
className="customize-gas__save"
onClick={() => this.handleSave()}
onClick={() => {
metricsEvent({
eventOpts: {
category: 'Activation',
action: 'userCloses',
name: 'closeCustomizeGas',
},
pageOpts: {
section: 'customizeGasModal',
component: 'customizeGasSaveButton',
},
customVariables: {
gasPriceChange: (new BigNumber(gasPrice)).minus(new BigNumber(originalGasPrice)).toString(10),
gasLimitChange: (new BigNumber(gasLimit)).minus(new BigNumber(originalGasLimit)).toString(10),
},
})
this.handleSave()
}}
style={{ marginRight: '10px' }}
disabled={!valid}
>

@ -7,3 +7,5 @@
@import './qr-scanner/index';
@import './transaction-confirmed/index';
@import './metametrics-opt-in-modal/index';

@ -0,0 +1 @@
export { default } from './metametrics-opt-in-modal.container'

@ -0,0 +1,30 @@
.metametrics-opt-in-modal {
.metametrics-opt-in__main {
justify-content: center;
margin-left: 3%;
margin-right: 0%;
max-height: 75vh;
@media screen and (max-width: 575px) {
max-height: 100vh;
}
}
.metametrics-opt-in__title {
font-size: 38px;
}
.metametrics-opt-in__content {
padding-right: 6px;
}
.metametrics-opt-in__footer {
@media screen and (max-width: 575px) {
margin-top: 10px;
justify-content: center;
margin-left: 2%;
max-height: 520px;
}
}
}

@ -0,0 +1,135 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainerFooter from '../../page-container/page-container-footer'
export default class MetaMetricsOptInModal extends Component {
static propTypes = {
setParticipateInMetaMetrics: PropTypes.func,
hideModal: PropTypes.func,
}
static contextTypes = {
metricsEvent: PropTypes.func,
}
render () {
const { metricsEvent } = this.context
const { setParticipateInMetaMetrics, hideModal } = this.props
return (
<div className="metametrics-opt-in metametrics-opt-in-modal">
<div className="metametrics-opt-in__main">
<div className="metametrics-opt-in__content">
<div className="app-header__logo-container">
<img
className="app-header__metafox-logo app-header__metafox-logo--horizontal"
src="/images/logo/metamask-logo-horizontal.svg"
height={30}
/>
<img
className="app-header__metafox-logo app-header__metafox-logo--icon"
src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
</div>
<div className="metametrics-opt-in__body-graphic">
<img src="images/metrics-chart.svg" />
</div>
<div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
<div className="metametrics-opt-in__body">
<div className="metametrics-opt-in__description">
MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
will be used to continually improve the usability and user experience of our product and the etheruem ecosystem.
</div>
<div className="metametrics-opt-in__description">
MetaMask will..
</div>
<div className="metametrics-opt-in__committments">
<div className="metametrics-opt-in__row">
<i className="fa fa-check" />
<div className="metametrics-opt-in__row-description">
Always allow you to opt-out via Settings
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-check" />
<div className="metametrics-opt-in__row-description">
Send anonymized click & pageview events
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-check" />
<div className="metametrics-opt-in__row-description">
Maintain a public aggregate dashboard to educate the community
</div>
</div>
<div className="metametrics-opt-in__row metametrics-opt-in__break-row">
<i className="fa fa-times" />
<div className="metametrics-opt-in__row-description">
<span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-times" />
<div className="metametrics-opt-in__row-description">
<span className="metametrics-opt-in__bold">Never</span> collect your full IP address
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-times" />
<div className="metametrics-opt-in__row-description">
<span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
</div>
</div>
</div>
</div>
<div className="metametrics-opt-in__bottom-text">
This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our Privacy Policy here.
</div>
</div>
<div className="metametrics-opt-in__footer">
<PageContainerFooter
onCancel={() => {
setParticipateInMetaMetrics(false)
.then(() => {
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt Out',
},
isOptIn: true,
}, {
excludeMetaMetricsId: true,
})
hideModal()
})
}}
cancelText={'No Thanks'}
hideCancel={false}
onSubmit={() => {
setParticipateInMetaMetrics(true)
.then(() => {
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt In',
},
isOptIn: true,
})
hideModal()
})
}}
submitText={'I agree'}
submitButtonType={'confirm'}
disabled={false}
/>
</div>
</div>
</div>
)
}
}

@ -0,0 +1,24 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import MetaMetricsOptInModal from './metametrics-opt-in-modal.component'
import withModalProps from '../../../higher-order-components/with-modal-props'
import { setParticipateInMetaMetrics } from '../../../actions'
const mapStateToProps = (state, ownProps) => {
const { unapprovedTxCount } = ownProps
return {
unapprovedTxCount,
}
}
const mapDispatchToProps = dispatch => {
return {
setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
}
}
export default compose(
withModalProps,
connect(mapStateToProps, mapDispatchToProps),
)(MetaMetricsOptInModal)

@ -25,6 +25,8 @@ import ConfirmRemoveAccount from './confirm-remove-account'
import ConfirmResetAccount from './confirm-reset-account'
import TransactionConfirmed from './transaction-confirmed'
import CancelTransaction from './cancel-transaction'
import MetaMetricsOptInModal from './metametrics-opt-in-modal'
import RejectTransactions from './reject-transactions'
import ClearApprovedOrigins from './clear-approved-origins'
import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
@ -213,6 +215,23 @@ const MODALS = {
},
},
METAMETRICS_OPT_IN_MODAL: {
contents: h(MetaMetricsOptInModal),
mobileModalStyle: {
...modalContainerMobileStyle,
width: '100%',
height: '100%',
top: '0px',
},
laptopModalStyle: {
...modalContainerLaptopStyle,
top: '10%',
},
contentStyle: {
borderRadius: '8px',
},
},
OLD_UI_NOTIFICATION_MODAL: {
contents: [
h(NotifcationModal, {

@ -16,6 +16,7 @@ import AdvancedGasInputs from '../../gas-customization/advanced-gas-inputs'
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -77,6 +78,8 @@ export default class ConfirmTransactionBase extends Component {
onEdit: PropTypes.func,
onEditGas: PropTypes.func,
onSubmit: PropTypes.func,
setMetaMetricsSendCount: PropTypes.func,
metaMetricsSendCount: PropTypes.number,
subtitle: PropTypes.string,
subtitleComponent: PropTypes.node,
summaryComponent: PropTypes.node,
@ -154,7 +157,20 @@ export default class ConfirmTransactionBase extends Component {
}
handleEditGas () {
const { onEditGas, showCustomizeGasModal } = this.props
const { onEditGas, showCustomizeGasModal, methodData = {}, txData: { origin } } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'User clicks "Edit" on gas',
},
customVariables: {
recipientKnown: null,
functionType: methodData.name || 'notFound',
origin,
},
})
if (onEditGas) {
onEditGas()
@ -274,7 +290,21 @@ export default class ConfirmTransactionBase extends Component {
}
handleEdit () {
const { txData, tokenData, tokenProps, onEdit } = this.props
const { txData, tokenData, tokenProps, onEdit, methodData = {}, txData: { origin } } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Edit Transaction',
},
customVariables: {
recipientKnown: null,
functionType: methodData.name || 'notFound',
origin,
},
})
onEdit({ txData, tokenData, tokenProps })
}
@ -298,9 +328,22 @@ export default class ConfirmTransactionBase extends Component {
}
handleCancel () {
const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props
const { metricsEvent } = this.context
const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, methodData = {}, txData: { origin } } = this.props
if (onCancel) {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Cancel',
},
customVariables: {
recipientKnown: null,
functionType: methodData.name || 'notFound',
origin,
},
})
onCancel(txData)
} else {
cancelTransaction(txData)
@ -312,7 +355,8 @@ export default class ConfirmTransactionBase extends Component {
}
handleSubmit () {
const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props
const { metricsEvent } = this.context
const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, methodData = {}, metaMetricsSendCount = 0, setMetaMetricsSendCount } = this.props
const { submitting } = this.state
if (submitting) {
@ -323,6 +367,21 @@ export default class ConfirmTransactionBase extends Component {
submitting: true,
submitError: null,
}, () => {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Transaction Completed',
},
customVariables: {
recipientKnown: null,
functionType: methodData.name || 'notFound',
origin,
},
})
setMetaMetricsSendCount(metaMetricsSendCount + 1)
.then(() => {
if (onSubmit) {
Promise.resolve(onSubmit(txData))
.then(() => {
@ -348,6 +407,7 @@ export default class ConfirmTransactionBase extends Component {
})
}
})
})
}
renderTitleComponent () {
@ -413,6 +473,21 @@ export default class ConfirmTransactionBase extends Component {
}
}
componentDidMount () {
const { txData: { origin } = {} } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Confirm: Started',
},
customVariables: {
origin,
},
})
}
render () {
const {
isTxReprice,

@ -8,7 +8,7 @@ import {
clearConfirmTransaction,
updateGasAndCalculate,
} from '../../../ducks/confirm-transaction.duck'
import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal } from '../../../actions'
import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount } from '../../../actions'
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
GAS_LIMIT_TOO_LOW_ERROR_KEY,
@ -66,6 +66,7 @@ const mapStateToProps = (state, props) => {
assetImages,
network,
unapprovedTxs,
metaMetricsSendCount,
} = metamask
const assetImage = assetImages[txParamsToAddress]
@ -139,6 +140,7 @@ const mapStateToProps = (state, props) => {
insufficientBalance,
hideSubtitle: (!isMainnet && !showFiatInTestnets),
hideFiatConversion: (!isMainnet && !showFiatInTestnets),
metaMetricsSendCount,
}
}
@ -161,6 +163,7 @@ const mapDispatchToProps = dispatch => {
cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)),
sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)),
}
}

@ -154,8 +154,25 @@ class ConnectHardwareForm extends Component {
this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
.then(_ => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Connected Hardware Wallet',
name: 'Connected Account with: ' + device,
},
})
this.props.history.push(DEFAULT_ROUTE)
}).catch(e => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Connected Hardware Wallet',
name: 'Error connecting hardware wallet',
},
customVariables: {
error: e.toString(),
},
})
this.setState({ error: e.toString() })
})
}
@ -268,6 +285,7 @@ const mapDispatchToProps = dispatch => {
ConnectHardwareForm.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(

@ -108,9 +108,23 @@ class JsonImportSubview extends Component {
.then(({ selectedAddress }) => {
if (selectedAddress) {
history.push(DEFAULT_ROUTE)
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Import Account',
name: 'Imported Account with JSON',
},
})
displayWarning(null)
} else {
displayWarning('Error importing account.')
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Import Account',
name: 'Error importing JSON',
},
})
setSelectedAddress(firstAddress)
}
})
@ -147,6 +161,7 @@ const mapDispatchToProps = dispatch => {
JsonImportSubview.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = compose(

@ -12,6 +12,7 @@ import Button from '../../../button'
PrivateKeyImportView.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = compose(
@ -102,10 +103,24 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
importNewAccount('Private Key', [ privateKey ])
.then(({ selectedAddress }) => {
if (selectedAddress) {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Import Account',
name: 'Imported Account with Private Key',
},
})
history.push(DEFAULT_ROUTE)
displayWarning(null)
} else {
displayWarning('Error importing account.')
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Import Account',
name: 'Error importing with Private Key',
},
})
setSelectedAddress(firstAddress)
}
})

@ -52,7 +52,28 @@ class NewAccountCreateForm extends Component {
className: 'new-account-create-form__button',
onClick: () => {
createAccount(newAccountName || defaultAccountName)
.then(() => history.push(DEFAULT_ROUTE))
.then(() => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Add New Account',
name: 'Added New Account',
},
})
history.push(DEFAULT_ROUTE)
})
.catch((e) => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Add New Account',
name: 'Error',
},
customVariables: {
errorMessage: e.message,
},
})
})
},
}, [this.context.t('create')]),
@ -102,6 +123,7 @@ const mapDispatchToProps = dispatch => {
NewAccountCreateForm.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)

@ -3,18 +3,16 @@ import PropTypes from 'prop-types'
import { Switch, Route } from 'react-router-dom'
import NewAccount from './new-account'
import ImportWithSeedPhrase from './import-with-seed-phrase'
import UniqueImage from './unique-image'
import {
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_END_OF_FLOW_ROUTE,
} from '../../../../routes'
export default class CreatePassword extends PureComponent {
static propTypes = {
history: PropTypes.object,
isInitialized: PropTypes.bool,
isImportedKeyring: PropTypes.bool,
onCreateNewAccount: PropTypes.func,
onCreateNewAccountFromSeed: PropTypes.func,
}
@ -23,12 +21,12 @@ export default class CreatePassword extends PureComponent {
const { isInitialized, history } = this.props
if (isInitialized) {
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
}
}
render () {
const { onCreateNewAccount, onCreateNewAccountFromSeed, isImportedKeyring } = this.props
const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props
return (
<div className="first-time-flow__wrapper">
@ -46,15 +44,6 @@ export default class CreatePassword extends PureComponent {
/>
</div>
<Switch>
<Route exact
path={INITIALIZE_UNIQUE_IMAGE_ROUTE}
render={props => (
<UniqueImage
{ ...props }
isImportedKeyring={isImportedKeyring}
/>
)}
/>
<Route
exact
path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}

@ -5,12 +5,13 @@ import TextField from '../../../../text-field'
import Button from '../../../../button'
import {
INITIALIZE_SELECT_ACTION_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_END_OF_FLOW_ROUTE,
} from '../../../../../routes'
export default class ImportWithSeedPhrase extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -104,7 +105,14 @@ export default class ImportWithSeedPhrase extends PureComponent {
try {
await onSubmit(password, seedPhrase)
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Import Seed Phrase',
name: 'Import Complete',
},
})
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
} catch (error) {
this.setState({ seedPhraseError: error.message })
}
@ -132,6 +140,14 @@ export default class ImportWithSeedPhrase extends PureComponent {
}
toggleTermsCheck = () => {
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Import Seed Phrase',
name: 'Check ToS',
},
})
this.setState((prevState) => ({
termsChecked: !prevState.termsChecked,
}))
@ -150,6 +166,13 @@ export default class ImportWithSeedPhrase extends PureComponent {
<a
onClick={e => {
e.preventDefault()
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Import Seed Phrase',
name: 'Go Back from Onboarding Import',
},
})
this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
}}
href="#"
@ -208,7 +231,15 @@ export default class ImportWithSeedPhrase extends PureComponent {
{termsChecked ? <i className="fa fa-check fa-2x" /> : null}
</div>
<span className="first-time-flow__checkbox-label">
{ t('agreeTermsOfService') }
I have read and agree to the <a
href="https://metamask.io/terms.html"
target="_blank"
rel="noopener noreferrer"
>
<span className="first-time-flow__link-text">
{ 'Terms of Use' }
</span>
</a>
</span>
</div>
<Button

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../../../button'
import {
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_SEED_PHRASE_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_SELECT_ACTION_ROUTE,
} from '../../../../../routes'
@ -10,6 +10,7 @@ import TextField from '../../../../text-field'
export default class NewAccount extends PureComponent {
static contextTypes = {
metricsEvent: PropTypes.func,
t: PropTypes.func,
}
@ -99,7 +100,16 @@ export default class NewAccount extends PureComponent {
try {
await onSubmit(password)
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Create Password',
name: 'Submit Password',
},
})
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
} catch (error) {
this.setState({ passwordError: error.message })
}
@ -113,6 +123,14 @@ export default class NewAccount extends PureComponent {
}
toggleTermsCheck = () => {
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Create Password',
name: 'Check ToS',
},
})
this.setState((prevState) => ({
termsChecked: !prevState.termsChecked,
}))
@ -128,6 +146,13 @@ export default class NewAccount extends PureComponent {
<a
onClick={e => {
e.preventDefault()
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Create Password',
name: 'Go Back from Onboarding Create',
},
})
this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
}}
href="#"
@ -174,7 +199,15 @@ export default class NewAccount extends PureComponent {
{termsChecked ? <i className="fa fa-check fa-2x" /> : null}
</div>
<span className="first-time-flow__checkbox-label">
I agree to the Terms Of Service
I have read and agree to the <a
href="https://metamask.io/terms.html"
target="_blank"
rel="noopener noreferrer"
>
<span className="first-time-flow__link-text">
{ 'Terms of Use' }
</span>
</a>
</span>
</div>
<Button

@ -1,21 +1,21 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../../../button'
import { INITIALIZE_SEED_PHRASE_ROUTE, INITIALIZE_END_OF_FLOW_ROUTE } from '../../../../../routes'
import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../../../../routes'
export default class UniqueImageScreen extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
history: PropTypes.object,
isImportedKeyring: PropTypes.bool,
}
render () {
const { t } = this.context
const { history, isImportedKeyring } = this.props
const { history } = this.props
return (
<div>
@ -37,11 +37,14 @@ export default class UniqueImageScreen extends PureComponent {
type="confirm"
className="first-time-flow__button"
onClick={() => {
if (isImportedKeyring) {
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Agree to Phishing Warning',
name: 'Agree to Phishing Warning',
},
})
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
} else {
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
}
}}
>
{ t('next') }

@ -6,16 +6,18 @@ import { DEFAULT_ROUTE } from '../../../../routes'
export default class EndOfFlowScreen extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
history: PropTypes.object,
completeOnboarding: PropTypes.func,
completionMetaMetricsName: PropTypes.string,
}
render () {
const { t } = this.context
const { history, completeOnboarding } = this.props
const { history, completeOnboarding, completionMetaMetricsName } = this.props
return (
<div className="end-of-flow">
@ -42,23 +44,44 @@ export default class EndOfFlowScreen extends PureComponent {
<div className="first-time-flow__text-block end-of-flow__text-2">
{ t('endOfFlowMessage2') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-3">
<div className="end-of-flow__text-3">
{ '• ' + t('endOfFlowMessage3') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-4">
<div className="end-of-flow__text-3">
{ '• ' + t('endOfFlowMessage4') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-3">
{ t('endOfFlowMessage5') }
<div className="end-of-flow__text-3">
{ '• ' + t('endOfFlowMessage5') }
</div>
<div className="end-of-flow__text-3">
{ '• ' + t('endOfFlowMessage6') }
</div>
<div className="end-of-flow__text-3">
{ '• ' + t('endOfFlowMessage7') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-3">
{ '*' + t('endOfFlowMessage6') }
<div className="first-time-flow__text-block end-of-flow__text-4">
*MetaMask cannot recover your seedphrase. <a
href="https://metamask.zendesk.com/hc/en-us/articles/360015489591-Basic-Safety-Tips"
target="_blank"
rel="noopener noreferrer"
>
<span className="first-time-flow__link-text">
Learn More
</span>
</a>.
</div>
<Button
type="confirm"
className="first-time-flow__button"
onClick={async () => {
await completeOnboarding()
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Onboarding Complete',
name: completionMetaMetricsName,
},
})
history.push(DEFAULT_ROUTE)
}}
>

@ -2,10 +2,24 @@ import { connect } from 'react-redux'
import EndOfFlow from './end-of-flow.component'
import { setCompletedOnboarding } from '../../../../actions'
const firstTimeFlowTypeNameMap = {
create: 'New Wallet Created',
'import': 'New Wallet Imported',
}
const mapStateToProps = ({ metamask }) => {
const { firstTimeFlowType } = metamask
return {
completionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
}
}
const mapDispatchToProps = dispatch => {
return {
completeOnboarding: () => dispatch(setCompletedOnboarding()),
}
}
export default connect(null, mapDispatchToProps)(EndOfFlow)
export default connect(mapStateToProps, mapDispatchToProps)(EndOfFlow)

@ -25,11 +25,17 @@
}
&__text-3 {
margin-top: 26px;
margin-top: 2px;
margin-bottom: 2px;
@media screen and (max-width: $break-small) {
margin-bottom: 16px;
font-size: .875rem;
}
}
&__text-3 {
margin-top: 2px;
&__text-4 {
margin-top: 26px;
}
button {

@ -7,6 +7,7 @@ import {
INITIALIZE_WELCOME_ROUTE,
INITIALIZE_UNLOCK_ROUTE,
INITIALIZE_SEED_PHRASE_ROUTE,
INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
} from '../../../../routes'
export default class FirstTimeFlowSwitch extends PureComponent {
@ -15,6 +16,7 @@ export default class FirstTimeFlowSwitch extends PureComponent {
isInitialized: PropTypes.bool,
isUnlocked: PropTypes.bool,
seedPhrase: PropTypes.string,
optInMetaMetrics: PropTypes.bool,
}
render () {
@ -23,6 +25,7 @@ export default class FirstTimeFlowSwitch extends PureComponent {
isInitialized,
isUnlocked,
seedPhrase,
optInMetaMetrics,
} = this.props
if (completedOnboarding) {
@ -45,6 +48,10 @@ export default class FirstTimeFlowSwitch extends PureComponent {
return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }} />
}
if (optInMetaMetrics === null) {
return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
}
return <Redirect to={{ pathname: INITIALIZE_METAMETRICS_OPT_IN_ROUTE }} />
}
}

@ -6,12 +6,14 @@ const mapStateToProps = ({ metamask }) => {
completedOnboarding,
isInitialized,
isUnlocked,
participateInMetaMetrics: optInMetaMetrics,
} = metamask
return {
completedOnboarding,
isInitialized,
isUnlocked,
optInMetaMetrics,
}
}

@ -8,6 +8,7 @@ import EndOfFlow from './end-of-flow'
import Unlock from '../unlock-page'
import CreatePassword from './create-password'
import SeedPhrase from './seed-phrase'
import MetaMetricsOptInScreen from './metametrics-opt-in'
import {
DEFAULT_ROUTE,
INITIALIZE_WELCOME_ROUTE,
@ -16,6 +17,7 @@ import {
INITIALIZE_UNLOCK_ROUTE,
INITIALIZE_SELECT_ACTION_ROUTE,
INITIALIZE_END_OF_FLOW_ROUTE,
INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
} from '../../../routes'
export default class FirstTimeFlow extends PureComponent {
@ -27,6 +29,7 @@ export default class FirstTimeFlow extends PureComponent {
isInitialized: PropTypes.bool,
isUnlocked: PropTypes.bool,
unlockAccount: PropTypes.func,
nextRoute: PropTypes.func,
}
state = {
@ -71,12 +74,12 @@ export default class FirstTimeFlow extends PureComponent {
}
handleUnlock = async password => {
const { unlockAccount, history } = this.props
const { unlockAccount, history, nextRoute } = this.props
try {
const seedPhrase = await unlockAccount(password)
this.setState({ seedPhrase }, () => {
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
history.push(nextRoute)
})
} catch (error) {
throw new Error(error.message)
@ -132,6 +135,11 @@ export default class FirstTimeFlow extends PureComponent {
path={INITIALIZE_WELCOME_ROUTE}
component={Welcome}
/>
<Route
exact
path={INITIALIZE_METAMETRICS_OPT_IN_ROUTE}
component={MetaMetricsOptInScreen}
/>
<Route
exact
path="*"

@ -1,5 +1,6 @@
import { connect } from 'react-redux'
import FirstTimeFlow from './first-time-flow.component'
import { getFirstTimeFlowTypeRoute } from './first-time-flow.selectors'
import {
createNewVaultAndGetSeedPhrase,
createNewVaultAndRestore,
@ -13,6 +14,7 @@ const mapStateToProps = state => {
completedOnboarding,
isInitialized,
isUnlocked,
nextRoute: getFirstTimeFlowTypeRoute(state),
}
}

@ -0,0 +1,26 @@
import {
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
DEFAULT_ROUTE,
} from '../../../routes'
const selectors = {
getFirstTimeFlowTypeRoute,
}
module.exports = selectors
function getFirstTimeFlowTypeRoute (state) {
const { firstTimeFlowType } = state.metamask
let nextRoute
if (firstTimeFlowType === 'create') {
nextRoute = INITIALIZE_CREATE_PASSWORD_ROUTE
} else if (firstTimeFlowType === 'import') {
nextRoute = INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE
} else {
nextRoute = DEFAULT_ROUTE
}
return nextRoute
}

@ -6,6 +6,9 @@
@import './end-of-flow/index';
@import './metametrics-opt-in/index';
.first-time-flow {
width: 100%;
background-color: $white;
@ -149,4 +152,8 @@
color: #939090;
margin-left: 18px;
}
&__link-text {
color: $curious-blue;
}
}

@ -0,0 +1 @@
export { default } from './metametrics-opt-in.container'

@ -0,0 +1,136 @@
.metametrics-opt-in {
position: relative;
width: 100%;
a {
color: #2f9ae0bf;
}
&__main {
display: flex;
flex-direction: column;
margin-left: 26.26%;
margin-right: 28%;
color: black;
@media screen and (max-width: 575px) {
justify-content: center;
margin-left: 2%;
margin-right: 0%;
}
.app-header__logo-container {
margin-top: 3%;
}
}
&__title {
position: relative;
margin-top: 20px;
font-family: Roboto;
font-style: normal;
font-weight: normal;
line-height: normal;
font-size: 42px;
}
&__body-graphic {
margin-top: 25px;
.fa-bar-chart {
color: #C4C4C4;
}
}
&__description {
font-family: Roboto;
font-style: normal;
font-weight: normal;
line-height: 21px;
font-size: 16px;
margin-top: 12px;
}
&__committments {
display: flex;
flex-direction: column;
}
&__content {
overflow-y: scroll;
flex: 1;
}
&__row {
display: flex;
margin-top: 8px;
.fa-check {
margin-right: 12px;
color: #1ACC56;
}
.fa-times {
margin-right: 12px;
color: #D0021B;
}
}
&__bold {
font-weight: bold;
}
&__break-row {
margin-top: 30px;
}
&__body {
position: relative;
display: flex;
max-width: 730px;
flex-direction: column;
}
&__body-text {
max-width: 548px;
margin-left: 16px;
margin-right: 16px;
}
&__bottom-text {
margin-top: 10px;
color: #9a9a9a;
}
&__content {
overflow-y: auto;
}
&__footer {
margin-top: 26px;
@media screen and (max-width: 575px) {
margin-top: 10px;
justify-content: center;
margin-left: 2%;
max-height: 520px;
}
.page-container__footer {
border-top: none;
max-width: 535px;
margin-bottom: 15px;
button {
height: 44px;
min-height: 44px;
margin-right: 16px;
}
header {
padding: 0px;
}
}
}
}

@ -0,0 +1,169 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainerFooter from '../../../page-container/page-container-footer'
export default class MetaMetricsOptIn extends Component {
static propTypes = {
history: PropTypes.object,
setParticipateInMetaMetrics: PropTypes.func,
nextRoute: PropTypes.string,
firstTimeSelectionMetaMetricsName: PropTypes.string,
participateInMetaMetrics: PropTypes.bool,
}
static contextTypes = {
metricsEvent: PropTypes.func,
}
render () {
const { metricsEvent } = this.context
const {
nextRoute,
history,
setParticipateInMetaMetrics,
firstTimeSelectionMetaMetricsName,
participateInMetaMetrics,
} = this.props
return (
<div className="metametrics-opt-in">
<div className="metametrics-opt-in__main">
<div className="app-header__logo-container">
<img
className="app-header__metafox-logo app-header__metafox-logo--horizontal"
src="/images/logo/metamask-logo-horizontal.svg"
height={30}
/>
<img
className="app-header__metafox-logo app-header__metafox-logo--icon"
src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
</div>
<div className="metametrics-opt-in__body-graphic">
<img src="images/metrics-chart.svg" />
</div>
<div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
<div className="metametrics-opt-in__body">
<div className="metametrics-opt-in__description">
MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
will be used to continually improve the usability and user experience of our product and the Etheruem ecosystem.
</div>
<div className="metametrics-opt-in__description">
MetaMask will..
</div>
<div className="metametrics-opt-in__committments">
<div className="metametrics-opt-in__row">
<i className="fa fa-check" />
<div className="metametrics-opt-in__row-description">
Always allow you to opt-out via Settings
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-check" />
<div className="metametrics-opt-in__row-description">
Send anonymized click & pageview events
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-check" />
<div className="metametrics-opt-in__row-description">
Maintain a public aggregate dashboard to educate the community
</div>
</div>
<div className="metametrics-opt-in__row metametrics-opt-in__break-row">
<i className="fa fa-times" />
<div className="metametrics-opt-in__row-description">
<span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-times" />
<div className="metametrics-opt-in__row-description">
<span className="metametrics-opt-in__bold">Never</span> collect your full IP address
</div>
</div>
<div className="metametrics-opt-in__row">
<i className="fa fa-times" />
<div className="metametrics-opt-in__row-description">
<span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
</div>
</div>
</div>
</div>
<div className="metametrics-opt-in__footer">
<PageContainerFooter
onCancel={() => {
setParticipateInMetaMetrics(false)
.then(() => {
if (participateInMetaMetrics === null) {
return metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt Out',
},
isOptIn: true,
}, {
excludeMetaMetricsId: true,
})
.then(() => {
history.push(nextRoute)
})
}
})
}}
cancelText={'No Thanks'}
hideCancel={false}
onSubmit={() => {
setParticipateInMetaMetrics(true)
.then(([participateStatus, metaMetricsId]) => {
const promise = participateInMetaMetrics === null
? metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt In',
},
isOptIn: true,
})
: Promise.resolve()
promise
.then(() => {
return metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Import or Create',
name: firstTimeSelectionMetaMetricsName,
},
isOptIn: true,
metaMetricsId,
})
})
.then(() => {
history.push(nextRoute)
})
})
}}
submitText={'I agree'}
submitButtonType={'confirm'}
disabled={false}
/>
<div className="metametrics-opt-in__bottom-text">
This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a
href="https://metamask.io/privacy.html"
target="_blank"
rel="noopener noreferrer"
>
Privacy Policy here
</a>.
</div>
</div>
</div>
</div>
)
}
}

@ -0,0 +1,27 @@
import { connect } from 'react-redux'
import MetaMetricsOptIn from './metametrics-opt-in.component'
import { setParticipateInMetaMetrics } from '../../../../actions'
import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
const firstTimeFlowTypeNameMap = {
create: 'Selected Create New Wallet',
'import': 'Selected Import Wallet',
}
const mapStateToProps = (state) => {
const { firstTimeFlowType, participateInMetaMetrics } = state.metamask
return {
nextRoute: getFirstTimeFlowTypeRoute(state),
firstTimeSelectionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
participateInMetaMetrics,
}
}
const mapDispatchToProps = dispatch => {
return {
setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MetaMetricsOptIn)

@ -3,12 +3,16 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import shuffle from 'lodash.shuffle'
import Button from '../../../../button'
import { INITIALIZE_END_OF_FLOW_ROUTE, INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../../routes'
import {
INITIALIZE_END_OF_FLOW_ROUTE,
INITIALIZE_SEED_PHRASE_ROUTE,
} from '../../../../../routes'
import { exportAsFile } from '../../../../../../app/util'
import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
export default class ConfirmSeedPhrase extends PureComponent {
static contextTypes = {
metricsEvent: PropTypes.func,
t: PropTypes.func,
}
@ -47,6 +51,13 @@ export default class ConfirmSeedPhrase extends PureComponent {
}
try {
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Seed Phrase Setup',
name: 'Verify Complete',
},
})
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
} catch (error) {
console.error(error.message)

@ -9,6 +9,7 @@ import { exportAsFile } from '../../../../../../app/util'
export default class RevealSeedPhrase extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -29,6 +30,14 @@ export default class RevealSeedPhrase extends PureComponent {
const { isShowingSeedPhrase } = this.state
const { history } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Seed Phrase Setup',
name: 'Advance to Verify',
},
})
if (!isShowingSeedPhrase) {
return
}
@ -53,7 +62,16 @@ export default class RevealSeedPhrase extends PureComponent {
!isShowingSeedPhrase && (
<div
className="reveal-seed-phrase__secret-blocker"
onClick={() => this.setState({ isShowingSeedPhrase: true })}
onClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Seed Phrase Setup',
name: 'Revealed Words',
},
})
this.setState({ isShowingSeedPhrase: true })
}}
>
<LockIcon
width="28px"

@ -1 +1 @@
export { default } from './select-action.component'
export { default } from './select-action.container'

@ -32,7 +32,7 @@
flex-direction: column;
align-items: center;
justify-content: space-evenly;
width: 269px;
width: 388px;
height: 278px;
border: 1px solid #D8D8D8;
@ -78,6 +78,7 @@
font-size: 14px;
color: #7A7A7B;
margin-top: 10px;
text-align: center;
}
button {

@ -2,15 +2,15 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../../button'
import {
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
} from '../../../../routes'
export default class SelectAction extends PureComponent {
static propTypes = {
history: PropTypes.object,
isInitialized: PropTypes.bool,
setFirstTimeFlowType: PropTypes.func,
nextRoute: PropTypes.string,
}
static contextTypes = {
@ -18,19 +18,21 @@ export default class SelectAction extends PureComponent {
}
componentDidMount () {
const { history, isInitialized } = this.props
const { history, isInitialized, nextRoute } = this.props
if (isInitialized) {
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
history.push(nextRoute)
}
}
handleCreate = () => {
this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
this.props.setFirstTimeFlowType('create')
this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
}
handleImport = () => {
this.props.history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
this.props.setFirstTimeFlowType('import')
this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
}
render () {
@ -68,6 +70,9 @@ export default class SelectAction extends PureComponent {
<div className="select-action__button-text-big">
{ t('noAlreadyHaveSeed') }
</div>
<div className="select-action__button-text-small">
{ t('importYourExisting') }
</div>
</div>
<Button
type="primary"
@ -85,6 +90,9 @@ export default class SelectAction extends PureComponent {
<div className="select-action__button-text-big">
{ t('letsGoSetUp') }
</div>
<div className="select-action__button-text-small">
{ t('thisWillCreate') }
</div>
</div>
<Button
type="confirm"

@ -0,0 +1,23 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import { setFirstTimeFlowType } from '../../../../actions'
import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
import Welcome from './select-action.component'
const mapStateToProps = (state) => {
return {
nextRoute: getFirstTimeFlowTypeRoute(state),
}
}
const mapDispatchToProps = dispatch => {
return {
setFirstTimeFlowType: type => dispatch(setFirstTimeFlowType(type)),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(Welcome)

@ -3,12 +3,14 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Mascot from '../../../mascot'
import Button from '../../../button'
import { INITIALIZE_SELECT_ACTION_ROUTE, INITIALIZE_UNIQUE_IMAGE_ROUTE } from '../../../../routes'
import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE } from '../../../../routes'
export default class Welcome extends PureComponent {
static propTypes = {
history: PropTypes.object,
isInitialized: PropTypes.bool,
participateInMetaMetrics: PropTypes.bool,
welcomeScreenSeen: PropTypes.bool,
}
static contextTypes = {
@ -22,10 +24,12 @@ export default class Welcome extends PureComponent {
}
componentDidMount () {
const { history, isInitialized } = this.props
const { history, participateInMetaMetrics, welcomeScreenSeen } = this.props
if (isInitialized) {
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
if (welcomeScreenSeen && participateInMetaMetrics !== null) {
history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
} else if (welcomeScreenSeen) {
history.push(INITIALIZE_SELECT_ACTION_ROUTE)
}
}

@ -5,11 +5,12 @@ import { closeWelcomeScreen } from '../../../../actions'
import Welcome from './welcome.component'
const mapStateToProps = ({ metamask }) => {
const { welcomeScreenSeen, isInitialized } = metamask
const { welcomeScreenSeen, isInitialized, participateInMetaMetrics } = metamask
return {
welcomeScreenSeen,
isInitialized,
participateInMetaMetrics,
}
}

@ -12,6 +12,7 @@ import Button from '../../button'
class RestoreVaultPage extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -84,7 +85,16 @@ class RestoreVaultPage extends Component {
leaveImportSeedScreenState()
createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
.then(() => history.push(DEFAULT_ROUTE))
.then(() => {
this.context.metricsEvent({
eventOpts: {
category: 'Retention',
action: 'userEntersSeedPhrase',
name: 'onboardingRestoredVault',
},
})
history.push(DEFAULT_ROUTE)
})
}
hasError () {
@ -176,10 +186,6 @@ class RestoreVaultPage extends Component {
}
}
RestoreVaultPage.contextTypes = {
t: PropTypes.func,
}
export default connect(
({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
dispatch => ({

@ -33,6 +33,7 @@ const localeOptions = locales.map(locale => {
export default class SettingsTab extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -65,6 +66,8 @@ export default class SettingsTab extends PureComponent {
mobileSync: PropTypes.bool,
showFiatInTestnets: PropTypes.bool,
setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
participateInMetaMetrics: PropTypes.bool,
setParticipateInMetaMetrics: PropTypes.func,
}
state = {
@ -235,11 +238,34 @@ export default class SettingsTab extends PureComponent {
validateRpc (newRpc, chainId, ticker = 'ETH', nickname) {
const { setRpcTarget, displayWarning } = this.props
if (validUrl.isWebUri(newRpc)) {
this.context.metricsEvent({
eventOpts: {
category: 'Settings',
action: 'Custom RPC',
name: 'Success',
},
customVariables: {
networkId: newRpc,
chainId,
},
})
if (!!chainId && Number.isNaN(parseInt(chainId))) {
return displayWarning(`${this.context.t('invalidInput')} chainId`)
}
setRpcTarget(newRpc, chainId, ticker, nickname)
} else {
this.context.metricsEvent({
eventOpts: {
category: 'Settings',
action: 'Custom RPC',
name: 'Error',
},
customVariables: {
networkId: newRpc,
chainId,
},
})
const appendedRpc = `http://${newRpc}`
if (validUrl.isWebUri(appendedRpc)) {
@ -331,6 +357,13 @@ export default class SettingsTab extends PureComponent {
large
onClick={event => {
event.preventDefault()
this.context.metricsEvent({
eventOpts: {
category: 'Settings',
action: 'Reveal Seed Phrase',
name: 'Reveal Seed Phrase',
},
})
history.push(REVEAL_SEED_ROUTE)
}}
>
@ -392,6 +425,13 @@ export default class SettingsTab extends PureComponent {
className="settings-tab__button--orange"
onClick={event => {
event.preventDefault()
this.context.metricsEvent({
eventOpts: {
category: 'Settings',
action: 'Reset Account',
name: 'Reset Account',
},
})
showResetAccountConfirmationModal()
}}
>
@ -586,6 +626,32 @@ export default class SettingsTab extends PureComponent {
)
}
renderMetaMetricsOptIn () {
const { t } = this.context
const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props
return (
<div className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{ t('participateInMetaMetrics') }</span>
<div className="settings-page__content-description">
{ t('participateInMetaMetricsDescription') }
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<ToggleButton
value={participateInMetaMetrics}
onToggle={value => setParticipateInMetaMetrics(!value)}
activeLabel=""
inactiveLabel=""
/>
</div>
</div>
</div>
)
}
render () {
const { warning } = this.props
@ -606,6 +672,7 @@ export default class SettingsTab extends PureComponent {
{ this.renderAdvancedGasInputInline() }
{ this.renderBlockieOptIn() }
{ this.renderMobileSync() }
{ this.renderMetaMetricsOptIn() }
</div>
)
}

@ -13,6 +13,7 @@ import {
showModal,
setUseNativeCurrencyAsPrimaryCurrencyPreference,
setShowFiatConversionOnTestnetsPreference,
setParticipateInMetaMetrics,
} from '../../../../actions'
import { preferencesSelector } from '../../../../selectors'
@ -31,6 +32,7 @@ const mapStateToProps = state => {
} = {},
provider = {},
currentLocale,
participateInMetaMetrics,
} = metamask
const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets } = preferencesSelector(state)
@ -48,6 +50,7 @@ const mapStateToProps = state => {
useNativeCurrencyAsPrimaryCurrency,
mobileSync,
showFiatInTestnets,
participateInMetaMetrics,
}
}
@ -70,6 +73,7 @@ const mapDispatchToProps = dispatch => {
return dispatch(setShowFiatConversionOnTestnetsPreference(value))
},
showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })),
setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
}
}

@ -9,6 +9,7 @@ import { DEFAULT_ROUTE } from '../../../routes'
export default class UnlockPage extends Component {
static contextTypes = {
metricsEvent: PropTypes.func,
t: PropTypes.func,
}
@ -45,7 +46,7 @@ export default class UnlockPage extends Component {
event.stopPropagation()
const { password } = this.state
const { onSubmit } = this.props
const { onSubmit, forceUpdateMetamaskState, showOptInModal } = this.props
if (password === '' || this.submitting) {
return
@ -56,7 +57,35 @@ export default class UnlockPage extends Component {
try {
await onSubmit(password)
const newState = await forceUpdateMetamaskState()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Unlock',
name: 'Success',
},
isNewVisit: true,
})
if (newState.participateInMetaMetrics === null || newState.participateInMetaMetrics === undefined) {
showOptInModal()
}
} catch ({ message }) {
if (message === 'Incorrect password') {
const newState = await forceUpdateMetamaskState()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Unlock',
name: 'Incorrect Passowrd',
},
customVariables: {
numberOfTokens: newState.tokens.length,
numberOfAccounts: Object.keys(newState.accounts).length,
},
})
}
this.setState({ error: message })
this.submitting = false
}

@ -8,6 +8,8 @@ import {
tryUnlockMetamask,
forgotPassword,
markPasswordForgotten,
forceUpdateMetamaskState,
showModal,
} from '../../../actions'
import UnlockPage from './unlock-page.component'
@ -23,6 +25,8 @@ const mapDispatchToProps = dispatch => {
forgotPassword: () => dispatch(forgotPassword()),
tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
markPasswordForgotten: () => dispatch(markPasswordForgotten()),
forceUpdateMetamaskState: () => forceUpdateMetamaskState(dispatch),
showOptInModal: () => dispatch(showModal({ name: 'METAMETRICS_OPT_IN_MODAL' })),
}
}

@ -15,15 +15,40 @@ export default class ProviderPageContainer extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
};
componentDidMount () {
this.context.metricsEvent({
eventOpts: {
category: 'Auth',
action: 'Connect',
name: 'Popup Opened',
},
})
}
onCancel = () => {
const { tabID, rejectProviderRequest } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Auth',
action: 'Connect',
name: 'Canceled',
},
})
rejectProviderRequest(tabID)
}
onSubmit = () => {
const { approveProviderRequest, tabID } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Auth',
action: 'Connect',
name: 'Confirmed',
},
})
approveProviderRequest(tabID)
}

@ -35,7 +35,12 @@ export default class AmountMaxButton extends Component {
}
onMaxClick = (event) => {
const { setMaxModeTo } = this.props
const { setMaxModeTo, selectedToken } = this.props
fetch('https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&e_c=send&e_a=amountMax&e_n=' + (selectedToken ? 'token' : 'eth'), {
'headers': {},
'method': 'GET',
})
event.preventDefault()
setMaxModeTo(true)

@ -27,11 +27,22 @@ export default class SendGasRow extends Component {
static contextTypes = {
t: PropTypes.func,
}
metricsEvent: PropTypes.func,
};
renderAdvancedOptionsButton () {
const { metricsEvent } = this.context
const { showCustomizeGasModal } = this.props
return <div className="advanced-gas-options-btn" onClick={() => showCustomizeGasModal()}>
return <div className="advanced-gas-options-btn" onClick={() => {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Edit Screen',
name: 'Clicked "Advanced Options"',
},
})
showCustomizeGasModal()
}}>
{ this.context.t('advancedOptions') }
</div>
}
@ -53,12 +64,23 @@ export default class SendGasRow extends Component {
gasLimit,
insufficientBalance,
} = this.props
const { metricsEvent } = this.context
const gasPriceButtonGroup = <div>
<GasPriceButtonGroup
className="gas-price-button-group--small"
showCheck={false}
{...gasPriceButtonGroupProps}
handleGasPriceSelection={(...args) => {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Edit Screen',
name: 'Changed Gas Button',
},
})
gasPriceButtonGroupProps.handleGasPriceSelection(...args)
}}
/>
{ this.renderAdvancedOptionsButton() }
</div>

@ -30,7 +30,7 @@ describe('SendGasRow Component', function () {
someGasPriceButtonGroupProp: 'foo',
anotherGasPriceButtonGroupProp: 'bar',
}}
/>, { context: { t: str => str + '_t' } })
/>, { context: { t: str => str + '_t', metricsEvent: () => ({}) } })
})
afterEach(() => {

@ -27,6 +27,7 @@ export default class SendToRow extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
handleToChange (to, nickname = '', toError, toWarning, network) {
@ -62,7 +63,16 @@ export default class SendToRow extends Component {
warningType={'to'}
>
<EnsInput
scanQrCode={_ => this.props.scanQrCode()}
scanQrCode={_ => {
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Edit Screen',
name: 'Used QR scanner',
},
})
this.props.scanQrCode()
}}
accounts={toAccounts}
closeDropdown={() => closeToDropdown()}
dropdownOpen={toDropdownOpen}

@ -26,11 +26,13 @@ export default class SendFooter extends Component {
tokenBalance: PropTypes.string,
unapprovedTxs: PropTypes.object,
update: PropTypes.func,
sendErrors: PropTypes.object,
}
static contextTypes = {
t: PropTypes.func,
}
metricsEvent: PropTypes.func,
};
onCancel () {
this.props.clearSend()
@ -56,6 +58,7 @@ export default class SendFooter extends Component {
toAccounts,
history,
} = this.props
const { metricsEvent } = this.context
// Should not be needed because submit should be disabled if there are errors.
// const noErrors = !amountError && toError === null
@ -66,7 +69,6 @@ export default class SendFooter extends Component {
// TODO: add nickname functionality
addToAddressBookIfNew(to, toAccounts)
const promise = editingTransactionId
? update({
amount,
@ -82,13 +84,44 @@ export default class SendFooter extends Component {
: sign({ data, selectedToken, to, amount, from, gas, gasPrice })
Promise.resolve(promise)
.then(() => history.push(CONFIRM_TRANSACTION_ROUTE))
.then(() => {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Edit Screen',
name: 'Complete',
},
})
history.push(CONFIRM_TRANSACTION_ROUTE)
})
}
formShouldBeDisabled () {
const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props
const missingTokenBalance = selectedToken && !tokenBalance
return inError || !gasTotal || missingTokenBalance || !(data || to)
const shouldBeDisabled = inError || !gasTotal || missingTokenBalance || !(data || to)
return shouldBeDisabled
}
componentDidUpdate (prevProps) {
const { inError, sendErrors } = this.props
const { metricsEvent } = this.context
if (!prevProps.inError && inError) {
const errorField = Object.keys(sendErrors).find(key => sendErrors[key])
const errorMessage = sendErrors[errorField]
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Edit Screen',
name: 'Error',
},
customVariables: {
errorField,
errorMessage,
},
})
}
}
render () {

@ -21,6 +21,7 @@ import {
getSendHexData,
getTokenBalance,
getUnapprovedTxs,
getSendErrors,
} from '../send.selectors'
import {
isSendFormInError,
@ -48,6 +49,7 @@ function mapStateToProps (state) {
toAccounts: getSendToAccounts(state),
tokenBalance: getTokenBalance(state),
unapprovedTxs: getUnapprovedTxs(state),
sendErrors: getSendErrors(state),
}
}

@ -45,7 +45,8 @@ describe('SendFooter Component', function () {
tokenBalance={'mockTokenBalance'}
unapprovedTxs={['mockTx']}
update={propsMethodSpies.update}
/>, { context: { t: str => str } })
sendErrors={{}}
/>, { context: { t: str => str, metricsEvent: () => ({}) } })
})
afterEach(() => {
@ -201,7 +202,7 @@ describe('SendFooter Component', function () {
tokenBalance={'mockTokenBalance'}
unapprovedTxs={['mockTx']}
update={propsMethodSpies.update}
/>, { context: { t: str => str } })
/>, { context: { t: str => str, metricsEvent: () => ({}) } })
})
afterEach(() => {

@ -42,6 +42,7 @@ proxyquire('../send-footer.container.js', {
getTokenBalance: (s) => `mockTokenBalance:${s}`,
getSendHexData: (s) => `mockHexData:${s}`,
getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`,
getSendErrors: (s) => `mockSendErrors:${s}`,
},
'./send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` },
'./send-footer.utils': utilsStubs,
@ -66,6 +67,7 @@ describe('send-footer container', () => {
toAccounts: 'mockToAccounts:mockState',
tokenBalance: 'mockTokenBalance:mockState',
unapprovedTxs: 'mockUnapprovedTxs:mockState',
sendErrors: 'mockSendErrors:mockState',
})
})

@ -23,6 +23,8 @@ export default class SenderToRecipient extends PureComponent {
variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]),
addressOnly: PropTypes.bool,
assetImage: PropTypes.string,
onRecipientClick: PropTypes.func,
onSenderClick: PropTypes.func,
}
static defaultProps = {
@ -86,7 +88,7 @@ export default class SenderToRecipient extends PureComponent {
renderRecipientWithAddress () {
const { t } = this.context
const { recipientName, recipientAddress, addressOnly } = this.props
const { recipientName, recipientAddress, addressOnly, onRecipientClick } = this.props
const checksummedRecipientAddress = checksumAddress(recipientAddress)
return (
@ -95,6 +97,7 @@ export default class SenderToRecipient extends PureComponent {
onClick={() => {
this.setState({ recipientAddressCopied: true })
copyToClipboard(checksummedRecipientAddress)
onRecipientClick()
}}
>
{ this.renderRecipientIdenticon() }
@ -151,7 +154,7 @@ export default class SenderToRecipient extends PureComponent {
}
render () {
const { senderAddress, recipientAddress, variant } = this.props
const { senderAddress, recipientAddress, variant, onSenderClick } = this.props
const checksummedSenderAddress = checksumAddress(senderAddress)
return (
@ -161,6 +164,7 @@ export default class SenderToRecipient extends PureComponent {
onClick={() => {
this.setState({ senderAddressCopied: true })
copyToClipboard(checksummedSenderAddress)
onSenderClick()
}}
>
{ this.renderSenderIdenticon() }

@ -14,10 +14,19 @@ export default class Sidebar extends Component {
transitionName: PropTypes.string,
type: PropTypes.string,
sidebarProps: PropTypes.object,
onOverlayClose: PropTypes.func,
};
renderOverlay () {
return <div className="sidebar-overlay" onClick={() => this.props.hideSidebar()} />
const { onOverlayClose } = this.props
return <div
className="sidebar-overlay"
onClick={() => {
onOverlayClose && onOverlayClose()
this.props.hideSidebar()
}
} />
}
renderSidebarContent () {

@ -49,6 +49,7 @@ function mapDispatchToProps (dispatch) {
SignatureRequest.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = compose(
@ -264,6 +265,13 @@ SignatureRequest.prototype.renderFooter = function () {
className: 'request-signature__footer__cancel-button',
onClick: event => {
cancel(event).then(() => {
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Sign Request',
name: 'Cancel',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
})
@ -275,6 +283,13 @@ SignatureRequest.prototype.renderFooter = function () {
className: 'request-signature__footer__sign-button',
onClick: event => {
sign(event).then(() => {
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Sign Request',
name: 'Confirm',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
})

@ -1,4 +1,5 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
@ -40,6 +41,10 @@ function TokenCell () {
}
}
TokenCell.contextTypes = {
metricsEvent: PropTypes.func,
}
TokenCell.prototype.render = function () {
const { tokenMenuOpen } = this.state
const props = this.props
@ -88,6 +93,13 @@ TokenCell.prototype.render = function () {
// onClick: this.view.bind(this, address, userAddress, network),
onClick: () => {
setSelectedToken(address)
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Token Menu',
name: 'Clicked Token',
},
})
selectedTokenAddress !== address && sidebarOpen && hideSidebar()
},
}, [

@ -10,6 +10,7 @@ import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
export default class TransactionActivityLog extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricEvent: PropTypes.func,
}
static propTypes = {

@ -12,6 +12,7 @@ import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
export default class TransactionListItemDetails extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -33,6 +34,14 @@ export default class TransactionListItemDetails extends PureComponent {
const prefix = prefixForNetwork(metamaskNetworkId)
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Clicked "View on Etherscan"',
},
})
global.platform.openWindow({ url: etherscanUrl })
}
@ -55,6 +64,14 @@ export default class TransactionListItemDetails extends PureComponent {
const { primaryTransaction: transaction } = transactionGroup
const { hash } = transaction
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Copied Transaction ID',
},
})
this.setState({ justCopied: true }, () => {
copyToClipboard(hash)
setTimeout(() => this.setState({ justCopied: false }), 1000)
@ -125,6 +142,24 @@ export default class TransactionListItemDetails extends PureComponent {
addressOnly
recipientAddress={to}
senderAddress={from}
onRecipientClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Copied "To" Address',
},
})
}}
onSenderClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Copied "From" Address',
},
})
}}
/>
</div>
<div className="transaction-list-item-details__cards-container">

@ -38,6 +38,10 @@ export default class TransactionListItem extends PureComponent {
showFiat: true,
}
static contextTypes = {
metricsEvent: PropTypes.func,
}
state = {
showTransactionDetails: false,
}
@ -55,6 +59,16 @@ export default class TransactionListItem extends PureComponent {
return
}
if (!showTransactionDetails) {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Expand Transaction',
},
})
}
this.setState({ showTransactionDetails: !showTransactionDetails })
}

@ -16,6 +16,7 @@ const historySpies = {
}
const t = (str1, str2) => str2 ? str1 + str2 : str1
const metricsEvent = () => ({})
describe('TransactionViewBalance Component', () => {
afterEach(() => {
@ -31,7 +32,7 @@ describe('TransactionViewBalance Component', () => {
ethBalance={123}
fiatBalance={456}
currentCurrency="usd"
/>, { context: { t } })
/>, { context: { t, metricsEvent } })
assert.equal(wrapper.find('.transaction-view-balance').length, 1)
assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)

@ -12,6 +12,7 @@ import Tooltip from '../tooltip-v2'
export default class TransactionViewBalance extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
@ -78,7 +79,7 @@ export default class TransactionViewBalance extends PureComponent {
}
renderButtons () {
const { t } = this.context
const { t, metricsEvent } = this.context
const { selectedToken, showDepositModal, history } = this.props
return (
@ -88,7 +89,16 @@ export default class TransactionViewBalance extends PureComponent {
<Button
type="primary"
className="transaction-view-balance__button"
onClick={() => showDepositModal()}
onClick={() => {
metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Clicked Deposit',
},
})
showDepositModal()
}}
>
{ t('deposit') }
</Button>
@ -97,7 +107,16 @@ export default class TransactionViewBalance extends PureComponent {
<Button
type="primary"
className="transaction-view-balance__button"
onClick={() => history.push(SEND_ROUTE)}
onClick={() => {
metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Clicked Send',
},
})
history.push(SEND_ROUTE)
}}
>
{ t('send') }
</Button>

@ -26,6 +26,7 @@ module.exports = compose(
WalletView.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
WalletView.defaultProps = {
@ -39,7 +40,6 @@ function mapStateToProps (state) {
sidebarOpen: state.appState.sidebar.isOpen,
identities: state.metamask.identities,
accounts: selectors.getMetaMaskAccounts(state),
tokens: state.metamask.tokens,
keyrings: state.metamask.keyrings,
selectedAddress: selectors.getSelectedAddress(state),
selectedAccount: selectors.getSelectedAccount(state),
@ -110,6 +110,13 @@ WalletView.prototype.renderAddToken = function () {
return h(AddTokenButton, {
onClick () {
history.push(ADD_TOKEN_ROUTE)
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Token Menu',
name: 'Clicked "Add Token"',
},
})
if (sidebarOpen) {
hideSidebar()
}
@ -197,6 +204,13 @@ WalletView.prototype.render = function () {
}),
onClick: () => {
copyToClipboard(checksummedAddress)
this.context.metricsEvent({
eventOpts: {
category: 'Activation',
action: 'userClicks',
name: 'navCopyToClipboard',
},
})
this.setState({ hasCopied: true })
setTimeout(() => this.setState({ hasCopied: false }), 3000)
},

@ -372,6 +372,7 @@ export function setTransactionToConfirm (transactionId) {
try {
dispatch(setFetchingData(true))
const methodData = await getMethodData(data)
dispatch(updateMethodData(methodData))
} catch (error) {
dispatch(updateMethodData({}))

@ -0,0 +1,106 @@
import { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import {
getCurrentNetworkId,
getSelectedAsset,
getAccountType,
getNumberOfAccounts,
getNumberOfTokens,
} from '../selectors'
import {
txDataSelector,
} from '../selectors/confirm-transaction'
import { getEnvironmentType } from '../../../app/scripts/lib/util'
import {
sendMetaMetricsEvent,
sendCountIsTrackable,
} from './metametrics.util'
class MetaMetricsProvider extends Component {
static propTypes = {
network: PropTypes.string.isRequired,
environmentType: PropTypes.string.isRequired,
activeCurrency: PropTypes.string.isRequired,
accountType: PropTypes.string.isRequired,
metaMetricsSendCount: PropTypes.number.isRequired,
children: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
}
static childContextTypes = {
metricsEvent: PropTypes.func,
}
constructor (props) {
super(props)
this.state = {
previousPath: '',
currentPath: window.location.href,
}
props.history.listen(locationObj => {
this.setState({
previousPath: this.state.currentPath,
currentPath: window.location.href,
})
})
}
getChildContext () {
const props = this.props
const { pathname } = location
const { previousPath, currentPath } = this.state
return {
metricsEvent: (config = {}, overrides = {}) => {
const { eventOpts = {} } = config
const { name = '' } = eventOpts
const { pathname: overRidePathName = '' } = overrides
const isSendFlow = Boolean(name.match(/^send|^confirm/) || overRidePathName.match(/send|confirm/))
if (props.participateInMetaMetrics || config.isOptIn) {
return sendMetaMetricsEvent({
...props,
...config,
previousPath,
currentPath,
pathname,
excludeMetaMetricsId: isSendFlow && !sendCountIsTrackable(props.metaMetricsSendCount + 1),
...overrides,
})
}
},
}
}
render () {
return this.props.children
}
}
const mapStateToProps = state => {
const txData = txDataSelector(state) || {}
return {
network: getCurrentNetworkId(state),
environmentType: getEnvironmentType(),
activeCurrency: getSelectedAsset(state),
accountType: getAccountType(state),
confirmTransactionOrigin: txData.origin,
metaMetricsId: state.metamask.metaMetricsId,
participateInMetaMetrics: state.metamask.participateInMetaMetrics,
metaMetricsSendCount: state.metamask.metaMetricsSendCount,
numberOfTokens: getNumberOfTokens(state),
numberOfAccounts: getNumberOfAccounts(state),
}
}
module.exports = compose(
withRouter,
connect(mapStateToProps)
)(MetaMetricsProvider)

@ -0,0 +1,188 @@
/* eslint camelcase: 0 */
const ethUtil = require('ethereumjs-util')
const inDevelopment = process.env.METAMETRICS_URL === 'development'
const METAMETRICS_BASE_URL = 'https://chromeextensionmm.innocraft.cloud/piwik.php'
const METAMETRICS_REQUIRED_PARAMS = `?idsite=${inDevelopment ? 1 : 2}&rec=1&apiv=1`
const METAMETRICS_BASE_FULL = METAMETRICS_BASE_URL + METAMETRICS_REQUIRED_PARAMS
const METAMETRICS_TRACKING_URL = inDevelopment
? 'http://www.metamask.io/metametrics'
: 'http://www.metamask.io/metametrics-prod'
const METAMETRICS_CUSTOM_HAD_ERROR = 'hadError'
const METAMETRICS_CUSTOM_HEX_DATA = 'hexData'
const METAMETRICS_CUSTOM_FUNCTION_TYPE = 'functionType'
const METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE = 'gasLimitChange'
const METAMETRICS_CUSTOM_GAS_PRICE_CHANGE = 'gasPriceChange'
const METAMETRICS_CUSTOM_RECIPIENT_KNOWN = 'recipientKnown'
const METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN = 'origin'
const METAMETRICS_CUSTOM_FROM_NETWORK = 'fromNetwork'
const METAMETRICS_CUSTOM_TO_NETWORK = 'toNetwork'
const METAMETRICS_CUSTOM_ERROR_FIELD = 'errorField'
const METAMETRICS_CUSTOM_ERROR_MESSAGE = 'errorMessage'
const METAMETRICS_CUSTOM_RPC_NETWORK_ID = 'networkId'
const METAMETRICS_CUSTOM_RPC_CHAIN_ID = 'chainId'
const METAMETRICS_CUSTOM_NETWORK = 'network'
const METAMETRICS_CUSTOM_ENVIRONMENT_TYPE = 'environmentType'
const METAMETRICS_CUSTOM_ACTIVE_CURRENCY = 'activeCurrency'
const METAMETRICS_CUSTOM_ACCOUNT_TYPE = 'accountType'
const METAMETRICS_CUSTOM_NUMBER_OF_TOKENS = 'numberOfTokens'
const METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS = 'numberOfAccounts'
const customVariableNameIdMap = {
[METAMETRICS_CUSTOM_HAD_ERROR]: 1,
[METAMETRICS_CUSTOM_HEX_DATA]: 2,
[METAMETRICS_CUSTOM_FUNCTION_TYPE]: 3,
[METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE]: 4,
[METAMETRICS_CUSTOM_GAS_PRICE_CHANGE]: 5,
[METAMETRICS_CUSTOM_RECIPIENT_KNOWN]: 6,
[METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN]: 7,
[METAMETRICS_CUSTOM_FROM_NETWORK]: 8,
[METAMETRICS_CUSTOM_TO_NETWORK]: 9,
[METAMETRICS_CUSTOM_RPC_NETWORK_ID]: 10,
[METAMETRICS_CUSTOM_RPC_CHAIN_ID]: 11,
[METAMETRICS_CUSTOM_ERROR_FIELD]: 12,
[METAMETRICS_CUSTOM_ERROR_MESSAGE]: 13,
}
const customDimensionsNameIdMap = {
[METAMETRICS_CUSTOM_NETWORK]: 5,
[METAMETRICS_CUSTOM_ENVIRONMENT_TYPE]: 6,
[METAMETRICS_CUSTOM_ACTIVE_CURRENCY]: 7,
[METAMETRICS_CUSTOM_ACCOUNT_TYPE]: 8,
[METAMETRICS_CUSTOM_NUMBER_OF_TOKENS]: 9,
[METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS]: 10,
}
function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'MetaMask'
return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(previousPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}`
}
function composeCustomDimensionParamAddition (customDimensions) {
const customDimensionParamStrings = Object.keys(customDimensions).reduce((acc, name) => {
return [...acc, `dimension${customDimensionsNameIdMap[name]}=${customDimensions[name]}`]
}, [])
return `&${customDimensionParamStrings.join('&')}`
}
function composeCustomVarParamAddition (customVariables) {
const customVariableIdValuePairs = Object.keys(customVariables).reduce((acc, name) => {
return {
[customVariableNameIdMap[name]]: [name, customVariables[name]],
...acc,
}
}, {})
return `&cvar=${encodeURIComponent(JSON.stringify(customVariableIdValuePairs))}`
}
function composeParamAddition (paramValue, paramName) {
return paramValue !== 0 && !paramValue
? ''
: `&${paramName}=${paramValue}`
}
function composeUrl (config, permissionPreferences = {}) {
const {
eventOpts = {},
customVariables = '',
pageOpts = '',
network,
environmentType,
activeCurrency,
accountType,
numberOfTokens,
numberOfAccounts,
previousPath = '',
currentPath,
metaMetricsId,
confirmTransactionOrigin,
url: configUrl,
excludeMetaMetricsId,
isNewVisit,
} = config
const base = METAMETRICS_BASE_FULL
const e_c = composeParamAddition(eventOpts.category, 'e_c')
const e_a = composeParamAddition(eventOpts.action, 'e_a')
const e_n = composeParamAddition(eventOpts.name, 'e_n')
const new_visit = isNewVisit ? `&new_visit=1` : ''
const cvar = customVariables && composeCustomVarParamAddition(customVariables) || ''
const action_name = ''
const urlref = previousPath && composeUrlRefParamAddition(previousPath, confirmTransactionOrigin)
const dimensions = !pageOpts.hideDimensions ? composeCustomDimensionParamAddition({
network,
environmentType,
activeCurrency,
accountType,
numberOfTokens: customVariables && customVariables.numberOfTokens || numberOfTokens,
numberOfAccounts: customVariables && customVariables.numberOfAccounts || numberOfAccounts,
}) : ''
const url = configUrl || `&url=${encodeURIComponent(currentPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}`
const _id = metaMetricsId && !excludeMetaMetricsId ? `&_id=${metaMetricsId.slice(2, 18)}` : ''
const rand = `&rand=${String(Math.random()).slice(2)}`
const pv_id = `&pv_id=${ethUtil.bufferToHex(ethUtil.sha3(url || currentPath.match(/chrome-extension:\/\/\w+\/(.+)/)[0])).slice(2, 8)}`
const uid = metaMetricsId && !excludeMetaMetricsId
? `&uid=${metaMetricsId.slice(2, 18)}`
: excludeMetaMetricsId
? '&uid=0000000000000000'
: ''
return [ base, e_c, e_a, e_n, cvar, action_name, urlref, dimensions, url, _id, rand, pv_id, uid, new_visit ].join('')
}
export function sendMetaMetricsEvent (config, permissionPreferences) {
return fetch(composeUrl(config, permissionPreferences), {
'headers': {},
'method': 'GET',
})
}
export function verifyUserPermission (config, props) {
const {
eventOpts = {},
} = config
const { userPermissionPreferences } = props
const {
allowAll,
allowNone,
allowSendMetrics,
} = userPermissionPreferences
if (allowNone) {
return false
} else if (allowAll) {
return true
} else if (allowSendMetrics && eventOpts.name === 'send') {
return true
} else {
return false
}
}
const trackableSendCounts = {
1: true,
10: true,
30: true,
50: true,
100: true,
250: true,
500: true,
1000: true,
2500: true,
5000: true,
10000: true,
25000: true,
}
export function sendCountIsTrackable (sendCount) {
return Boolean(trackableSendCounts[sendCount])
}

@ -55,8 +55,11 @@ function reduceMetamask (state, action) {
useNativeCurrencyAsPrimaryCurrency: true,
showFiatInTestnets: false,
},
firstTimeFlowType: null,
completedOnboarding: false,
knownMethodData: {},
participateInMetaMetrics: null,
metaMetricsSendCount: 0,
}, state.metamask)
switch (action.type) {
@ -338,6 +341,16 @@ function reduceMetamask (state, action) {
coinOptions,
})
case actions.SET_PARTICIPATE_IN_METAMETRICS:
return extend(metamaskState, {
participateInMetaMetrics: action.value,
})
case actions.SET_METAMETRICS_SEND_COUNT:
return extend(metamaskState, {
metaMetricsSendCount: action.value,
})
case actions.SET_USE_BLOCKIE:
return extend(metamaskState, {
useBlockie: action.value,
@ -395,6 +408,12 @@ function reduceMetamask (state, action) {
})
}
case actions.SET_FIRST_TIME_FLOW_TYPE: {
return extend(metamaskState, {
firstTimeFlowType: action.value,
})
}
default:
return metamaskState

@ -5,6 +5,7 @@ const h = require('react-hyperscript')
const { HashRouter } = require('react-router-dom')
const App = require('./app')
const I18nProvider = require('./i18n-provider')
const MetaMetricsProvider = require('./metametrics/metametrics.provider')
class Root extends Component {
render () {
@ -15,10 +16,12 @@ class Root extends Component {
h(HashRouter, {
hashType: 'noslash',
}, [
h(MetaMetricsProvider, [
h(I18nProvider, [
h(App),
]),
]),
]),
])
)
}

@ -29,6 +29,7 @@ const INITIALIZE_SELECT_ACTION_ROUTE = '/initialize/select-action'
const INITIALIZE_SEED_PHRASE_ROUTE = '/initialize/seed-phrase'
const INITIALIZE_END_OF_FLOW_ROUTE = '/initialize/end-of-flow'
const INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE = '/initialize/seed-phrase/confirm'
const INITIALIZE_METAMETRICS_OPT_IN_ROUTE = '/initialize/metametrics-opt-in'
const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
const CONFIRM_SEND_ETHER_PATH = '/send-ether'
@ -78,4 +79,5 @@ module.exports = {
CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH,
INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
}

@ -1,4 +1,5 @@
import {NETWORK_TYPES} from './constants/common'
import { stripHexPrefix } from 'ethereumjs-util'
const abi = require('human-standard-token-abi')
import {
@ -40,6 +41,12 @@ const selectors = {
isBalanceCached,
getAdvancedInlineGasShown,
getIsMainnet,
getCurrentNetworkId,
getSelectedAsset,
getCurrentKeyring,
getAccountType,
getNumberOfAccounts,
getNumberOfTokens,
}
module.exports = selectors
@ -50,6 +57,46 @@ function getNetworkIdentifier (state) {
return nickname || rpcTarget || type
}
function getCurrentKeyring (state) {
const identity = getSelectedIdentity(state)
if (!identity) {
return null
}
const simpleAddress = stripHexPrefix(identity.address).toLowerCase()
const keyring = state.metamask.keyrings.find((kr) => {
return kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
})
return keyring
}
function getAccountType (state) {
const currentKeyring = getCurrentKeyring(state)
const type = currentKeyring && currentKeyring.type
switch (type) {
case 'Trezor Hardware':
case 'Ledger Hardware':
return 'hardware'
case 'Simple Key Pair':
return 'imported'
default:
return 'default'
}
}
function getSelectedAsset (state) {
return getSelectedToken(state) || 'ETH'
}
function getCurrentNetworkId (state) {
return state.metamask.network
}
function getSelectedAddress (state) {
const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
@ -63,6 +110,15 @@ function getSelectedIdentity (state) {
return identities[selectedAddress]
}
function getNumberOfAccounts (state) {
return Object.keys(state.metamask.accounts).length
}
function getNumberOfTokens (state) {
const tokens = state.metamask.tokens
return tokens ? tokens.length : 0
}
function getMetaMaskAccounts (state) {
const currentAccounts = state.metamask.accounts
const cachedBalances = state.metamask.cachedBalances[state.metamask.network]

@ -95,7 +95,7 @@ export const currentCurrencySelector = state => state.metamask.currentCurrency
export const conversionRateSelector = state => state.metamask.conversionRate
export const getNativeCurrency = state => state.metamask.nativeCurrency
const txDataSelector = state => state.confirmTransaction.txData
export const txDataSelector = state => state.confirmTransaction.txData
const tokenDataSelector = state => state.confirmTransaction.tokenData
const tokenPropsSelector = state => state.confirmTransaction.tokenProps

Loading…
Cancel
Save