Skip to content

Commit

Permalink
Bugfix: onboarding analytics (#976)
Browse files Browse the repository at this point in the history
* handle state

* handle onboarding flow

* tests

* fix timing to save erase event

* dah

* typo

* events

* events
  • Loading branch information
estebanmino authored Aug 12, 2019
1 parent 653b1e4 commit 46aae2b
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 36 deletions.
20 changes: 20 additions & 0 deletions app/actions/onboarding/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Saves an onboarding analytics event in state
*
* @param {object} event - Event object
*/
export function saveOnboardingEvent(event) {
return {
type: 'SAVE_EVENT',
event
};
}

/**
* Erases any event stored in state
*/
export function clearOnboardingEvents() {
return {
type: 'CLEAR_EVENTS'
};
}
48 changes: 42 additions & 6 deletions app/components/UI/OptinMetrics/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React, { PureComponent } from 'react';
import { View, SafeAreaView, Text, StyleSheet, TouchableOpacity, ScrollView, BackHandler, Alert } from 'react-native';
import {
View,
SafeAreaView,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
BackHandler,
Alert,
InteractionManager
} from 'react-native';
import PropTypes from 'prop-types';
import { baseStyles, fontStyles, colors } from '../../../styles/common';
import AsyncStorage from '@react-native-community/async-storage';
Expand All @@ -12,6 +22,7 @@ import { NavigationActions, withNavigationFocus } from 'react-navigation';
import StyledButton from '../StyledButton';
import Analytics from '../../../core/Analytics';
import ANALYTICS_EVENT_OPTS from '../../../util/analytics';
import { clearOnboardingEvents } from '../../../actions/onboarding';

const styles = StyleSheet.create({
root: {
Expand Down Expand Up @@ -97,7 +108,15 @@ class OptinMetrics extends PureComponent {
/**
* React navigation prop to know if this view is focused
*/
isFocused: PropTypes.bool
isFocused: PropTypes.bool,
/**
* Onboarding events array created in previous onboarding views
*/
events: PropTypes.array,
/**
* Action to erase any event stored in onboarding state
*/
clearOnboardingEvents: PropTypes.func
};

actionsList = [
Expand Down Expand Up @@ -182,7 +201,13 @@ class OptinMetrics extends PureComponent {
onCancel = async () => {
await AsyncStorage.setItem('@MetaMask:metricsOptIn', 'denied');
Analytics.disable();
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_METRICS_OPT_OUT);
InteractionManager.runAfterInteractions(() => {
if (this.props.events && this.props.events.length) {
this.props.events.forEach(e => Analytics.trackEvent(e));
}
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_METRICS_OPT_OUT);
this.props.clearOnboardingEvents();
});
this.continue();
};

Expand All @@ -192,7 +217,13 @@ class OptinMetrics extends PureComponent {
onConfirm = async () => {
await AsyncStorage.setItem('@MetaMask:metricsOptIn', 'agreed');
Analytics.enable();
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_METRICS_OPT_IN);
InteractionManager.runAfterInteractions(() => {
if (this.props.events && this.props.events.length) {
this.props.events.forEach(e => Analytics.trackEvent(e));
}
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_METRICS_OPT_IN);
this.props.clearOnboardingEvents();
});
this.continue();
};

Expand Down Expand Up @@ -257,11 +288,16 @@ class OptinMetrics extends PureComponent {
}
}

const mapStateToProps = state => ({
events: state.onboarding.events
});

const mapDispatchToProps = dispatch => ({
setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step))
setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)),
clearOnboardingEvents: () => dispatch(clearOnboardingEvents())
});

export default connect(
null,
mapStateToProps,
mapDispatchToProps
)(withNavigationFocus(OptinMetrics));
6 changes: 5 additions & 1 deletion app/components/UI/OptinMetrics/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ const mockStore = configureMockStore();

describe('OptinMetrics', () => {
it('should render correctly', () => {
const initialState = {};
const initialState = {
onboarding: {
event: 'event'
}
};

const wrapper = shallow(<OptinMetrics />, {
context: { store: mockStore(initialState) }
Expand Down
67 changes: 45 additions & 22 deletions app/components/Views/ImportWallet/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Platform, Alert, ActivityIndicator, Image, Text, View, ScrollView, StyleSheet } from 'react-native';
import {
Platform,
Alert,
ActivityIndicator,
Image,
Text,
View,
ScrollView,
StyleSheet,
InteractionManager
} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import { connect } from 'react-redux';
import { passwordSet, seedphraseBackedUp } from '../../../actions/user';
Expand All @@ -16,6 +26,9 @@ import SecureKeychain from '../../../core/SecureKeychain';
import AppConstants from '../../../core/AppConstants';
import PubNubWrapper from '../../../util/syncWithExtension';
import AnimatedFox from 'react-native-animated-fox';
import Analytics from '../../../core/Analytics';
import ANALYTICS_EVENT_OPTS from '../../../util/analytics';
import { saveOnboardingEvent } from '../../../actions/onboarding';

const styles = StyleSheet.create({
scroll: {
Expand Down Expand Up @@ -120,7 +133,11 @@ class ImportWallet extends PureComponent {
/**
* Selected address
*/
selectedAddress: PropTypes.string
selectedAddress: PropTypes.string,
/**
* Save onboarding event to state
*/
saveOnboardingEvent: PropTypes.func
};

seedwords = null;
Expand Down Expand Up @@ -297,7 +314,19 @@ class ImportWallet extends PureComponent {

onPressImport = () => {
const { existingUser } = this.state;
const action = () => this.props.navigation.push('ImportFromSeed');
const action = () => {
this.props.navigation.push('ImportFromSeed');
InteractionManager.runAfterInteractions(async () => {
if (Analytics.getEnabled()) {
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_IMPORT_WITH_SEEDPHRASE);
return;
}
const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn');
if (!metricsOptIn) {
this.props.saveOnboardingEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_IMPORT_WITH_SEEDPHRASE);
}
});
};
if (existingUser) {
this.alertExistingUser(action);
} else {
Expand All @@ -324,24 +353,17 @@ class ImportWallet extends PureComponent {
);
return false;
}

if (this.props.navigation.getParam('existingUser', false)) {
Alert.alert(
strings('sync_with_extension.warning_title'),
strings('sync_with_extension.warning_message'),
[
{
text: strings('sync_with_extension.warning_cancel_button'),
onPress: () => false,
style: 'cancel'
},
{ text: strings('sync_with_extension.warning_ok_button'), onPress: () => this.showQrCode() }
],
{ cancelable: false }
);
} else {
this.showQrCode();
}
InteractionManager.runAfterInteractions(async () => {
if (Analytics.getEnabled()) {
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_SYNC_WITH_EXTENSION);
return;
}
const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn');
if (!metricsOptIn) {
this.props.saveOnboardingEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_SYNC_WITH_EXTENSION);
}
});
this.showQrCode();
};

renderLoader() {
Expand Down Expand Up @@ -439,7 +461,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
passwordHasBeenSet: () => dispatch(passwordSet()),
setLockTime: time => dispatch(setLockTime(time)),
seedphraseBackedUp: () => dispatch(seedphraseBackedUp())
seedphraseBackedUp: () => dispatch(seedphraseBackedUp()),
saveOnboardingEvent: event => dispatch(saveOnboardingEvent(event))
});

export default connect(
Expand Down
44 changes: 38 additions & 6 deletions app/components/Views/Onboarding/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Platform, Text, View, ScrollView, StyleSheet, Image, Alert } from 'react-native';
import { Platform, Text, View, ScrollView, StyleSheet, Image, Alert, InteractionManager } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import StyledButton from '../../UI/StyledButton';
import AnimatedFox from 'react-native-animated-fox';
Expand All @@ -15,6 +15,7 @@ import FadeOutOverlay from '../../UI/FadeOutOverlay';
import TermsAndConditions from '../TermsAndConditions';
import Analytics from '../../../core/Analytics';
import ANALYTICS_EVENT_OPTS from '../../../util/analytics';
import { saveOnboardingEvent } from '../../../actions/onboarding';

const styles = StyleSheet.create({
scroll: {
Expand Down Expand Up @@ -108,7 +109,11 @@ class Onboarding extends PureComponent {
/**
* redux flag that indicates if the user set a password
*/
passwordSet: PropTypes.bool
passwordSet: PropTypes.bool,
/**
* Save onboarding event to state
*/
saveOnboardingEvent: PropTypes.func
};

state = {
Expand Down Expand Up @@ -141,8 +146,19 @@ class Onboarding extends PureComponent {

onPressCreate = () => {
const { existingUser } = this.state;
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_CREATE_NEW_WALLET);
const action = () => this.props.navigation.navigate('CreateWallet');
const action = () => {
this.props.navigation.navigate('CreateWallet');
InteractionManager.runAfterInteractions(async () => {
if (Analytics.getEnabled()) {
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_CREATE_NEW_WALLET);
return;
}
const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn');
if (!metricsOptIn) {
this.props.saveOnboardingEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_CREATE_NEW_WALLET);
}
});
};
if (existingUser) {
this.alertExistingUser(action);
} else {
Expand All @@ -152,7 +168,16 @@ class Onboarding extends PureComponent {

onPressImport = () => {
this.props.navigation.push('ImportWallet');
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_IMPORT_WALLET);
InteractionManager.runAfterInteractions(async () => {
if (Analytics.getEnabled()) {
Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_IMPORT_WALLET);
return;
}
const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn');
if (!metricsOptIn) {
this.props.saveOnboardingEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_IMPORT_WALLET);
}
});
};

alertExistingUser = callback => {
Expand Down Expand Up @@ -246,4 +271,11 @@ const mapStateToProps = state => ({
passwordSet: state.user.passwordSet
});

export default connect(mapStateToProps)(Onboarding);
const mapDispatchToProps = dispatch => ({
saveOnboardingEvent: event => dispatch(saveOnboardingEvent(event))
});

export default connect(
mapStateToProps,
mapDispatchToProps
)(Onboarding);
4 changes: 3 additions & 1 deletion app/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import transactionReducer from './transaction';
import userReducer from './user';
import wizardReducer from './wizard';
import analyticsReducer from './analytics';
import onboardingReducer from './onboarding';
import { combineReducers } from 'redux';

const rootReducer = combineReducers({
Expand All @@ -22,7 +23,8 @@ const rootReducer = combineReducers({
alert: alertReducer,
transaction: transactionReducer,
user: userReducer,
wizard: wizardReducer
wizard: wizardReducer,
onboarding: onboardingReducer
});

export default rootReducer;
31 changes: 31 additions & 0 deletions app/reducers/onboarding/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { REHYDRATE } from 'redux-persist';

const initialState = {
events: []
};

/**
* Reducer to keep track of user oboarding actions to send it to analytics if the user
* decides to optin after finishing onboarding flow
*/
const onboardingReducer = (state = initialState, action) => {
switch (action.type) {
case REHYDRATE:
if (action.payload && action.payload.onboarding) {
return { ...state, ...action.payload.onboarding };
}
return state;
case 'SAVE_EVENT':
state.events.push(action.event);
return state;
case 'CLEAR_EVENTS':
return {
...state,
events: []
};
default:
return state;
}
};

export default onboardingReducer;
13 changes: 13 additions & 0 deletions app/util/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const NAMES = {
ONBOARDING_METRICS_OPT_OUT: 'Metrics Opt Out',
ONBOARDING_SELECTED_CREATE_NEW_WALLET: 'Selected Create New Wallet',
ONBOARDING_SELECTED_IMPORT_WALLET: 'Selected Import Wallet',
ONBOARDING_SELECTED_SYNC_WITH_EXTENSION: 'Selected Sync with Extension',
ONBOARDING_SELECTED_WITH_SEEDPHRASE: 'Selected Import with Seedphrase',
// Navigation Drawer
NAVIGATION_TAPS_ACCOUNT_NAME: 'Tapped Account Name / Profile',
NAVIGATION_TAPS_SEND: "Taps on 'Send'",
Expand Down Expand Up @@ -67,6 +69,7 @@ const ACTIONS = {
//Onboarding
METRICS_OPTS: 'Metrics Option',
IMPORT_OR_CREATE: 'Import or Create',
IMPORT_OR_SYNC: 'Import or Sync',
// Navigation Drawer
NAVIGATION_DRAWER: 'Navigation Drawer',
// Common Navigation
Expand Down Expand Up @@ -126,6 +129,16 @@ const ANALYTICS_EVENT_OPTS = {
ACTIONS.IMPORT_OR_CREATE,
NAMES.ONBOARDING_SELECTED_IMPORT_WALLET
),
ONBOARDING_SELECTED_IMPORT_WITH_SEEDPHRASE: generateOpt(
CATEGORIES.ONBOARDING,
ACTIONS.IMPORT_OR_SYNC,
NAMES.ONBOARDING_SELECTED_WITH_SEEDPHRASE
),
ONBOARDING_SELECTED_SYNC_WITH_EXTENSION: generateOpt(
CATEGORIES.ONBOARDING,
ACTIONS.IMPORT_OR_SYNC,
NAMES.ONBOARDING_SELECTED_SYNC_WITH_EXTENSION
),
// Navigation Drawer
NAVIGATION_TAPS_ACCOUNT_NAME: generateOpt(
CATEGORIES.NAVIGATION_DRAWER,
Expand Down

0 comments on commit 46aae2b

Please sign in to comment.