aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app
diff options
context:
space:
mode:
authorDan J Miller <danjm.com@gmail.com>2019-03-05 23:45:01 +0800
committerGitHub <noreply@github.com>2019-03-05 23:45:01 +0800
commitc7573663557b0db778a2907eaf2fd1918ced4914 (patch)
tree3fed2dbe12a845704bff3268e9ee610d55bcc8c2 /ui/app
parent1765864e40f548b805cdbdbe59ada3e0c445a52d (diff)
downloadtangerine-wallet-browser-c7573663557b0db778a2907eaf2fd1918ced4914.tar
tangerine-wallet-browser-c7573663557b0db778a2907eaf2fd1918ced4914.tar.gz
tangerine-wallet-browser-c7573663557b0db778a2907eaf2fd1918ced4914.tar.bz2
tangerine-wallet-browser-c7573663557b0db778a2907eaf2fd1918ced4914.tar.lz
tangerine-wallet-browser-c7573663557b0db778a2907eaf2fd1918ced4914.tar.xz
tangerine-wallet-browser-c7573663557b0db778a2907eaf2fd1918ced4914.tar.zst
tangerine-wallet-browser-c7573663557b0db778a2907eaf2fd1918ced4914.zip
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
Diffstat (limited to 'ui/app')
-rw-r--r--ui/app/actions.js66
-rw-r--r--ui/app/app.js29
-rw-r--r--ui/app/components/account-dropdowns.js13
-rw-r--r--ui/app/components/account-menu/account-menu.component.js42
-rw-r--r--ui/app/components/app-header/app-header.component.js32
-rw-r--r--ui/app/components/app-header/app-header.container.js2
-rw-r--r--ui/app/components/customize-gas-modal/index.js22
-rw-r--r--ui/app/components/dropdowns/account-details-dropdown.js22
-rw-r--r--ui/app/components/dropdowns/network-dropdown.js28
-rw-r--r--ui/app/components/menu-bar/menu-bar.component.js12
-rw-r--r--ui/app/components/menu-bar/menu-bar.container.js3
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.component.js25
-rw-r--r--ui/app/components/modals/index.scss2
-rw-r--r--ui/app/components/modals/metametrics-opt-in-modal/index.js1
-rw-r--r--ui/app/components/modals/metametrics-opt-in-modal/index.scss30
-rw-r--r--ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js135
-rw-r--r--ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js24
-rw-r--r--ui/app/components/modals/modal.js19
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js131
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js5
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js18
-rw-r--r--ui/app/components/pages/create-account/import-account/json.js15
-rw-r--r--ui/app/components/pages/create-account/import-account/private-key.js15
-rw-r--r--ui/app/components/pages/create-account/new-account.js24
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/create-password.component.js17
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js37
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js39
-rw-r--r--ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js19
-rw-r--r--ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js37
-rw-r--r--ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js16
-rw-r--r--ui/app/components/pages/first-time-flow/end-of-flow/index.scss14
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js9
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js2
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow.component.js12
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow.container.js2
-rw-r--r--ui/app/components/pages/first-time-flow/first-time-flow.selectors.js26
-rw-r--r--ui/app/components/pages/first-time-flow/index.scss7
-rw-r--r--ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js1
-rw-r--r--ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss136
-rw-r--r--ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js169
-rw-r--r--ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js27
-rw-r--r--ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js13
-rw-r--r--ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js20
-rw-r--r--ui/app/components/pages/first-time-flow/select-action/index.js2
-rw-r--r--ui/app/components/pages/first-time-flow/select-action/index.scss3
-rw-r--r--ui/app/components/pages/first-time-flow/select-action/select-action.component.js22
-rw-r--r--ui/app/components/pages/first-time-flow/select-action/select-action.container.js23
-rw-r--r--ui/app/components/pages/first-time-flow/welcome/welcome.component.js12
-rw-r--r--ui/app/components/pages/first-time-flow/welcome/welcome.container.js3
-rw-r--r--ui/app/components/pages/keychains/restore-vault.js16
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.component.js67
-rw-r--r--ui/app/components/pages/settings/settings-tab/settings-tab.container.js4
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js31
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.container.js4
-rw-r--r--ui/app/components/provider-page-container/provider-page-container.component.js25
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js7
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js26
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js2
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js12
-rw-r--r--ui/app/components/send/send-footer/send-footer.component.js41
-rw-r--r--ui/app/components/send/send-footer/send-footer.container.js2
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-component.test.js5
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-container.test.js2
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js8
-rw-r--r--ui/app/components/sidebars/sidebar.component.js11
-rw-r--r--ui/app/components/signature-request.js15
-rw-r--r--ui/app/components/token-cell.js12
-rw-r--r--ui/app/components/transaction-activity-log/transaction-activity-log.component.js1
-rw-r--r--ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js35
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.component.js14
-rw-r--r--ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js3
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.component.js25
-rw-r--r--ui/app/components/wallet-view.js16
-rw-r--r--ui/app/ducks/confirm-transaction.duck.js1
-rw-r--r--ui/app/metametrics/metametrics.provider.js106
-rw-r--r--ui/app/metametrics/metametrics.util.js188
-rw-r--r--ui/app/reducers/metamask.js19
-rw-r--r--ui/app/root.js7
-rw-r--r--ui/app/routes.js2
-rw-r--r--ui/app/selectors.js56
-rw-r--r--ui/app/selectors/confirm-transaction.js2
81 files changed, 2014 insertions, 134 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 1d01a72ad..d8363eba6 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -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,
+ })
+ }
+}
diff --git a/ui/app/app.js b/ui/app/app.js
index 1001adc9a..b9f6cafe7 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -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(
diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js
index 06376e48b..b05ba219c 100644
--- a/ui/app/components/account-dropdowns.js
+++ b/ui/app/components/account-dropdowns.js
@@ -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 = {
diff --git a/ui/app/components/account-menu/account-menu.component.js b/ui/app/components/account-menu/account-menu.component.js
index b2fec647a..ce7482108 100644
--- a/ui/app/components/account-menu/account-menu.component.js
+++ b/ui/app/components/account-menu/account-menu.component.js
@@ -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
diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js
index f7d8c8598..14f8b9f30 100644
--- a/ui/app/components/app-header/app-header.component.js
+++ b/ui/app/components/app-header/app-header.component.js
@@ -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}
diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app-header/app-header.container.js
index 30d3f8cc4..1abc2afeb 100644
--- a/ui/app/components/app-header/app-header.container.js
+++ b/ui/app/components/app-header/app-header.container.js
@@ -13,6 +13,7 @@ const mapStateToProps = state => {
provider,
selectedAddress,
isUnlocked,
+ isAccountMenuOpen,
} = metamask
return {
@@ -21,6 +22,7 @@ const mapStateToProps = state => {
provider,
selectedAddress,
isUnlocked,
+ isAccountMenuOpen,
}
}
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
index e67fbe45b..fd660ead2 100644
--- a/ui/app/components/customize-gas-modal/index.js
+++ b/ui/app/components/customize-gas-modal/index.js
@@ -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))
diff --git a/ui/app/components/dropdowns/account-details-dropdown.js b/ui/app/components/dropdowns/account-details-dropdown.js
index 7476cfdd9..bda8b9517 100644
--- a/ui/app/components/dropdowns/account-details-dropdown.js
+++ b/ui/app/components/dropdowns/account-details-dropdown.js
@@ -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()
},
diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js
index 6e002219a..86a5a5268 100644
--- a/ui/app/components/dropdowns/network-dropdown.js
+++ b/ui/app/components/dropdowns/network-dropdown.js
@@ -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
diff --git a/ui/app/components/menu-bar/menu-bar.component.js b/ui/app/components/menu-bar/menu-bar.component.js
index e64809f3f..24f84516d 100644
--- a/ui/app/components/menu-bar/menu-bar.component.js
+++ b/ui/app/components/menu-bar/menu-bar.component.js
@@ -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 />
diff --git a/ui/app/components/menu-bar/menu-bar.container.js b/ui/app/components/menu-bar/menu-bar.container.js
index ae32882ae..4c5276402 100644
--- a/ui/app/components/menu-bar/menu-bar.container.js
+++ b/ui/app/components/menu-bar/menu-bar.container.js
@@ -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()),
diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js
index 3f526bd43..4e2e20660 100644
--- a/ui/app/components/modals/customize-gas/customize-gas.component.js
+++ b/ui/app/components/modals/customize-gas/customize-gas.component.js
@@ -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}
>
diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss
index 45453a582..555da87ef 100644
--- a/ui/app/components/modals/index.scss
+++ b/ui/app/components/modals/index.scss
@@ -7,3 +7,5 @@
@import './qr-scanner/index';
@import './transaction-confirmed/index';
+
+@import './metametrics-opt-in-modal/index';
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/index.js b/ui/app/components/modals/metametrics-opt-in-modal/index.js
new file mode 100644
index 000000000..47f946757
--- /dev/null
+++ b/ui/app/components/modals/metametrics-opt-in-modal/index.js
@@ -0,0 +1 @@
+export { default } from './metametrics-opt-in-modal.container'
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/index.scss b/ui/app/components/modals/metametrics-opt-in-modal/index.scss
new file mode 100644
index 000000000..88b6d7a4d
--- /dev/null
+++ b/ui/app/components/modals/metametrics-opt-in-modal/index.scss
@@ -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;
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js
new file mode 100644
index 000000000..36f1ed92d
--- /dev/null
+++ b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js
new file mode 100644
index 000000000..525806b75
--- /dev/null
+++ b/ui/app/components/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js
@@ -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)
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index 08bf205ef..8ab599a71 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -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, {
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
index 3650dc869..e76b4699b 100644
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -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,30 +367,46 @@ export default class ConfirmTransactionBase extends Component {
submitting: true,
submitError: null,
}, () => {
- if (onSubmit) {
- Promise.resolve(onSubmit(txData))
- .then(() => {
- this.setState({
- submitting: false,
- })
- })
- } else {
- sendTransaction(txData)
- .then(() => {
- clearConfirmTransaction()
- this.setState({
- submitting: false,
- }, () => {
- history.push(DEFAULT_ROUTE)
- })
- })
- .catch(error => {
- this.setState({
- submitting: false,
- submitError: error.message,
- })
- })
- }
+ 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(() => {
+ this.setState({
+ submitting: false,
+ })
+ })
+ } else {
+ sendTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ this.setState({
+ submitting: false,
+ }, () => {
+ history.push(DEFAULT_ROUTE)
+ })
+ })
+ .catch(error => {
+ this.setState({
+ submitting: false,
+ submitError: error.message,
+ })
+ })
+ }
+ })
})
}
@@ -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,
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
index 2a8033c8f..22f509905 100644
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -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)),
}
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
index bd877fd4e..712cc5cbb 100644
--- a/ui/app/components/pages/create-account/connect-hardware/index.js
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -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)(
diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js
index 8bb6e154b..9aeea5579 100644
--- a/ui/app/components/pages/create-account/import-account/json.js
+++ b/ui/app/components/pages/create-account/import-account/json.js
@@ -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(
diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js
index 45068b96e..4ba31806f 100644
--- a/ui/app/components/pages/create-account/import-account/private-key.js
+++ b/ui/app/components/pages/create-account/import-account/private-key.js
@@ -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)
}
})
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 94a5fa487..a7595e346 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -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)
diff --git a/ui/app/components/pages/first-time-flow/create-password/create-password.component.js b/ui/app/components/pages/first-time-flow/create-password/create-password.component.js
index 7cca82ca6..3faaa3764 100644
--- a/ui/app/components/pages/first-time-flow/create-password/create-password.component.js
+++ b/ui/app/components/pages/first-time-flow/create-password/create-password.component.js
@@ -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}
diff --git a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
index 2e99147bb..6b3c03bb3 100644
--- a/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
+++ b/ui/app/components/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
@@ -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
diff --git a/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js
index b82cba0c5..11d10e2d9 100644
--- a/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js
+++ b/ui/app/components/pages/first-time-flow/create-password/new-account/new-account.component.js
@@ -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
diff --git a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js b/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js
index fa76074f5..cbc85c0e4 100644
--- a/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js
+++ b/ui/app/components/pages/first-time-flow/create-password/unique-image/unique-image.component.js
@@ -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) {
- history.push(INITIALIZE_END_OF_FLOW_ROUTE)
- } else {
- history.push(INITIALIZE_SEED_PHRASE_ROUTE)
- }
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Agree to Phishing Warning',
+ name: 'Agree to Phishing Warning',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
}}
>
{ t('next') }
diff --git a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js
index 2ca5fd8ec..c0e2f59d9 100644
--- a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js
+++ b/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.component.js
@@ -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="first-time-flow__text-block end-of-flow__text-3">
- { '*' + t('endOfFlowMessage6') }
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage7') }
+ </div>
+ <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)
}}
>
diff --git a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js b/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js
index ffe2c0efb..91ae5a941 100644
--- a/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js
+++ b/ui/app/components/pages/first-time-flow/end-of-flow/end-of-flow.container.js
@@ -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)
diff --git a/ui/app/components/pages/first-time-flow/end-of-flow/index.scss b/ui/app/components/pages/first-time-flow/end-of-flow/index.scss
index 5f5cc5991..d7eb4513b 100644
--- a/ui/app/components/pages/first-time-flow/end-of-flow/index.scss
+++ b/ui/app/components/pages/first-time-flow/end-of-flow/index.scss
@@ -24,12 +24,18 @@
margin-top: 26px;
}
- &__text-3 {
- margin-top: 26px;
+ &__text-3 {
+ 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 {
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
index 43f792e06..5c2294393 100644
--- a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
+++ b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
@@ -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 }} />
}
- return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
+ if (optInMetaMetrics === null) {
+ return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
+ }
+
+ return <Redirect to={{ pathname: INITIALIZE_METAMETRICS_OPT_IN_ROUTE }} />
}
}
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
index e44c216c0..d68f7a153 100644
--- a/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
+++ b/ui/app/components/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
@@ -6,12 +6,14 @@ const mapStateToProps = ({ metamask }) => {
completedOnboarding,
isInitialized,
isUnlocked,
+ participateInMetaMetrics: optInMetaMetrics,
} = metamask
return {
completedOnboarding,
isInitialized,
isUnlocked,
+ optInMetaMetrics,
}
}
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.component.js b/ui/app/components/pages/first-time-flow/first-time-flow.component.js
index 82308dda2..a1f629431 100644
--- a/ui/app/components/pages/first-time-flow/first-time-flow.component.js
+++ b/ui/app/components/pages/first-time-flow/first-time-flow.component.js
@@ -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)
@@ -134,6 +137,11 @@ export default class FirstTimeFlow extends PureComponent {
/>
<Route
exact
+ path={INITIALIZE_METAMETRICS_OPT_IN_ROUTE}
+ component={MetaMetricsOptInScreen}
+ />
+ <Route
+ exact
path="*"
component={FirstTimeFlowSwitch}
/>
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.container.js b/ui/app/components/pages/first-time-flow/first-time-flow.container.js
index 1419dd59f..293f94c47 100644
--- a/ui/app/components/pages/first-time-flow/first-time-flow.container.js
+++ b/ui/app/components/pages/first-time-flow/first-time-flow.container.js
@@ -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),
}
}
diff --git a/ui/app/components/pages/first-time-flow/first-time-flow.selectors.js b/ui/app/components/pages/first-time-flow/first-time-flow.selectors.js
new file mode 100644
index 000000000..1286afed9
--- /dev/null
+++ b/ui/app/components/pages/first-time-flow/first-time-flow.selectors.js
@@ -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
+}
diff --git a/ui/app/components/pages/first-time-flow/index.scss b/ui/app/components/pages/first-time-flow/index.scss
index e14d57f58..d41748575 100644
--- a/ui/app/components/pages/first-time-flow/index.scss
+++ b/ui/app/components/pages/first-time-flow/index.scss
@@ -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;
+ }
}
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js b/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js
new file mode 100644
index 000000000..4bc2fc3a7
--- /dev/null
+++ b/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js
@@ -0,0 +1 @@
+export { default } from './metametrics-opt-in.container'
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss b/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss
new file mode 100644
index 000000000..6c2e37785
--- /dev/null
+++ b/ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss
@@ -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;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
new file mode 100644
index 000000000..834516f5f
--- /dev/null
+++ b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
new file mode 100644
index 000000000..b13af8bf6
--- /dev/null
+++ b/ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
@@ -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)
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
index b5c4bf463..bd5ab8a84 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
+++ b/ui/app/components/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
@@ -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)
diff --git a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
index 732ce14af..cb8a01322 100644
--- a/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
+++ b/ui/app/components/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
@@ -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"
diff --git a/ui/app/components/pages/first-time-flow/select-action/index.js b/ui/app/components/pages/first-time-flow/select-action/index.js
index 3aa968834..4fbe1823b 100644
--- a/ui/app/components/pages/first-time-flow/select-action/index.js
+++ b/ui/app/components/pages/first-time-flow/select-action/index.js
@@ -1 +1 @@
-export { default } from './select-action.component'
+export { default } from './select-action.container'
diff --git a/ui/app/components/pages/first-time-flow/select-action/index.scss b/ui/app/components/pages/first-time-flow/select-action/index.scss
index b9585eb3b..e1b22d05b 100644
--- a/ui/app/components/pages/first-time-flow/select-action/index.scss
+++ b/ui/app/components/pages/first-time-flow/select-action/index.scss
@@ -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 {
diff --git a/ui/app/components/pages/first-time-flow/select-action/select-action.component.js b/ui/app/components/pages/first-time-flow/select-action/select-action.component.js
index 385efe02a..b6a6942c3 100644
--- a/ui/app/components/pages/first-time-flow/select-action/select-action.component.js
+++ b/ui/app/components/pages/first-time-flow/select-action/select-action.component.js
@@ -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"
diff --git a/ui/app/components/pages/first-time-flow/select-action/select-action.container.js b/ui/app/components/pages/first-time-flow/select-action/select-action.container.js
index e69de29bb..42fac7af2 100644
--- a/ui/app/components/pages/first-time-flow/select-action/select-action.container.js
+++ b/ui/app/components/pages/first-time-flow/select-action/select-action.container.js
@@ -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)
diff --git a/ui/app/components/pages/first-time-flow/welcome/welcome.component.js b/ui/app/components/pages/first-time-flow/welcome/welcome.component.js
index 08eb86939..88cdb936c 100644
--- a/ui/app/components/pages/first-time-flow/welcome/welcome.component.js
+++ b/ui/app/components/pages/first-time-flow/welcome/welcome.component.js
@@ -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)
}
}
diff --git a/ui/app/components/pages/first-time-flow/welcome/welcome.container.js b/ui/app/components/pages/first-time-flow/welcome/welcome.container.js
index 4362d89cb..47753e16f 100644
--- a/ui/app/components/pages/first-time-flow/welcome/welcome.container.js
+++ b/ui/app/components/pages/first-time-flow/welcome/welcome.container.js
@@ -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,
}
}
diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js
index ce18d998c..73ff5191a 100644
--- a/ui/app/components/pages/keychains/restore-vault.js
+++ b/ui/app/components/pages/keychains/restore-vault.js
@@ -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 => ({
diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
index 01621c354..73b2cfc29 100644
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
+++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js
@@ -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>
)
}
diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
index 5cb9a9aae..64c256412 100644
--- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
+++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js
@@ -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)),
}
}
diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js
index 58a8b0566..3ba870885 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.component.js
+++ b/ui/app/components/pages/unlock-page/unlock-page.component.js
@@ -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
}
diff --git a/ui/app/components/pages/unlock-page/unlock-page.container.js b/ui/app/components/pages/unlock-page/unlock-page.container.js
index 5f302dc37..fe51c8095 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.container.js
+++ b/ui/app/components/pages/unlock-page/unlock-page.container.js
@@ -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' })),
}
}
diff --git a/ui/app/components/provider-page-container/provider-page-container.component.js b/ui/app/components/provider-page-container/provider-page-container.component.js
index 30b6b02fc..ff063166d 100644
--- a/ui/app/components/provider-page-container/provider-page-container.component.js
+++ b/ui/app/components/provider-page-container/provider-page-container.component.js
@@ -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)
}
diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
index 80518977e..f17137c1e 100644
--- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
@@ -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)
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
index 50337e0bf..bf7446626 100644
--- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
@@ -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>
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
index 059c6cdd3..08f26854e 100644
--- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
@@ -30,7 +30,7 @@ describe('SendGasRow Component', function () {
someGasPriceButtonGroupProp: 'foo',
anotherGasPriceButtonGroupProp: 'bar',
}}
- />, { context: { t: str => str + '_t' } })
+ />, { context: { t: str => str + '_t', metricsEvent: () => ({}) } })
})
afterEach(() => {
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
index 3fbf9a76b..434204490 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
@@ -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}
diff --git a/ui/app/components/send/send-footer/send-footer.component.js b/ui/app/components/send/send-footer/send-footer.component.js
index b78b56373..d943b4b22 100644
--- a/ui/app/components/send/send-footer/send-footer.component.js
+++ b/ui/app/components/send/send-footer/send-footer.component.js
@@ -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 () {
diff --git a/ui/app/components/send/send-footer/send-footer.container.js b/ui/app/components/send/send-footer/send-footer.container.js
index 60de4d030..0c6120cc5 100644
--- a/ui/app/components/send/send-footer/send-footer.container.js
+++ b/ui/app/components/send/send-footer/send-footer.container.js
@@ -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),
}
}
diff --git a/ui/app/components/send/send-footer/tests/send-footer-component.test.js b/ui/app/components/send/send-footer/tests/send-footer-component.test.js
index 65e4bb654..4b63e422d 100644
--- a/ui/app/components/send/send-footer/tests/send-footer-component.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-component.test.js
@@ -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(() => {
diff --git a/ui/app/components/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
index daefa5103..70cb28df3 100644
--- a/ui/app/components/send/send-footer/tests/send-footer-container.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
@@ -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',
})
})
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
index 89a1a9c08..65102a7ad 100644
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
@@ -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() }
diff --git a/ui/app/components/sidebars/sidebar.component.js b/ui/app/components/sidebars/sidebar.component.js
index f68515ad6..b9e0f9e81 100644
--- a/ui/app/components/sidebars/sidebar.component.js
+++ b/ui/app/components/sidebars/sidebar.component.js
@@ -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 () {
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index a76064bff..25bd9a7b1 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -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)
})
diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js
index 75ba347fa..d9c80b4f4 100644
--- a/ui/app/components/token-cell.js
+++ b/ui/app/components/token-cell.js
@@ -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()
},
}, [
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
index 4e4e553c0..ca46d7830 100644
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js
@@ -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 = {
diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
index eaf1166f0..3e39212d3 100644
--- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
+++ b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
@@ -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">
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js
index 29d3a7b1f..e843fe1a0 100644
--- a/ui/app/components/transaction-list-item/transaction-list-item.component.js
+++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js
@@ -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 })
}
diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
index 513a8aac9..efc987371 100644
--- a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
+++ b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
@@ -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)
diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
index b16e04f4f..a18e959b5 100644
--- a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
+++ b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
@@ -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>
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index 4566cb390..400b9aa90 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -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)
},
diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js
index c6e2c1be3..f75ff809a 100644
--- a/ui/app/ducks/confirm-transaction.duck.js
+++ b/ui/app/ducks/confirm-transaction.duck.js
@@ -372,6 +372,7 @@ export function setTransactionToConfirm (transactionId) {
try {
dispatch(setFetchingData(true))
const methodData = await getMethodData(data)
+
dispatch(updateMethodData(methodData))
} catch (error) {
dispatch(updateMethodData({}))
diff --git a/ui/app/metametrics/metametrics.provider.js b/ui/app/metametrics/metametrics.provider.js
new file mode 100644
index 000000000..5ff0294e5
--- /dev/null
+++ b/ui/app/metametrics/metametrics.provider.js
@@ -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)
+
diff --git a/ui/app/metametrics/metametrics.util.js b/ui/app/metametrics/metametrics.util.js
new file mode 100644
index 000000000..2da7e2da8
--- /dev/null
+++ b/ui/app/metametrics/metametrics.util.js
@@ -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])
+}
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index fb0fd7130..c1aa20bf7 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -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
diff --git a/ui/app/root.js b/ui/app/root.js
index f9e3709a0..c95c56581 100644
--- a/ui/app/root.js
+++ b/ui/app/root.js
@@ -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,8 +16,10 @@ class Root extends Component {
h(HashRouter, {
hashType: 'noslash',
}, [
- h(I18nProvider, [
- h(App),
+ h(MetaMetricsProvider, [
+ h(I18nProvider, [
+ h(App),
+ ]),
]),
]),
])
diff --git a/ui/app/routes.js b/ui/app/routes.js
index 7c4e805ab..932dfa7df 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -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,
}
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
index a36671b42..663c3f12b 100644
--- a/ui/app/selectors.js
+++ b/ui/app/selectors.js
@@ -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]
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
index 23ef26d95..ccd16fadd 100644
--- a/ui/app/selectors/confirm-transaction.js
+++ b/ui/app/selectors/confirm-transaction.js
@@ -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