diff --git a/app/src/component/fileDownloadButton/view.js b/app/src/component/fileDownloadButton/view.js index cccf2841..7453d8e9 100644 --- a/app/src/component/fileDownloadButton/view.js +++ b/app/src/component/fileDownloadButton/view.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Text, View, TouchableOpacity } from 'react-native'; +import { NativeModules, Text, View, TouchableOpacity } from 'react-native'; import fileDownloadButtonStyle from '../../styles/fileDownloadButton'; class FileDownloadButton extends React.PureComponent { @@ -60,6 +60,9 @@ class FileDownloadButton extends React.PureComponent { } return ( { + if (NativeModules.Mixpanel) { + NativeModules.Mixpanel.track('Purchase Uri', { uri }); + } purchaseUri(uri); }}> Download diff --git a/app/src/component/fileItem/view.js b/app/src/component/fileItem/view.js index cde14dfa..7ececf27 100644 --- a/app/src/component/fileItem/view.js +++ b/app/src/component/fileItem/view.js @@ -1,7 +1,7 @@ import React from 'react'; import { normalizeURI } from 'lbry-redux'; import { NavigationActions } from 'react-navigation'; -import { Text, View, TouchableOpacity } from 'react-native'; +import { NativeModules, Text, View, TouchableOpacity } from 'react-native'; import FileItemMedia from '../fileItemMedia'; import FilePrice from '../filePrice'; import NsfwOverlay from '../nsfwOverlay'; @@ -58,6 +58,9 @@ class FileItem extends React.PureComponent { return ( { + if (NativeModules.Mixpanel) { + NativeModules.Mixpanel.track('Tap', { uri }); + } navigation.navigate('File', { uri: uri }); } }> diff --git a/app/src/component/mediaPlayer/view.js b/app/src/component/mediaPlayer/view.js index 8552f04b..469bd12a 100644 --- a/app/src/component/mediaPlayer/view.js +++ b/app/src/component/mediaPlayer/view.js @@ -1,6 +1,13 @@ import React from 'react'; import { Lbry } from 'lbry-redux'; -import { PanResponder, Text, View, ScrollView, TouchableOpacity } from 'react-native'; +import { + NativeModules, + PanResponder, + Text, + View, + ScrollView, + TouchableOpacity +} from 'react-native'; import Video from 'react-native-video'; import Icon from 'react-native-vector-icons/FontAwesome'; import FileItemMedia from '../fileItemMedia'; @@ -76,6 +83,10 @@ class MediaPlayer extends React.PureComponent { } if (this.state.firstPlay) { + if (NativeModules.Mixpanel) { + const { uri } = this.props; + NativeModules.Mixpanel.track('Play', { uri }); + } this.setState({ firstPlay: false }); this.hidePlayerControls(); } @@ -232,7 +243,7 @@ class MediaPlayer extends React.PureComponent { } render() { - const { fileInfo, title, thumbnail, style, fullScreenStyle } = this.props; + const { fileInfo, thumbnail, style, fullScreenStyle } = this.props; const flexCompleted = this.getCurrentTimePercentage() * 100; const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100; diff --git a/app/src/component/searchInput/index.js b/app/src/component/searchInput/index.js index baffcd6b..ab89d5a9 100644 --- a/app/src/component/searchInput/index.js +++ b/app/src/component/searchInput/index.js @@ -1,9 +1,15 @@ import { connect } from 'react-redux'; +import { NativeModules } from 'react-native'; import { doSearch, doUpdateSearchQuery } from 'lbry-redux'; import SearchInput from './view'; const perform = dispatch => ({ - search: search => dispatch(doSearch(search)), + search: search => { + if (NativeModules.Mixpanel) { + NativeModules.Mixpanel.track('Search', { query: search }); + } + return dispatch(doSearch(search)); + }, updateSearchQuery: query => dispatch(doUpdateSearchQuery(query, false)) }); diff --git a/app/src/page/file/view.js b/app/src/page/file/view.js index e3c1b79f..95209227 100644 --- a/app/src/page/file/view.js +++ b/app/src/page/file/view.js @@ -32,6 +32,9 @@ class FilePage extends React.PureComponent { StatusBar.setHidden(false); this.fetchFileInfo(this.props); this.fetchCostInfo(this.props); + if (NativeModules.Mixpanel) { + NativeModules.Mixpanel.track('Open File Page', { uri: this.props.navigation.state.params.uri }); + } } componentWillReceiveProps(nextProps) { @@ -136,6 +139,7 @@ class FilePage extends React.PureComponent { {isPlayable && !this.state.mediaLoaded && } {!completed && } {fileInfo && isPlayable && { this.setState({ mediaLoaded: true }); }}/>} diff --git a/app/src/page/search/index.js b/app/src/page/search/index.js index 1f56c3d6..c9f856da 100644 --- a/app/src/page/search/index.js +++ b/app/src/page/search/index.js @@ -14,7 +14,7 @@ const select = (state) => ({ }); const perform = dispatch => ({ - search: (query) => dispatch(doSearch(query)), + search: (query) => dispatch(doSearch(query)) }); export default connect(select, perform)(SearchPage); diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js index cdd8da07..2352e96d 100644 --- a/app/src/page/splash/view.js +++ b/app/src/page/splash/view.js @@ -1,6 +1,6 @@ import React from 'react'; import { Lbry } from 'lbry-redux'; -import { View, Text } from 'react-native'; +import { View, Text, NativeModules } from 'react-native'; import PropTypes from 'prop-types'; import splashStyle from '../../styles/splash'; @@ -66,6 +66,10 @@ class SplashScreen extends React.PureComponent { } componentDidMount() { + if (NativeModules.Mixpanel) { + NativeModules.Mixpanel.track('App Launch', null); + } + Lbry .connect() .then(() => { diff --git a/buildozer.spec.sample b/buildozer.spec.sample index 7cba7bcf..f191bb40 100644 --- a/buildozer.spec.sample +++ b/buildozer.spec.sample @@ -86,7 +86,7 @@ fullscreen = 0 #android.presplash_color = #FFFFFF # (list) Permissions -android.permissions = INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE +android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE # (int) Android API to use android.api = 23 @@ -148,7 +148,7 @@ android.react_src = ./app # (list) Gradle dependencies to add (currently works only with sdl2_gradle # bootstrap) -android.gradle_dependencies = com.android.support:appcompat-v7:23.4.0, com.facebook.react:react-native:+ +android.gradle_dependencies = com.android.support:appcompat-v7:23.4.0, com.facebook.react:react-native:+, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+ # (str) python-for-android branch to use, defaults to master #p4a.branch = stable diff --git a/buildozer.spec.travis b/buildozer.spec.travis index 7cba7bcf..f191bb40 100644 --- a/buildozer.spec.travis +++ b/buildozer.spec.travis @@ -86,7 +86,7 @@ fullscreen = 0 #android.presplash_color = #FFFFFF # (list) Permissions -android.permissions = INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE +android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE # (int) Android API to use android.api = 23 @@ -148,7 +148,7 @@ android.react_src = ./app # (list) Gradle dependencies to add (currently works only with sdl2_gradle # bootstrap) -android.gradle_dependencies = com.android.support:appcompat-v7:23.4.0, com.facebook.react:react-native:+ +android.gradle_dependencies = com.android.support:appcompat-v7:23.4.0, com.facebook.react:react-native:+, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+ # (str) python-for-android branch to use, defaults to master #p4a.branch = stable diff --git a/src/main/java/io/lbry/browser/reactmodules/MixpanelModule.java b/src/main/java/io/lbry/browser/reactmodules/MixpanelModule.java new file mode 100644 index 00000000..492a5dbd --- /dev/null +++ b/src/main/java/io/lbry/browser/reactmodules/MixpanelModule.java @@ -0,0 +1,54 @@ +package io.lbry.browser.reactmodules; + +import android.content.Context; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; + +import com.mixpanel.android.mpmetrics.MixpanelAPI; + +import java.util.HashMap; +import java.util.Map; +import org.json.JSONObject; +import org.json.JSONException; + +public class MixpanelModule extends ReactContextBaseJavaModule { + + private static final String MIXPANEL_TOKEN = "93b81fb957cb0ddcd3198c10853a6a95"; + + private Context context; + + private MixpanelAPI mixpanel; + + public MixpanelModule(ReactApplicationContext reactContext) { + super(reactContext); + this.context = reactContext; + this.mixpanel = MixpanelAPI.getInstance(this.context, MIXPANEL_TOKEN); + } + + @Override + public String getName() { + return "Mixpanel"; + } + + @ReactMethod + public void track(String name, ReadableMap payload) { + JSONObject props = new JSONObject(); + try { + if (payload != null) { + HashMap payloadMap = payload.toHashMap(); + for (Map.Entry entry : payloadMap.entrySet()) { + props.put(entry.getKey(), entry.getValue()); + } + } + } catch (JSONException e) { + // Cannot use props. Stick with empty props. + } + + if (mixpanel != null) { + mixpanel.track(name, props); + } + } +} diff --git a/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java b/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java index a630e8f3..53b9302d 100644 --- a/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java +++ b/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java @@ -7,6 +7,7 @@ import com.facebook.react.uimanager.ViewManager; import io.lbry.browser.reactmodules.DaemonServiceControlModule; import io.lbry.browser.reactmodules.DownloadManagerModule; +import io.lbry.browser.reactmodules.MixpanelModule; import io.lbry.browser.reactmodules.ScreenOrientationModule; import java.util.ArrayList; @@ -25,6 +26,7 @@ public class LbryReactPackage implements ReactPackage { modules.add(new DaemonServiceControlModule(reactContext)); modules.add(new DownloadManagerModule(reactContext)); + modules.add(new MixpanelModule(reactContext)); modules.add(new ScreenOrientationModule(reactContext)); return modules;