From 6ce11695efc8bcd6661b3202bfbb9f131ae0cc3f Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Sun, 22 Jul 2018 09:19:32 +0100
Subject: [PATCH 01/12] start implementation of rewards page

---
 app/package-lock.json             |  9 +++++-
 app/package.json                  |  1 +
 app/src/component/AppNavigator.js | 14 ++++++++-
 app/src/page/rewards/index.js     | 22 ++++++++++++++
 app/src/page/rewards/view.js      | 49 +++++++++++++++++++++++++++++++
 app/src/styles/reward.js          | 33 +++++++++++++++++++++
 6 files changed, 126 insertions(+), 2 deletions(-)
 create mode 100644 app/src/page/rewards/index.js
 create mode 100644 app/src/page/rewards/view.js
 create mode 100644 app/src/styles/reward.js

diff --git a/app/package-lock.json b/app/package-lock.json
index 7b5fc652..2ba7a753 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -3953,12 +3953,19 @@
             }
         },
         "lbry-redux": {
-            "version": "github:lbryio/lbry-redux#8cbeed792906335577bbf4382a2707f8e463e242",
+            "version": "github:lbryio/lbry-redux#e0909b08647a790d155f3189b9f9bf0b3e55bd17",
             "requires": {
                 "proxy-polyfill": "0.1.6",
                 "reselect": "3.0.1"
             }
         },
+        "lbryinc": {
+            "version": "github:lbryio/lbryinc#217a603ceb1d00e602678348ea4012f646d9ed0b",
+            "requires": {
+                "lbry-redux": "github:lbryio/lbry-redux#e0909b08647a790d155f3189b9f9bf0b3e55bd17",
+                "reselect": "3.0.1"
+            }
+        },
         "lcid": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
diff --git a/app/package.json b/app/package.json
index 72c1b095..0aee1ec0 100644
--- a/app/package.json
+++ b/app/package.json
@@ -7,6 +7,7 @@
     },
     "dependencies": {
         "lbry-redux": "lbryio/lbry-redux",
+        "lbryinc": "lbryio/lbryinc#initial-module",
         "moment": "^2.22.1",
         "react": "16.2.0",
         "react-native": "0.55.3",
diff --git a/app/src/component/AppNavigator.js b/app/src/component/AppNavigator.js
index 91a4f0a1..aa37d046 100644
--- a/app/src/component/AppNavigator.js
+++ b/app/src/component/AppNavigator.js
@@ -3,8 +3,9 @@ import AboutPage from '../page/about';
 import DiscoverPage from '../page/discover';
 import FilePage from '../page/file';
 import FirstRunScreen from '../page/firstRun';
-import SearchPage from '../page/search';
+import RewardsPage from '../page/rewards';
 import TrendingPage from '../page/trending';
+import SearchPage from '../page/search';
 import SettingsPage from '../page/settings';
 import SplashScreen from '../page/splash';
 import TransactionHistoryPage from '../page/transactionHistory';
@@ -88,10 +89,21 @@ const walletStack = StackNavigator({
   headerMode: 'screen'
 });
 
+const rewardsStack = StackNavigator({
+  Rewards: {
+    screen: RewardsPage,
+    navigationOptions: ({ navigation }) => ({
+      title: 'Rewards',
+      headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
+    })
+  }
+});
+
 const drawer = DrawerNavigator({
   DiscoverStack: { screen: discoverStack },
   TrendingStack: { screen: trendingStack },
   WalletStack: { screen: walletStack },
+  Rewards: { screen: rewardsStack },
   Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } },
   About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } }
 }, {
diff --git a/app/src/page/rewards/index.js b/app/src/page/rewards/index.js
new file mode 100644
index 00000000..bd20a840
--- /dev/null
+++ b/app/src/page/rewards/index.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux';
+import {
+  selectFetchingRewards,
+  selectUnclaimedRewards,
+  selectClaimedRewards,
+  selectUser
+} from 'lbryinc';
+import { doRewardList } from 'redux/actions/rewards';
+import RewardsPage from './view';
+
+const select = state => ({
+  fetching: selectFetchingRewards(state),
+  rewards: selectUnclaimedRewards(state),
+  claimed: selectClaimedRewards(state),
+  user: selectUser(state),
+});
+
+const perform = dispatch => ({
+  fetchRewards: () => dispatch(doRewardList()),
+});
+
+export default connect(select, perform)(RewardsPage);
\ No newline at end of file
diff --git a/app/src/page/rewards/view.js b/app/src/page/rewards/view.js
new file mode 100644
index 00000000..82be1d65
--- /dev/null
+++ b/app/src/page/rewards/view.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import { Lbry } from 'lbry-redux';
+import { NativeModules, Text, View, ScrollView } from 'react-native';
+import Link from '../../component/link';
+import PageHeader from '../../component/pageHeader';
+import rewardStyle from '../../styles/reward';
+
+class RewardsPage extends React.PureComponent {
+  renderVerification() {
+    const { user } = this.props;
+    if (user && !user.is_reward_approved) {
+      if (!user.primary_email || !user.has_verified_email || !user.is_identity_verified) {
+        return (
+          <View style={rewardStyle.card}>
+            <Text style={rewardStyle.title}>Humans Only</Text>
+            <Text style={rewardStyle.text}>Rewards are for human beings only. You'll have to prove you're one of us before you can claim any rewards.</Text>
+          </View>
+        );
+      }
+
+      return (
+        <View>
+          <Text style={rewardStyle.text}>This account must undergo review.</Text>
+        </View>
+      );
+    }
+
+    return null;
+  }
+
+  renderUnclaimedRewards() {
+    return null;
+  }
+
+  render() {
+    return (
+      <View>
+        <PageHeader title={"Rewards"}
+          onBackPressed={() => { this.props.navigation.goBack(); }} />
+        <ScrollView style={rewardStyle.scrollContainer}>
+          {this.renderVerification()}
+          {this.renderUnclaimedRewards()}
+        </ScrollView>
+      </View>
+    );
+  }
+}
+
+export default RewardsPage;
diff --git a/app/src/styles/reward.js b/app/src/styles/reward.js
new file mode 100644
index 00000000..48e29ab6
--- /dev/null
+++ b/app/src/styles/reward.js
@@ -0,0 +1,33 @@
+import { StyleSheet } from 'react-native';
+import Colors from './colors';
+
+const walletStyle = StyleSheet.create({
+  row: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  card: {
+    backgroundColor: Colors.White,
+    marginTop: 16,
+    marginLeft: 16,
+    marginRight: 16,
+    padding: 16
+  },
+  title: {
+    fontFamily: 'Metropolis-Bold',
+    fontSize: 20,
+    marginBottom: 24
+  },
+  bottomMarginSmall: {
+    marginBottom: 8
+  },
+  bottomMarginMedium: {
+    marginBottom: 16
+  },
+  bottomMarginLarge: {
+    marginBottom: 24
+  }
+});
+
+export default walletStyle;
-- 
2.45.3


From df445bad77413514090ee3543a2d93c9df94907f Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Mon, 23 Jul 2018 15:15:56 +0100
Subject: [PATCH 02/12] Added reward card component

---
 app/package-lock.json                 |  2 +-
 app/package.json                      |  2 +-
 app/src/component/rewardCard/index.js |  5 +++
 app/src/component/rewardCard/view.js  | 40 +++++++++++++++++++++++
 app/src/page/discover/view.js         |  4 +--
 app/src/page/rewards/index.js         |  4 +--
 app/src/page/rewards/view.js          | 46 +++++++++++++++++++++++++--
 app/src/styles/reward.js              | 13 ++++++++
 8 files changed, 107 insertions(+), 9 deletions(-)
 create mode 100644 app/src/component/rewardCard/index.js
 create mode 100644 app/src/component/rewardCard/view.js

diff --git a/app/package-lock.json b/app/package-lock.json
index 2ba7a753..e491cd63 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -3960,7 +3960,7 @@
             }
         },
         "lbryinc": {
-            "version": "github:lbryio/lbryinc#217a603ceb1d00e602678348ea4012f646d9ed0b",
+            "version": "github:lbryio/lbryinc#0cca6c1811a8222de48099ec33b56450d7775dbd",
             "requires": {
                 "lbry-redux": "github:lbryio/lbry-redux#e0909b08647a790d155f3189b9f9bf0b3e55bd17",
                 "reselect": "3.0.1"
diff --git a/app/package.json b/app/package.json
index 0aee1ec0..b178cd6b 100644
--- a/app/package.json
+++ b/app/package.json
@@ -7,7 +7,7 @@
     },
     "dependencies": {
         "lbry-redux": "lbryio/lbry-redux",
-        "lbryinc": "lbryio/lbryinc#initial-module",
+        "lbryinc": "lbryio/lbryinc",
         "moment": "^2.22.1",
         "react": "16.2.0",
         "react-native": "0.55.3",
diff --git a/app/src/component/rewardCard/index.js b/app/src/component/rewardCard/index.js
new file mode 100644
index 00000000..f5cc0c4f
--- /dev/null
+++ b/app/src/component/rewardCard/index.js
@@ -0,0 +1,5 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import RewardCard from './view';
+
+export default connect(null, null)(RewardCard);
diff --git a/app/src/component/rewardCard/view.js b/app/src/component/rewardCard/view.js
new file mode 100644
index 00000000..85822e92
--- /dev/null
+++ b/app/src/component/rewardCard/view.js
@@ -0,0 +1,40 @@
+// @flow
+import React from 'react';
+import { Text, View } from 'react-native';
+import rewardStyle from '../../styles/reward';
+
+type Props = {
+  reward: {
+    id: string,
+    reward_title: string,
+    reward_amount: number,
+    transaction_id: string,
+    created_at: string,
+    reward_description: string,
+    reward_type: string,
+  },
+};
+
+class RewardCard extends React.PureComponent<Props> {
+  render() {
+    const { reward } = props;
+    const claimed = !!reward.transaction_id;
+
+    return (
+      <View style={[rewardStyle.card, rewardStyle.row]}>
+        <View style={rewardStyle.leftCol}>
+
+        </View>
+        <View style={rewardStyle.midCol}>
+          <View style={rewardStyle.rewardTitle}>{reward.reward_title}</View>
+          <View style={rewardStyle.rewardDescription}>{reward.reward_description}</View>
+        </View>
+        <View style={rewardStyle.rightCol}>
+          <View style={rewardStyle.rewardAmount}>{reward.reward_amount}</View>
+        </View>
+      </View>
+    );
+  }
+};
+
+export default RewardCard;
diff --git a/app/src/page/discover/view.js b/app/src/page/discover/view.js
index 4d603731..ae3b4309 100644
--- a/app/src/page/discover/view.js
+++ b/app/src/page/discover/view.js
@@ -23,7 +23,7 @@ class DiscoverPage extends React.PureComponent {
       if (startTime !== null && !isNaN(parseInt(startTime, 10))) {
         // We don't need this value anymore once we've retrieved it
         AsyncStorage.removeItem('firstLaunchTime');
-        
+
         // We know this is the first app launch because firstLaunchTime is set and it's a valid number
         const start = parseInt(startTime, 10);
         const now = moment().unix();
@@ -39,7 +39,7 @@ class DiscoverPage extends React.PureComponent {
         });
       }
     });
-    
+
     this.props.fetchFeaturedUris();
   }
 
diff --git a/app/src/page/rewards/index.js b/app/src/page/rewards/index.js
index bd20a840..96372492 100644
--- a/app/src/page/rewards/index.js
+++ b/app/src/page/rewards/index.js
@@ -1,17 +1,17 @@
 import { connect } from 'react-redux';
 import {
+  doRewardList,
   selectFetchingRewards,
   selectUnclaimedRewards,
   selectClaimedRewards,
   selectUser
 } from 'lbryinc';
-import { doRewardList } from 'redux/actions/rewards';
 import RewardsPage from './view';
 
 const select = state => ({
   fetching: selectFetchingRewards(state),
   rewards: selectUnclaimedRewards(state),
-  claimed: selectClaimedRewards(state),
+  /*claimed: selectClaimedRewards(state),*/
   user: selectUser(state),
 });
 
diff --git a/app/src/page/rewards/view.js b/app/src/page/rewards/view.js
index 82be1d65..ee791d8e 100644
--- a/app/src/page/rewards/view.js
+++ b/app/src/page/rewards/view.js
@@ -1,11 +1,17 @@
 import React from 'react';
 import { Lbry } from 'lbry-redux';
 import { NativeModules, Text, View, ScrollView } from 'react-native';
+import Colors from '../../styles/colors';
 import Link from '../../component/link';
 import PageHeader from '../../component/pageHeader';
+import RewardCard from '../../component/rewardCard';
 import rewardStyle from '../../styles/reward';
 
 class RewardsPage extends React.PureComponent {
+  componentDidMount() {
+    this.props.fetchRewards();
+  }
+
   renderVerification() {
     const { user } = this.props;
     if (user && !user.is_reward_approved) {
@@ -24,19 +30,53 @@ class RewardsPage extends React.PureComponent {
         </View>
       );
     }
+    console.log(user);
 
     return null;
   }
 
   renderUnclaimedRewards() {
-    return null;
+    const { fetching, rewards, user, claimed } = this.props;
+
+    console.log(fetching);
+    console.log(rewards);
+
+    if (fetching) {
+      return (
+        <View style={rewardStyle.busyContainer}>
+          <ActivityIndicator size="large" color={Colors.LbryGreen} />
+          <Text style={rewardStyle.infoText}>Fetching rewards...</Text>
+        </View>
+      );
+    } else if (user === null) {
+      return (
+        <View style={rewardStyle.busyContainer}>
+          <Text style={rewardStyle.infoText}>This application is unable to earn rewards due to an authentication failure.</Text>
+        </View>
+      );
+    } else if (!rewards || rewards.length <= 0) {
+      return (
+        <View style={rewardStyle.busyContainer}>
+          <Text style={rewardStyle.infoText}>
+            {(claimed && claimed.length) ? "You have claimed all available rewards! We're regularly adding more so be sure to check back later." :
+              "There are no rewards available at this time, please check back later."}
+          </Text>
+        </View>
+      );
+    }
+
+    const isNotEligible =
+      !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
+    return (
+      <View>
+        {rewards.map(reward => <RewardCard key={reward.reward_type} reward={reward} />)}
+      </View>
+    );
   }
 
   render() {
     return (
       <View>
-        <PageHeader title={"Rewards"}
-          onBackPressed={() => { this.props.navigation.goBack(); }} />
         <ScrollView style={rewardStyle.scrollContainer}>
           {this.renderVerification()}
           {this.renderUnclaimedRewards()}
diff --git a/app/src/styles/reward.js b/app/src/styles/reward.js
index 48e29ab6..99227be5 100644
--- a/app/src/styles/reward.js
+++ b/app/src/styles/reward.js
@@ -2,11 +2,24 @@ import { StyleSheet } from 'react-native';
 import Colors from './colors';
 
 const walletStyle = StyleSheet.create({
+  scrollContainer: {
+    flex: 1
+  },
   row: {
     flexDirection: 'row',
     justifyContent: 'space-between',
     alignItems: 'center'
   },
+  busyContainer: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  infoText: {
+    fontFamily: 'Metropolis-Regular',
+    fontSize: 18
+  },
   card: {
     backgroundColor: Colors.White,
     marginTop: 16,
-- 
2.45.3


From 7443e0c1a5cb92631b1cced292925528eedd5a3e Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Thu, 16 Aug 2018 15:36:38 +0100
Subject: [PATCH 03/12] ongoing rewards implementation

---
 app/src/component/link/view.js       | 14 +++++-----
 app/src/component/rewardCard/view.js |  9 ++++++-
 app/src/page/rewards/index.js        |  4 ++-
 app/src/page/rewards/view.js         | 38 +++++++++++++++++++++++-----
 app/src/styles/reward.js             | 29 ++++++++++++++++-----
 5 files changed, 72 insertions(+), 22 deletions(-)

diff --git a/app/src/component/link/view.js b/app/src/component/link/view.js
index b4df5284..86dc4148 100644
--- a/app/src/component/link/view.js
+++ b/app/src/component/link/view.js
@@ -10,10 +10,10 @@ export default class Link extends React.PureComponent {
     }
     this.addTappedStyle = this.addTappedStyle.bind(this)
   }
-  
+
   handlePress = () => {
     const { error, href, navigation, notify } = this.props;
-    
+
     if (navigation && href.startsWith('#')) {
       navigation.navigate(href.substring(1));
     } else {
@@ -32,14 +32,14 @@ export default class Link extends React.PureComponent {
     this.setState({ tappedStyle: true });
     setTimeout(() => { this.setState({ tappedStyle: false }); }, 2000);
   }
-  
+
   render() {
     const {
       onPress,
       style,
       text
     } = this.props;
-    
+
     let styles = [];
     if (style) {
       if (style.length) {
@@ -52,11 +52,9 @@ export default class Link extends React.PureComponent {
     if (this.props.effectOnTap && this.state.tappedStyle) {
       styles.push(this.props.effectOnTap);
     }
-    
+
     return (
-      <Text style={styles} onPress={onPress ? onPress : this.handlePress}>
-        {text}
-      </Text>
+      <Text style={styles} onPress={onPress ? onPress : this.handlePress}>{text}</Text>
     );
   }
 };
diff --git a/app/src/component/rewardCard/view.js b/app/src/component/rewardCard/view.js
index e125a624..ddc4249e 100644
--- a/app/src/component/rewardCard/view.js
+++ b/app/src/component/rewardCard/view.js
@@ -2,9 +2,12 @@
 import React from 'react';
 import { Text, TouchableOpacity, View } from 'react-native';
 import Icon from 'react-native-vector-icons/FontAwesome5';
+import Link from '../link';
 import rewardStyle from '../../styles/reward';
 
 type Props = {
+  canClaim: bool,
+  onClaimPress: object,
   reward: {
     id: string,
     reward_title: string,
@@ -22,7 +25,7 @@ class RewardCard extends React.PureComponent<Props> {
     const claimed = !!reward.transaction_id;
 
     return (
-      <View style={[rewardStyle.card, rewardStyle.row]}>
+      <View style={[rewardStyle.rewardCard, rewardStyle.row]}>
         <View style={rewardStyle.leftCol}>
           <TouchableOpacity onPress={() => { if (!claimed && onClaimPress) { onClaimPress(); } }}>
             <Icon name={claimed ? "check-circle" : "circle"}
@@ -33,6 +36,10 @@ class RewardCard extends React.PureComponent<Props> {
         <View style={rewardStyle.midCol}>
           <Text style={rewardStyle.rewardTitle}>{reward.reward_title}</Text>
           <Text style={rewardStyle.rewardDescription}>{reward.reward_description}</Text>
+          {claimed && <Link style={rewardStyle.link}
+                href={`https://explorer.lbry.io/tx/${reward.transaction_id}`}
+                text={reward.transaction_id.substring(0, 7)}
+                error={'The transaction URL could not be opened'} />}
         </View>
         <View style={rewardStyle.rightCol}>
           <Text style={rewardStyle.rewardAmount}>{reward.reward_amount}</Text>
diff --git a/app/src/page/rewards/index.js b/app/src/page/rewards/index.js
index 96372492..823f6d40 100644
--- a/app/src/page/rewards/index.js
+++ b/app/src/page/rewards/index.js
@@ -6,17 +6,19 @@ import {
   selectClaimedRewards,
   selectUser
 } from 'lbryinc';
+import { doNotify } from 'lbry-redux';
 import RewardsPage from './view';
 
 const select = state => ({
   fetching: selectFetchingRewards(state),
   rewards: selectUnclaimedRewards(state),
-  /*claimed: selectClaimedRewards(state),*/
+  claimed: selectClaimedRewards(state),
   user: selectUser(state),
 });
 
 const perform = dispatch => ({
   fetchRewards: () => dispatch(doRewardList()),
+  notify: data => dispatch(doNotify(data))
 });
 
 export default connect(select, perform)(RewardsPage);
\ No newline at end of file
diff --git a/app/src/page/rewards/view.js b/app/src/page/rewards/view.js
index 73f82eea..05d2eb87 100644
--- a/app/src/page/rewards/view.js
+++ b/app/src/page/rewards/view.js
@@ -34,6 +34,16 @@ class RewardsPage extends React.PureComponent {
     return null;
   }
 
+  onClaimRewardPress = () => {
+    const { notify, user } = this.props;
+    console.log(user);
+    const isNotEligible = !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
+    if (isNotEligible) {
+      notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.', displayType: ['toast'] });
+      return;
+    }
+  }
+
   renderUnclaimedRewards() {
     const { fetching, rewards, user, claimed } = this.props;
 
@@ -61,21 +71,37 @@ class RewardsPage extends React.PureComponent {
       );
     }
 
-    const isNotEligible =
-      !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
+    const isNotEligible = !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
     return (
-      <ScrollView contentContainerStyle={rewardStyle.scrollContentContainer}>
-        {rewards.map(reward => <RewardCard key={reward.reward_type} reward={reward} />)}
-      </ScrollView>
+      <View>
+        {rewards.map(reward => <RewardCard key={reward.reward_type}
+                                           canClaim={!isNotEligible}
+                                           reward={reward}
+                                           onClaimPress={this.onClaimRewardPress} />)}
+      </View>
     );
   }
 
+  renderClaimedRewards() {
+    const { claimed } = this.props;
+    if (claimed && claimed.length) {
+      return (
+        <View>
+          {claimed.map(reward => <RewardCard key={reward.reward_type} reward={reward} />)}
+        </View>
+      );
+    }
+  }
+
   render() {
     return (
       <View style={rewardStyle.container}>
         {this.renderVerification()}
         <View style={rewardStyle.rewardsContainer}>
-          {this.renderUnclaimedRewards()}
+          <ScrollView style={rewardStyle.scrollContainer} contentContainerStyle={rewardStyle.scrollContentContainer}>
+            {this.renderUnclaimedRewards()}
+            {this.renderClaimedRewards()}
+          </ScrollView>
         </View>
       </View>
     );
diff --git a/app/src/styles/reward.js b/app/src/styles/reward.js
index 1e31dd4d..7c93ca37 100644
--- a/app/src/styles/reward.js
+++ b/app/src/styles/reward.js
@@ -12,8 +12,10 @@ const rewardStyle = StyleSheet.create({
   },
   busyContainer: {
     flex: 1,
-    justifyContent: 'center',
+    marginTop: 32,
+    marginBottom: 16,
     alignItems: 'center',
+    justifyContent: 'center',
     flexDirection: 'row'
   },
   rewardsContainer: {
@@ -29,6 +31,14 @@ const rewardStyle = StyleSheet.create({
     marginRight: 16,
     padding: 16
   },
+  rewardCard: {
+    backgroundColor: Colors.White,
+    marginTop: 16,
+    marginLeft: 16,
+    marginRight: 16,
+    paddingTop: 16,
+    paddingBottom: 16
+  },
   text: {
     fontFamily: 'Metropolis-Regular',
     fontSize: 16,
@@ -54,16 +64,22 @@ const rewardStyle = StyleSheet.create({
   bottomMarginLarge: {
     marginBottom: 24
   },
+  link: {
+    color: Colors.LbryGreen,
+    fontFamily: 'Metropolis-Regular',
+    fontSize: 14,
+  },
   leftCol: {
-    width: '10%'
+    width: '15%',
+    alignItems: 'center',
+    paddingLeft: 6
   },
   midCol: {
     width: '65%'
   },
   rightCol: {
-    width: '15%',
-    alignItems: 'center',
-    justifyContent: 'center'
+    width: '18%',
+    alignItems: 'center'
   },
   rewardAmount: {
     fontFamily: 'Metropolis-Regular',
@@ -82,7 +98,8 @@ const rewardStyle = StyleSheet.create({
   rewardDescription: {
     fontFamily: 'Metropolis-Regular',
     fontSize: 14,
-    lineHeight: 18
+    lineHeight: 18,
+    marginBottom: 4
   },
   claimed: {
     color: Colors.LbryGreen,
-- 
2.45.3


From 861fb806ec357cc2d8cc621c0bcfb05574b6c3c5 Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Thu, 16 Aug 2018 17:31:03 +0100
Subject: [PATCH 04/12] increase reward amount font size

---
 app/src/styles/reward.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/src/styles/reward.js b/app/src/styles/reward.js
index 7c93ca37..656bb866 100644
--- a/app/src/styles/reward.js
+++ b/app/src/styles/reward.js
@@ -83,7 +83,7 @@ const rewardStyle = StyleSheet.create({
   },
   rewardAmount: {
     fontFamily: 'Metropolis-Regular',
-    fontSize: 24,
+    fontSize: 26,
     textAlign: 'center'
   },
   rewardCurrency: {
-- 
2.45.3


From a901589d7c59d6d2a073e1ce7a659f35fa908c10 Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Wed, 22 Aug 2018 13:26:07 +0100
Subject: [PATCH 05/12] add email verification and device ID request to rewards
 page

---
 .../component/deviceIdRewardSubcard/index.js  |   9 ++
 .../component/deviceIdRewardSubcard/view.js   |  46 ++++++++
 app/src/component/emailRewardSubcard/index.js |  22 ++++
 app/src/component/emailRewardSubcard/view.js  |  97 +++++++++++++++++
 app/src/component/fileItem/view.js            |   2 +-
 app/src/component/rewardSummary/index.js      |  15 +++
 app/src/component/rewardSummary/view.js       |  55 ++++++++++
 app/src/component/searchResultItem/view.js    |  10 +-
 app/src/page/discover/view.js                 |   4 +-
 .../firstRun/internal/email-collect-page.js   |  19 +++-
 app/src/page/firstRun/view.js                 |  30 +++++-
 app/src/page/rewards/index.js                 |   4 +
 app/src/page/rewards/view.js                  | 100 +++++++++++++++---
 app/src/page/splash/index.js                  |  14 +--
 app/src/page/splash/view.js                   |  11 +-
 app/src/styles/reward.js                      |  53 +++++++++-
 app/src/styles/wallet.js                      |   4 +-
 .../java/io/lbry/browser/MainActivity.java    |  29 +++--
 .../browser/reactmodules/UtilityModule.java   |  51 ++++++++-
 19 files changed, 515 insertions(+), 60 deletions(-)
 create mode 100644 app/src/component/deviceIdRewardSubcard/index.js
 create mode 100644 app/src/component/deviceIdRewardSubcard/view.js
 create mode 100644 app/src/component/emailRewardSubcard/index.js
 create mode 100644 app/src/component/emailRewardSubcard/view.js
 create mode 100644 app/src/component/rewardSummary/index.js
 create mode 100644 app/src/component/rewardSummary/view.js

diff --git a/app/src/component/deviceIdRewardSubcard/index.js b/app/src/component/deviceIdRewardSubcard/index.js
new file mode 100644
index 00000000..c79dc730
--- /dev/null
+++ b/app/src/component/deviceIdRewardSubcard/index.js
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux';
+import { doNotify } from 'lbry-redux';
+import DeviceIdRewardSubcard from './view';
+
+const perform = dispatch => ({
+  notify: data => dispatch(doNotify(data))
+});
+
+export default connect(null, perform)(DeviceIdRewardSubcard);
\ No newline at end of file
diff --git a/app/src/component/deviceIdRewardSubcard/view.js b/app/src/component/deviceIdRewardSubcard/view.js
new file mode 100644
index 00000000..63479ab4
--- /dev/null
+++ b/app/src/component/deviceIdRewardSubcard/view.js
@@ -0,0 +1,46 @@
+// @flow
+import React from 'react';
+import {
+  ActivityIndicator,
+  AsyncStorage,
+  NativeModules,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View
+} from 'react-native';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+import Button from '../button';
+import Colors from '../../styles/colors';
+import Constants from '../../constants';
+import Link from '../link';
+import rewardStyle from '../../styles/reward';
+
+class DeviceIdRewardSubcard extends React.PureComponent {
+  onAllowAccessPressed = () => {
+    if (!NativeModules.UtilityModule) {
+      return notify({
+        message: 'The device ID could not be obtained due to a missing module.',
+        displayType: ['toast']
+      });
+    }
+
+    NativeModules.UtilityModule.requestPhoneStatePermission();
+  }
+
+  render() {
+    return (
+      <View style={rewardStyle.subcard}>
+        <Text style={rewardStyle.subtitle}>Pending action: Device ID</Text>
+        <Text style={[rewardStyle.bottomMarginMedium, rewardStyle.subcardText]}>
+          The app requires the phone state permission in order to identify your device for reward eligibility.
+        </Text>
+        <Button style={rewardStyle.actionButton}
+                text={"Allow Access"}
+                onPress={this.onAllowAccessPressed} />
+      </View>
+    );
+  }
+};
+
+export default DeviceIdRewardSubcard;
diff --git a/app/src/component/emailRewardSubcard/index.js b/app/src/component/emailRewardSubcard/index.js
new file mode 100644
index 00000000..55998604
--- /dev/null
+++ b/app/src/component/emailRewardSubcard/index.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux';
+import {
+  doUserEmailNew,
+  selectEmailNewErrorMessage,
+  selectEmailNewIsPending,
+  selectEmailToVerify
+} from 'lbryinc';
+import { doNotify } from 'lbry-redux';
+import EmailRewardSubcard from './view';
+
+const select = state => ({
+  emailToVerify: selectEmailToVerify(state),
+  emailNewErrorMessage: selectEmailNewErrorMessage(state),
+  emailNewPending: selectEmailNewIsPending(state)
+});
+
+const perform = dispatch => ({
+  addUserEmail: email => dispatch(doUserEmailNew(email)),
+  notify: data => dispatch(doNotify(data))
+});
+
+export default connect(select, perform)(EmailRewardSubcard);
\ No newline at end of file
diff --git a/app/src/component/emailRewardSubcard/view.js b/app/src/component/emailRewardSubcard/view.js
new file mode 100644
index 00000000..7389f218
--- /dev/null
+++ b/app/src/component/emailRewardSubcard/view.js
@@ -0,0 +1,97 @@
+// @flow
+import React from 'react';
+import {
+  ActivityIndicator,
+  AsyncStorage,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View
+} from 'react-native';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+import Button from '../button';
+import Colors from '../../styles/colors';
+import Constants from '../../constants';
+import Link from '../link';
+import rewardStyle from '../../styles/reward';
+
+class EmailRewardSubcard extends React.PureComponent {
+  state = {
+    email: null,
+    verfiyStarted: false
+  };
+
+  componentDidMount() {
+    const { emailToVerify } = this.props;
+    AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
+      if (email && email.trim().length > 0) {
+        this.setState({ email });
+      } else {
+        this.setState({ email: emailToVerify });
+      }
+    });
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const { emailNewErrorMessage, emailNewPending } = nextProps;
+    const { notify } = this.props;
+
+    if (this.state.verifyStarted && !emailNewPending) {
+      if (emailNewErrorMessage) {
+        notify({ message: String(emailNewErrorMessage), displayType: ['toast']});
+        this.setState({ verifyStarted: false });
+      } else {
+        notify({
+          message: 'Please follow the instructions in the email sent to your address to continue.',
+          displayType: ['toast']
+        });
+      }
+    }
+  }
+
+  handleChangeText = (text) => {
+    // save the value to the state email
+    this.setState({ email: text });
+    AsyncStorage.setItem(Constants.KEY_FIRST_RUN_EMAIL, text);
+  }
+
+  onSendVerificationPressed = () => {
+    if (this.state.verifyStarted) {
+      return;
+    }
+
+    const { addUserEmail, notify } = this.props;
+    const { email } = this.state;
+    if (!email || email.trim().length === 0 || email.indexOf('@') === -1) {
+      return notify({
+        message: 'Please provide a valid email address to continue.',
+        displayType: ['toast'],
+      });
+    }
+
+    this.setState({ verifyStarted: true });
+    addUserEmail(email);
+  }
+
+  render() {
+    const { emailNewPending } = this.props;
+
+    return (
+      <View style={rewardStyle.subcard}>
+        <Text style={rewardStyle.subtitle}>Pending action: Verify Email</Text>
+        <Text style={rewardStyle.subcardText}>Please provide an email address to verify. If you received a link previously, please follow the instructions in the email to complete verification.</Text>
+        <TextInput style={rewardStyle.subcardTextInput}
+                   placeholder="you@example.com"
+                   underlineColorAndroid="transparent"
+                   value={this.state.email}
+                   onChangeText={text => this.handleChangeText(text)} />
+        {!this.state.verifyStarted && <Button style={rewardStyle.actionButton}
+                text={"Send Verification Email"}
+                onPress={this.onSendVerificationPressed} />}
+        {this.state.verifyStarted && emailNewPending && <ActivityIndicator size={"small"} color={Colors.LbryGreen} />}
+      </View>
+    );
+  }
+};
+
+export default EmailRewardSubcard;
diff --git a/app/src/component/fileItem/view.js b/app/src/component/fileItem/view.js
index 939f654a..883dc3bd 100644
--- a/app/src/component/fileItem/view.js
+++ b/app/src/component/fileItem/view.js
@@ -69,7 +69,7 @@ class FileItem extends React.PureComponent {
               navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
             }} />}
         </TouchableOpacity>
-        {obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
+        {obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
       </View>
     );
   }
diff --git a/app/src/component/rewardSummary/index.js b/app/src/component/rewardSummary/index.js
new file mode 100644
index 00000000..54dd41ef
--- /dev/null
+++ b/app/src/component/rewardSummary/index.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
+import RewardSummary from './view';
+
+const select = state => ({
+  unclaimedRewardAmount: selectUnclaimedRewardValue(state),
+  fetching: selectFetchingRewards(state),
+  user: selectUser(state)
+});
+
+const perform = dispatch => ({
+  fetchRewards: () => dispatch(doRewardList()),
+});
+
+export default connect(select, perform)(RewardSummary);
diff --git a/app/src/component/rewardSummary/view.js b/app/src/component/rewardSummary/view.js
new file mode 100644
index 00000000..1dc1f664
--- /dev/null
+++ b/app/src/component/rewardSummary/view.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import { NativeModules, Text, TouchableOpacity } from 'react-native';
+import rewardStyle from '../../styles/reward';
+
+class RewardSummary extends React.Component {
+  state = {
+    actionsLeft: 0
+  };
+
+  componentDidMount() {
+    this.props.fetchRewards();
+
+    const { user } = this.props;
+    let actionsLeft = 0;
+    if (!user || !user.has_verified_email) {
+        actionsLeft++;
+    }
+
+    this.setState({ actionsLeft }, () => {
+      if (NativeModules.UtilityModule) {
+        NativeModules.UtilityModule.canAcquireDeviceId().then(canAcquire => {
+          if (!canAcquire) {
+            this.setState({ actionsLeft: this.state.actionsLeft + 1 });
+            return;
+          }
+        }).catch(err => {
+          this.setState({ actionsLeft: this.state.actionsLeft + 1 });
+        });
+      } else {
+        // unable to retrieve device ID because the native module is not present.
+        this.setState({ actionsLeft: this.state.actionsLeft + 1 });
+      }
+    });
+  }
+
+  render() {
+    const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
+
+    if (unclaimedRewardAmount === 0) {
+      return null;
+    }
+
+    return (
+      <TouchableOpacity style={rewardStyle.summaryContainer} onPress={() => {
+        navigation.navigate('Rewards');
+      }}>
+        <Text style={rewardStyle.summaryText}>
+          You have {unclaimedRewardAmount} LBC in unclaimed rewards. You have {this.state.actionsLeft} action{this.state.actionsLeft === 1 ? '' : 's'} left to claim your first reward. Tap here to continue.
+        </Text>
+      </TouchableOpacity>
+    );
+  }
+}
+
+export default RewardSummary;
diff --git a/app/src/component/searchResultItem/view.js b/app/src/component/searchResultItem/view.js
index 58c318aa..b03ec4e8 100644
--- a/app/src/component/searchResultItem/view.js
+++ b/app/src/component/searchResultItem/view.js
@@ -18,18 +18,18 @@ class SearchResultItem extends React.PureComponent {
       onPress,
       navigation
     } = this.props;
-    
+
     const uri = normalizeURI(this.props.uri);
     const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
     const title = metadata && metadata.title ? metadata.title : parseURI(uri).contentName;
-    
+
     let name;
     let channel;
     if (claim) {
       name = claim.name;
       channel = claim.channel_name;
     }
-    
+
     return (
       <View style={style}>
         <TouchableOpacity style={style} onPress={onPress}>
@@ -46,12 +46,12 @@ class SearchResultItem extends React.PureComponent {
               <View style={searchStyle.row}>
                 <ActivityIndicator size={"small"} color={Colors.LbryGreen} />
               </View>
-            </View>)} 
+            </View>)}
             {!isResolvingUri && <Text style={searchStyle.title}>{title || name}</Text>}
             {!isResolvingUri && channel && <Text style={searchStyle.publisher}>{channel}</Text>}
           </View>
         </TouchableOpacity>
-        {obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
+        {obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
       </View>
     );
   }
diff --git a/app/src/page/discover/view.js b/app/src/page/discover/view.js
index a0473b6f..cec0900a 100644
--- a/app/src/page/discover/view.js
+++ b/app/src/page/discover/view.js
@@ -10,9 +10,10 @@ import {
 } from 'react-native';
 import { normalizeURI } from 'lbry-redux';
 import moment from 'moment';
-import FileItem from '../../component/fileItem';
 import discoverStyle from '../../styles/discover';
 import Colors from '../../styles/colors';
+import FileItem from '../../component/fileItem';
+import RewardSummary from '../../component/rewardSummary';
 import UriBar from '../../component/uriBar';
 
 class DiscoverPage extends React.PureComponent {
@@ -49,6 +50,7 @@ class DiscoverPage extends React.PureComponent {
 
     return (
       <View style={discoverStyle.container}>
+        <RewardSummary navigation={navigation} />
         {!hasContent && fetchingFeaturedUris && (
           <View style={discoverStyle.busyContainer}>
             <ActivityIndicator size="large" color={Colors.LbryGreen} />
diff --git a/app/src/page/firstRun/internal/email-collect-page.js b/app/src/page/firstRun/internal/email-collect-page.js
index 0f8aac2a..ccceecf8 100644
--- a/app/src/page/firstRun/internal/email-collect-page.js
+++ b/app/src/page/firstRun/internal/email-collect-page.js
@@ -19,6 +19,7 @@ class EmailCollectPage extends React.PureComponent {
     email: null,
     authenticationStarted: false,
     authenticationFailed: false,
+    placeholder: 'you@example.com',
     statusTries: 0
   };
 
@@ -63,12 +64,16 @@ class EmailCollectPage extends React.PureComponent {
 
   handleChangeText = (text) => {
     // save the value to the state email
+    const { onEmailChanged } = this.props;
     this.setState({ email: text });
+    if (onEmailChanged) {
+      onEmailChanged(text);
+    }
     AsyncStorage.setItem(Constants.KEY_FIRST_RUN_EMAIL, text);
   }
 
   render() {
-    const { authenticating, authToken, onEmailViewLayout, emailToVerify } = this.props;
+    const { authenticating, authToken, onEmailChanged, onEmailViewLayout, emailToVerify } = this.props;
 
     let content;
     if (this.state.authenticationFailed) {
@@ -92,10 +97,20 @@ class EmailCollectPage extends React.PureComponent {
           <Text style={firstRunStyle.paragraph}>You can earn LBRY Credits (LBC) rewards by completing various tasks in the app.</Text>
           <Text style={firstRunStyle.paragraph}>Please provide a valid email address below to be able to claim your rewards.</Text>
           <TextInput style={firstRunStyle.emailInput}
-              placeholder="you@example.com"
+              placeholder={this.state.placeholder}
               underlineColorAndroid="transparent"
               value={this.state.email}
               onChangeText={text => this.handleChangeText(text)}
+              onFocus={() => {
+                if (!this.state.email || this.state.email.length === 0) {
+                  this.setState({ placeholder: '' });
+                }
+              }}
+              onBlur={() => {
+                if (!this.state.email || this.state.email.length === 0) {
+                  this.setState({ placeholder: 'you@example.com' });
+                }
+              }}
               />
           <Text style={firstRunStyle.infoParagraph}>This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.</Text>
         </View>
diff --git a/app/src/page/firstRun/view.js b/app/src/page/firstRun/view.js
index e668083e..5c1a3665 100644
--- a/app/src/page/firstRun/view.js
+++ b/app/src/page/firstRun/view.js
@@ -27,6 +27,7 @@ class FirstRunScreen extends React.PureComponent {
     emailSubmitted: false,
     isFirstRun: false,
     launchUrl: null,
+    showSkip: false,
     showBottomContainer: true
   };
 
@@ -97,9 +98,21 @@ class FirstRunScreen extends React.PureComponent {
 
   handleEmailCollectPageContinue() {
     const { notify, addUserEmail } = this.props;
-    // validate the email
+    const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
+
     AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
-      if (!email || email.trim().length === 0 || email.indexOf('@') === -1) {
+      if (!email || email.trim().length === 0) {
+        // no email provided. Skip.
+        if (this.state.currentPage === 'email-collect' && pageIndex === (FirstRunScreen.pages.length - 1)) {
+          this.closeFinalPage();
+        } else {
+          this.showNextPage();
+        }
+        return;
+      }
+
+      // validate the email
+      if (email.indexOf('@') === -1) {
         return notify({
           message: 'Please provide a valid email address to continue.',
           displayType: ['toast'],
@@ -131,6 +144,14 @@ class FirstRunScreen extends React.PureComponent {
     this.launchSplashScreen();
   }
 
+  onEmailChanged = (email) => {
+    if ('email-collect' == this.state.currentPage && (!email || email.trim().length === 0)) {
+      this.setState({ showSkip: true });
+    } else {
+      this.setState({ showSkip: false });
+    }
+  }
+
   render() {
     const {
       authenticating,
@@ -149,7 +170,8 @@ class FirstRunScreen extends React.PureComponent {
       page = (<EmailCollectPage authenticating={authenticating}
                                 authToken={authToken}
                                 generateAuthToken={generateAuthToken}
-                                onEmailViewLayout={() => this.setState({ showBottomContainer: true })} />);
+                                onEmailChanged={this.onEmailChanged}
+                                onEmailViewLayout={() => this.setState({ showBottomContainer: true, showSkip: true })} />);
     }
 
     return (
@@ -162,7 +184,7 @@ class FirstRunScreen extends React.PureComponent {
 
           {!emailNewPending &&
           <TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
-            <Text style={firstRunStyle.buttonText}>Continue</Text>
+            <Text style={firstRunStyle.buttonText}>{this.state.showSkip ? 'Skip': 'Continue'}</Text>
           </TouchableOpacity>}
         </View>}
       </View>
diff --git a/app/src/page/rewards/index.js b/app/src/page/rewards/index.js
index 823f6d40..8fd3c821 100644
--- a/app/src/page/rewards/index.js
+++ b/app/src/page/rewards/index.js
@@ -1,6 +1,8 @@
 import { connect } from 'react-redux';
 import {
   doRewardList,
+  selectEmailVerifyErrorMessage,
+  selectEmailVerifyIsPending,
   selectFetchingRewards,
   selectUnclaimedRewards,
   selectClaimedRewards,
@@ -10,6 +12,8 @@ import { doNotify } from 'lbry-redux';
 import RewardsPage from './view';
 
 const select = state => ({
+  emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
+  emailVerifyPending: selectEmailVerifyIsPending(state),
   fetching: selectFetchingRewards(state),
   rewards: selectUnclaimedRewards(state),
   claimed: selectClaimedRewards(state),
diff --git a/app/src/page/rewards/view.js b/app/src/page/rewards/view.js
index 05d2eb87..c6ce8dab 100644
--- a/app/src/page/rewards/view.js
+++ b/app/src/page/rewards/view.js
@@ -1,32 +1,78 @@
 import React from 'react';
 import { Lbry } from 'lbry-redux';
-import { ActivityIndicator, NativeModules, ScrollView, Text, View } from 'react-native';
+import {
+  DeviceEventEmitter,
+  ActivityIndicator,
+  NativeModules,
+  ScrollView,
+  Text,
+  View
+} from 'react-native';
 import Colors from '../../styles/colors';
 import Link from '../../component/link';
+import DeviceIdRewardSubcard from '../../component/deviceIdRewardSubcard';
+import EmailRewardSubcard from '../../component/emailRewardSubcard';
 import PageHeader from '../../component/pageHeader';
 import RewardCard from '../../component/rewardCard';
 import rewardStyle from '../../styles/reward';
 
 class RewardsPage extends React.PureComponent {
+  state = {
+    canAcquireDeviceId: false,
+    isEmailVerified: false,
+    isRewardApproved: false,
+    verifyRequestStarted: false,
+  };
+
   componentDidMount() {
+    DeviceEventEmitter.addListener('onPhoneStatePermissionGranted', this.phoneStatePermissionGranted);
+
     this.props.fetchRewards();
+
+    const { user } = this.props;
+    this.setState({
+      isEmailVerified: (user && user.primary_email && user.has_verified_email),
+      isRewardApproved: (user && user.is_reward_approved)
+    });
+
+    if (NativeModules.UtilityModule) {
+      const util = NativeModules.UtilityModule;
+      util.canAcquireDeviceId().then(canAcquireDeviceId => {
+        this.setState({ canAcquireDeviceId });
+      });
+    }
+  }
+
+  componentWillUnmount() {
+    DeviceEventEmitter.removeListener('onPhoneStatePermissionGranted', this.phoneStatePermissionGranted);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const { emailVerifyErrorMessage, emailVerifyPending } = nextProps;
+    if (emailVerifyPending) {
+      this.setState({ verifyRequestStarted: true });
+    }
+
+    if (this.state.verifyRequestStarted && !emailVerifyPending) {
+      const { user } = nextProps;
+      this.setState({ verifyRequestStarted: false });
+      if (!emailVerifyErrorMessage) {
+        this.setState({
+          isEmailVerified: true,
+          isRewardApproved: (user && user.is_reward_approved)
+        });
+      }
+    }
   }
 
   renderVerification() {
-    const { user } = this.props;
-    if (user && !user.is_reward_approved) {
-      if (!user.primary_email || !user.has_verified_email || !user.is_identity_verified) {
-        return (
-          <View style={[rewardStyle.card, rewardStyle.verification]}>
-            <Text style={rewardStyle.title}>Humans Only</Text>
-            <Text style={rewardStyle.text}>Rewards are for human beings only. You'll have to prove you're one of us before you can claim any rewards.</Text>
-          </View>
-        );
-      }
-
+    if (!this.state.isRewardApproved) {
       return (
         <View style={[rewardStyle.card, rewardStyle.verification]}>
-          <Text style={rewardStyle.text}>This account must undergo review.</Text>
+          <Text style={rewardStyle.title}>Humans Only</Text>
+          <Text style={rewardStyle.text}>Rewards are for human beings only. You'll have to prove you're one of us before you can claim any rewards.</Text>
+          {!this.state.canAcquireDeviceId && <DeviceIdRewardSubcard />}
+          {!this.state.isEmailVerified && <EmailRewardSubcard />}
         </View>
       );
     }
@@ -36,7 +82,6 @@ class RewardsPage extends React.PureComponent {
 
   onClaimRewardPress = () => {
     const { notify, user } = this.props;
-    console.log(user);
     const isNotEligible = !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
     if (isNotEligible) {
       notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.', displayType: ['toast'] });
@@ -44,8 +89,29 @@ class RewardsPage extends React.PureComponent {
     }
   }
 
+  phoneStatePermissionGranted = () => {
+    const { notify } = this.props;
+    if (NativeModules.UtilityModule) {
+      const util = NativeModules.UtilityModule;
+
+      // Double-check just to be sure
+      util.canAcquireDeviceId().then(canAcquireDeviceId => {
+        this.setState({ canAcquireDeviceId });
+        if (canAcquireDeviceId) {
+          util.getDeviceId(false).then(deviceId => {
+            // TODO: Send doInstallNew request with the device ID?
+
+          }).catch((error) => {
+            notify({ message: error, displayType: ['toast'] });
+            this.setState({ canAcquireDeviceId: false });
+          });
+        }
+      });
+    }
+  }
+
   renderUnclaimedRewards() {
-    const { fetching, rewards, user, claimed } = this.props;
+    const { claimed, fetching, rewards, user } = this.props;
 
     if (fetching) {
       return (
@@ -57,7 +123,7 @@ class RewardsPage extends React.PureComponent {
     } else if (user === null) {
       return (
         <View style={rewardStyle.busyContainer}>
-          <Text style={rewardStyle.infoText}>This application is unable to earn rewards due to an authentication failure.</Text>
+          <Text style={rewardStyle.infoText}>This app is unable to earn rewards due to an authentication failure.</Text>
         </View>
       );
     } else if (!rewards || rewards.length <= 0) {
@@ -94,6 +160,8 @@ class RewardsPage extends React.PureComponent {
   }
 
   render() {
+    const { user } = this.props;
+
     return (
       <View style={rewardStyle.container}>
         {this.renderVerification()}
diff --git a/app/src/page/splash/index.js b/app/src/page/splash/index.js
index 6b832de6..940e9dee 100644
--- a/app/src/page/splash/index.js
+++ b/app/src/page/splash/index.js
@@ -17,13 +17,13 @@ const select = state => ({
 });
 
 const perform = dispatch => ({
-    authenticate: (appVersion, deviceId) => dispatch(doAuthenticate(appVersion, deviceId)),
-    deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
-    balanceSubscribe: () => dispatch(doBalanceSubscribe()),
-    notify: data => dispatch(doNotify(data)),
-    setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
-    verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
-    verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
+  authenticate: (appVersion, deviceId) => dispatch(doAuthenticate(appVersion, deviceId)),
+  deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
+  balanceSubscribe: () => dispatch(doBalanceSubscribe()),
+  notify: data => dispatch(doNotify(data)),
+  setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
+  verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
+  verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
 });
 
 export default connect(select, perform)(SplashScreen);
diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js
index 1efbd63d..b53b40f2 100644
--- a/app/src/page/splash/view.js
+++ b/app/src/page/splash/view.js
@@ -148,14 +148,15 @@ class SplashScreen extends React.PureComponent {
         balanceSubscribe();
         NativeModules.VersionInfo.getAppVersion().then(appVersion => {
           this.setState({ shouldAuthenticate: true });
-          if (NativeModules.UtilityModule) {
+          /*if (NativeModules.UtilityModule) {
             // authenticate with the device ID if the method is available
-            NativeModules.UtilityModule.getDeviceId().then(deviceId => {
+            NativeModules.UtilityModule.getDeviceId(false).then(deviceId => {
               authenticate(`android-${appVersion}`, deviceId);
             });
-          } else {
-            authenticate(appVersion);
-          }
+          } else {*/
+
+          authenticate(appVersion);
+          //}
         });
       });
 
diff --git a/app/src/styles/reward.js b/app/src/styles/reward.js
index 656bb866..b69d82b7 100644
--- a/app/src/styles/reward.js
+++ b/app/src/styles/reward.js
@@ -10,6 +10,14 @@ const rewardStyle = StyleSheet.create({
     justifyContent: 'space-between',
     alignItems: 'center'
   },
+  actionButton: {
+    backgroundColor: Colors.LbryGreen,
+    alignSelf: 'flex-start',
+    paddingTop: 9,
+    paddingBottom: 9,
+    paddingLeft: 24,
+    paddingRight: 24
+  },
   busyContainer: {
     flex: 1,
     marginTop: 32,
@@ -50,11 +58,30 @@ const rewardStyle = StyleSheet.create({
     marginLeft: 12
   },
   title: {
-    fontFamily: 'Metropolis-SemiBold',
-    fontSize: 20,
-    marginBottom: 8,
+    fontFamily: 'Metropolis-Regular',
+    fontSize: 22,
+    marginBottom: 6,
     color: Colors.LbryGreen
   },
+  subtitle: {
+    fontFamily: 'Metropolis-Regular',
+    fontSize: 18,
+    marginBottom: 6,
+    color: Colors.LbryGreen
+  },
+  subcardText: {
+    fontFamily: 'Metropolis-Regular',
+    fontSize: 15,
+    lineHeight: 20,
+    marginLeft: 2,
+    marginRight: 2
+  },
+  subcardTextInput: {
+    fontFamily: 'Metropolis-Regular',
+    fontSize: 16,
+    marginTop: 2,
+    marginBottom: 2
+  },
   bottomMarginSmall: {
     marginBottom: 8
   },
@@ -106,6 +133,26 @@ const rewardStyle = StyleSheet.create({
   },
   disabled: {
     color: Colors.LightGrey
+  },
+  subcard: {
+    borderTopColor: Colors.VeryLightGrey,
+    borderTopWidth: 1,
+    paddingTop: 16,
+    paddingLeft: 8,
+    paddingRight: 8,
+    marginTop: 16,
+    marginLeft: -8,
+    marginRight: -8
+  },
+  summaryContainer: {
+    backgroundColor: Colors.LbryGreen,
+    padding: 12
+  },
+  summaryText: {
+    color: Colors.White,
+    fontFamily: 'Metropolis-Regular',
+    fontSize: 14,
+    lineHeight: 22
   }
 });
 
diff --git a/app/src/styles/wallet.js b/app/src/styles/wallet.js
index 8b07c679..eaf9bb30 100644
--- a/app/src/styles/wallet.js
+++ b/app/src/styles/wallet.js
@@ -128,10 +128,10 @@ const walletStyle = StyleSheet.create({
     margin: 16
   },
   warningText: {
-    color: '#ffffff',
+    color: Colors.White,
     fontFamily: 'Metropolis-Regular',
     fontSize: 16,
-    lineHeight: 30
+    lineHeight: 24
   },
   understand: {
     marginLeft: 16
diff --git a/src/main/java/io/lbry/browser/MainActivity.java b/src/main/java/io/lbry/browser/MainActivity.java
index 821d0537..3a720ba6 100644
--- a/src/main/java/io/lbry/browser/MainActivity.java
+++ b/src/main/java/io/lbry/browser/MainActivity.java
@@ -158,9 +158,6 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
         switch (requestCode) {
             case STORAGE_PERMISSION_REQ_CODE:
                 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-                    // Request for the READ_PHONE_STATE permission
-                    checkPhoneStatePermission(this);
-
                     if (BuildConfig.DEBUG && !Settings.canDrawOverlays(this)) {
                         Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                                                    Uri.parse("package:" + getPackageName()));
@@ -180,8 +177,12 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
 
             case PHONE_STATE_PERMISSION_REQ_CODE:
                 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-                    // Permission granted
-                    acquireDeviceId(this);
+                    // Permission granted. Emit an onPhoneStatePermissionGranted event
+                    ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
+                    if (reactContext != null) {
+                        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+                            .emit("onPhoneStatePermissionGranted", null);
+                    }
                 } else {
                     // Permission not granted. Simply show a message.
                     Toast.makeText(this,
@@ -203,7 +204,6 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
             } else {
                 id = telephonyManager.getDeviceId();
             }
-
         } catch (SecurityException ex) {
             // Maybe the permission was not granted? Try to acquire permission
             checkPhoneStatePermission(context);
@@ -294,10 +294,10 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
         super.onNewIntent(intent);
     }
 
-    private static void checkPermission(String permission, int requestCode, String rationale, Context context) {
+    private static void checkPermission(String permission, int requestCode, String rationale, Context context, boolean forceRequest) {
         if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
             // Should we show an explanation?
-            if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, permission)) {
+            if (!forceRequest && ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, permission)) {
                 Toast.makeText(context, rationale, Toast.LENGTH_LONG).show();
             } else {
                 ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, requestCode);
@@ -305,13 +305,22 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
         }
     }
 
-    private static void checkPhoneStatePermission(Context context) {
+    private static void checkPermission(String permission, int requestCode, String rationale, Context context) {
+        checkPermission(permission, requestCode, rationale, context, false);
+    }
+
+    public static boolean hasPermission(String permission, Context context) {
+        return (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
+    }
+
+    public static void checkPhoneStatePermission(Context context) {
         // Request read phone state permission
         checkPermission(Manifest.permission.READ_PHONE_STATE,
                         PHONE_STATE_PERMISSION_REQ_CODE,
                         "LBRY requires optional access to be able to identify your device for rewards. " +
                         "You cannot claim rewards without this permission.",
-                        context);
+                        context,
+                        true);
     }
 
     private boolean isServiceRunning(Class<?> serviceClass) {
diff --git a/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java b/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java
index 574ef869..15b6e3b3 100644
--- a/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java
+++ b/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java
@@ -3,6 +3,9 @@ package io.lbry.browser.reactmodules;
 import android.app.Activity;
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.Manifest;
+import android.os.Build;
+import android.telephony.TelephonyManager;
 import android.view.View;
 import android.view.WindowManager;
 
@@ -87,9 +90,49 @@ public class UtilityModule extends ReactContextBaseJavaModule {
     }
 
     @ReactMethod
-    public void getDeviceId(final Promise promise) {
-        SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
-        String deviceId = sp.getString(MainActivity.DEVICE_ID_KEY, null);
-        promise.resolve(deviceId);
+    public void getDeviceId(boolean requestPermission, final Promise promise) {
+        // TODO: Check if this is an emulator and handle accordingly
+
+        TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        String id = null;
+        try {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                id = telephonyManager.getImei(); // GSM
+                if (id == null) {
+                    id = telephonyManager.getMeid(); // CDMA
+                }
+            } else {
+                id = telephonyManager.getDeviceId();
+            }
+        } catch (SecurityException ex) {
+            // Maybe the permission was not granted? Try to acquire permission
+            if (requestPermission) {
+                requestPhoneStatePermission();
+            }
+        } catch (Exception ex) {
+            // id could not be obtained. Display a warning that rewards cannot be claimed.
+            promise.reject(ex.getMessage());
+        }
+
+        if (id == null || id.trim().length() == 0) {
+            promise.reject("Rewards cannot be claimed because your device could not be identified.");
+            return;
+        }
+
+        promise.resolve(id);
+    }
+
+    @ReactMethod
+    public void canAcquireDeviceId(final Promise promise) {
+        promise.resolve(MainActivity.hasPermission(Manifest.permission.READ_PHONE_STATE, MainActivity.getActivity()));
+    }
+
+    @ReactMethod
+    public void requestPhoneStatePermission() {
+        MainActivity activity = (MainActivity) MainActivity.getActivity();
+        if (activity != null) {
+            // Request for the READ_PHONE_STATE permission
+            MainActivity.checkPhoneStatePermission(activity);
+        }
     }
 }
-- 
2.45.3


From f781c230ae0e8205ed28584fe40adf739dac403f Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Fri, 24 Aug 2018 12:40:49 +0100
Subject: [PATCH 06/12] implement claim reward action and flow

---
 app/package-lock.json                 | 26 ++++++++++----
 app/package.json                      |  2 +-
 app/src/component/rewardCard/index.js | 28 +++++++++++++--
 app/src/component/rewardCard/view.js  | 50 ++++++++++++++++++++++++---
 app/src/index.js                      |  3 ++
 app/src/page/rewards/view.js          | 11 +-----
 6 files changed, 97 insertions(+), 23 deletions(-)

diff --git a/app/package-lock.json b/app/package-lock.json
index e244d44f..680a8063 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -3966,10 +3966,10 @@
             }
         },
         "lbryinc": {
-            "version": "github:lbryio/lbryinc#71fc9220e1a82f35489439b15b3cc375f44e53d7",
-            "from": "github:lbryio/lbryinc#authentication-flow",
+            "version": "github:lbryio/lbryinc#f5d23dc5ee80198bee8e859bb8487c1137f9fbef",
+            "from": "github:lbryio/lbryinc#rewards",
             "requires": {
-                "lbry-redux": "github:lbryio/lbry-redux#467e48c77b8004cef738e950bdcc67654748ae9f",
+                "lbry-redux": "github:lbryio/lbry-redux#31f7afa8a37f5741dac01fc1ecdf153f3bed95dc",
                 "reselect": "^3.0.0"
             }
         },
@@ -5268,6 +5268,13 @@
                         "which-module": "^2.0.0",
                         "y18n": "^3.2.1",
                         "yargs-parser": "^7.0.0"
+                    },
+                    "dependencies": {
+                        "y18n": {
+                            "version": "3.2.1",
+                            "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+                            "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
+                        }
                     }
                 }
             }
@@ -6834,9 +6841,9 @@
             "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
         },
         "y18n": {
-            "version": "3.2.1",
-            "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
-            "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+            "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
         },
         "yallist": {
             "version": "2.1.2",
@@ -6861,6 +6868,13 @@
                 "which-module": "^2.0.0",
                 "y18n": "^3.2.1",
                 "yargs-parser": "^7.0.0"
+            },
+            "dependencies": {
+                "y18n": {
+                    "version": "3.2.1",
+                    "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+                    "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
+                }
             }
         },
         "yargs-parser": {
diff --git a/app/package.json b/app/package.json
index 45169703..73c4f22a 100644
--- a/app/package.json
+++ b/app/package.json
@@ -8,7 +8,7 @@
     "dependencies": {
         "base-64": "^0.1.0",
         "lbry-redux": "lbryio/lbry-redux",
-        "lbryinc": "lbryio/lbryinc#authentication-flow",
+        "lbryinc": "lbryio/lbryinc#rewards",
         "moment": "^2.22.1",
         "react": "16.2.0",
         "react-native": "0.55.3",
diff --git a/app/src/component/rewardCard/index.js b/app/src/component/rewardCard/index.js
index f5cc0c4f..c0ea7e1c 100644
--- a/app/src/component/rewardCard/index.js
+++ b/app/src/component/rewardCard/index.js
@@ -1,5 +1,29 @@
-import React from 'react';
 import { connect } from 'react-redux';
+import { doNotify } from 'lbry-redux';
+import {
+  doClaimRewardType,
+  doClaimRewardClearError,
+  makeSelectClaimRewardError,
+  makeSelectIsRewardClaimPending,
+} from 'lbryinc';
 import RewardCard from './view';
 
-export default connect(null, null)(RewardCard);
+const makeSelect = () => {
+  const selectIsPending = makeSelectIsRewardClaimPending();
+  const selectError = makeSelectClaimRewardError();
+
+  const select = (state, props) => ({
+    errorMessage: selectError(state, props),
+    isPending: selectIsPending(state, props),
+  });
+
+  return select;
+};
+
+const perform = dispatch => ({
+  claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
+  clearError: reward => dispatch(doClaimRewardClearError(reward)),
+  notify: data => dispatch(doNotify(data))
+});
+
+export default connect(makeSelect, perform)(RewardCard);
diff --git a/app/src/component/rewardCard/view.js b/app/src/component/rewardCard/view.js
index ddc4249e..16d10fec 100644
--- a/app/src/component/rewardCard/view.js
+++ b/app/src/component/rewardCard/view.js
@@ -1,6 +1,7 @@
 // @flow
 import React from 'react';
-import { Text, TouchableOpacity, View } from 'react-native';
+import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
+import Colors from '../../styles/colors';
 import Icon from 'react-native-vector-icons/FontAwesome5';
 import Link from '../link';
 import rewardStyle from '../../styles/reward';
@@ -20,18 +21,59 @@ type Props = {
 };
 
 class RewardCard extends React.PureComponent<Props> {
+  state = {
+    claimStarted: false
+  };
+
+  componentWillReceiveProps(nextProps) {
+    const { errorMessage, isPending } = nextProps;
+    const { clearError, notify, reward } = this.props;
+    if (this.state.claimStarted && !isPending) {
+      if (errorMessage && errorMessage.trim().length > 0) {
+        notify({ message: errorMessage, displayType: ['toast'] });
+        clearError(reward);
+      } else {
+        notify({ message: 'Reward successfully claimed!', displayType: ['toast'] });
+      }
+      this.setState({ claimStarted: false });
+    }
+  }
+
+  onClaimPress = () => {
+    const {
+      canClaim,
+      claimReward,
+      notify,
+      reward
+    } = this.props;
+
+    if (!canClaim) {
+      notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.', displayType: ['toast'] });
+      return;
+    }
+
+    this.setState({ claimStarted: true }, () => {
+      claimReward(reward);
+    });
+  }
+
   render() {
-    const { canClaim, onClaimPress, reward } = this.props;
+    const { canClaim, isPending, onClaimPress, reward } = this.props;
     const claimed = !!reward.transaction_id;
 
     return (
       <View style={[rewardStyle.rewardCard, rewardStyle.row]}>
         <View style={rewardStyle.leftCol}>
-          <TouchableOpacity onPress={() => { if (!claimed && onClaimPress) { onClaimPress(); } }}>
+          {!isPending && <TouchableOpacity onPress={() => {
+              if (!claimed) {
+                this.onClaimPress();
+              }
+            }}>
             <Icon name={claimed ? "check-circle" : "circle"}
                   style={claimed ? rewardStyle.claimed : (canClaim ? rewardStyle.unclaimed : rewardStyle.disabled)}
                   size={20} />
-          </TouchableOpacity>
+          </TouchableOpacity>}
+          {isPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
         </View>
         <View style={rewardStyle.midCol}>
           <Text style={rewardStyle.rewardTitle}>{reward.reward_title}</Text>
diff --git a/app/src/index.js b/app/src/index.js
index aa14e3f4..f24da3e8 100644
--- a/app/src/index.js
+++ b/app/src/index.js
@@ -121,6 +121,9 @@ persistStore(store, persistOptions, err => {
   }
 });
 
+// TODO: Find i18n module that is compatible with react-native
+global.__ = (str) => str;
+
 class LBRYApp extends React.Component {
   render() {
     return (
diff --git a/app/src/page/rewards/view.js b/app/src/page/rewards/view.js
index c6ce8dab..179621b2 100644
--- a/app/src/page/rewards/view.js
+++ b/app/src/page/rewards/view.js
@@ -80,15 +80,6 @@ class RewardsPage extends React.PureComponent {
     return null;
   }
 
-  onClaimRewardPress = () => {
-    const { notify, user } = this.props;
-    const isNotEligible = !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
-    if (isNotEligible) {
-      notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.', displayType: ['toast'] });
-      return;
-    }
-  }
-
   phoneStatePermissionGranted = () => {
     const { notify } = this.props;
     if (NativeModules.UtilityModule) {
@@ -143,7 +134,7 @@ class RewardsPage extends React.PureComponent {
         {rewards.map(reward => <RewardCard key={reward.reward_type}
                                            canClaim={!isNotEligible}
                                            reward={reward}
-                                           onClaimPress={this.onClaimRewardPress} />)}
+                                           reward_type={reward.reward_type} />)}
       </View>
     );
   }
-- 
2.45.3


From 12e7e219c3a7f8e1236fc4cd6635de66570e34de Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Fri, 24 Aug 2018 12:57:18 +0100
Subject: [PATCH 07/12] add emulator checks

---
 .../browser/reactmodules/UtilityModule.java   | 41 ++++++++++++++++++-
 1 file changed, 40 insertions(+), 1 deletion(-)

diff --git a/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java b/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java
index 15b6e3b3..8f9d6bf7 100644
--- a/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java
+++ b/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java
@@ -91,7 +91,10 @@ public class UtilityModule extends ReactContextBaseJavaModule {
 
     @ReactMethod
     public void getDeviceId(boolean requestPermission, final Promise promise) {
-        // TODO: Check if this is an emulator and handle accordingly
+        if (isEmulator()) {
+            promise.reject("Rewards cannot be claimed from an emulator nor virtual device.");
+            return;
+        }
 
         TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
         String id = null;
@@ -124,6 +127,10 @@ public class UtilityModule extends ReactContextBaseJavaModule {
 
     @ReactMethod
     public void canAcquireDeviceId(final Promise promise) {
+        if (isEmulator()) {
+            promise.resolve(false);
+        }
+
         promise.resolve(MainActivity.hasPermission(Manifest.permission.READ_PHONE_STATE, MainActivity.getActivity()));
     }
 
@@ -135,4 +142,36 @@ public class UtilityModule extends ReactContextBaseJavaModule {
             MainActivity.checkPhoneStatePermission(activity);
         }
     }
+
+    private static boolean isEmulator() {
+        String buildModel = Build.MODEL.toLowerCase();
+        return (// Check FINGERPRINT
+                Build.FINGERPRINT.startsWith("generic") ||
+                Build.FINGERPRINT.startsWith("unknown") ||
+                Build.FINGERPRINT.contains("test-keys") ||
+
+                // Check MODEL
+                buildModel.contains("google_sdk") ||
+                buildModel.contains("emulator") ||
+                buildModel.contains("android sdk built for x86") ||
+
+                // Check MANUFACTURER
+                Build.MANUFACTURER.contains("Genymotion") ||
+                "unknown".equals(Build.MANUFACTURER) ||
+
+                // Check HARDWARE
+                Build.HARDWARE.contains("goldfish") ||
+                Build.HARDWARE.contains("vbox86") ||
+
+                // Check PRODUCT
+                "google_sdk".equals(Build.PRODUCT) ||
+                "sdk_google_phone_x86".equals(Build.PRODUCT) ||
+                "sdk".equals(Build.PRODUCT) ||
+                "sdk_x86".equals(Build.PRODUCT) ||
+                "vbox86p".equals(Build.PRODUCT) ||
+
+                // Check BRAND and DEVICE
+                (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
+               );
+    }
 }
-- 
2.45.3


From 37863bac80067df415f2b0685ed6db4a970c7743 Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Fri, 24 Aug 2018 13:23:44 +0100
Subject: [PATCH 08/12] call doInstallNew after the user allows access to
 device ID

---
 app/src/page/rewards/index.js |  4 ++--
 app/src/page/rewards/view.js  |  8 +++++---
 app/src/page/splash/view.js   | 10 +---------
 3 files changed, 8 insertions(+), 14 deletions(-)

diff --git a/app/src/page/rewards/index.js b/app/src/page/rewards/index.js
index 8fd3c821..89fbcc43 100644
--- a/app/src/page/rewards/index.js
+++ b/app/src/page/rewards/index.js
@@ -6,7 +6,7 @@ import {
   selectFetchingRewards,
   selectUnclaimedRewards,
   selectClaimedRewards,
-  selectUser
+  selectUser,
 } from 'lbryinc';
 import { doNotify } from 'lbry-redux';
 import RewardsPage from './view';
@@ -22,7 +22,7 @@ const select = state => ({
 
 const perform = dispatch => ({
   fetchRewards: () => dispatch(doRewardList()),
-  notify: data => dispatch(doNotify(data))
+  notify: data => dispatch(doNotify(data)),
 });
 
 export default connect(select, perform)(RewardsPage);
\ No newline at end of file
diff --git a/app/src/page/rewards/view.js b/app/src/page/rewards/view.js
index 179621b2..45af3729 100644
--- a/app/src/page/rewards/view.js
+++ b/app/src/page/rewards/view.js
@@ -8,6 +8,7 @@ import {
   Text,
   View
 } from 'react-native';
+import { doInstallNew } from 'lbryinc';
 import Colors from '../../styles/colors';
 import Link from '../../component/link';
 import DeviceIdRewardSubcard from '../../component/deviceIdRewardSubcard';
@@ -81,7 +82,7 @@ class RewardsPage extends React.PureComponent {
   }
 
   phoneStatePermissionGranted = () => {
-    const { notify } = this.props;
+    const { install, notify } = this.props;
     if (NativeModules.UtilityModule) {
       const util = NativeModules.UtilityModule;
 
@@ -90,8 +91,9 @@ class RewardsPage extends React.PureComponent {
         this.setState({ canAcquireDeviceId });
         if (canAcquireDeviceId) {
           util.getDeviceId(false).then(deviceId => {
-            // TODO: Send doInstallNew request with the device ID?
-
+            NativeModules.VersionInfo.getAppVersion().then(appVersion => {
+              doInstallNew(`android-${appVersion}`, deviceId);
+            });
           }).catch((error) => {
             notify({ message: error, displayType: ['toast'] });
             this.setState({ canAcquireDeviceId: false });
diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js
index b53b40f2..633447cf 100644
--- a/app/src/page/splash/view.js
+++ b/app/src/page/splash/view.js
@@ -148,15 +148,7 @@ class SplashScreen extends React.PureComponent {
         balanceSubscribe();
         NativeModules.VersionInfo.getAppVersion().then(appVersion => {
           this.setState({ shouldAuthenticate: true });
-          /*if (NativeModules.UtilityModule) {
-            // authenticate with the device ID if the method is available
-            NativeModules.UtilityModule.getDeviceId(false).then(deviceId => {
-              authenticate(`android-${appVersion}`, deviceId);
-            });
-          } else {*/
-
-          authenticate(appVersion);
-          //}
+          authenticate(`android-${appVersion}`);
         });
       });
 
-- 
2.45.3


From 9377d3854c9c4ab24afdc3845f6e47a4ebdc104c Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Sat, 25 Aug 2018 11:17:14 +0100
Subject: [PATCH 09/12] handle cases where displayType is not set for the
 doNotify action

---
 app/src/component/AppNavigator.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/app/src/component/AppNavigator.js b/app/src/component/AppNavigator.js
index d278bf61..0619a65f 100644
--- a/app/src/component/AppNavigator.js
+++ b/app/src/component/AppNavigator.js
@@ -201,7 +201,7 @@ class AppWithNavigationState extends React.Component {
     if (notification) {
       const { displayType, message } = notification;
       let currentDisplayType;
-      if (displayType.length) {
+      if (displayType && displayType.length) {
         for (let i = 0; i < displayType.length; i++) {
           const type = displayType[i];
           if (AppWithNavigationState.supportedDisplayTypes.indexOf(type) > -1) {
@@ -213,6 +213,11 @@ class AppWithNavigationState extends React.Component {
         currentDisplayType = displayType;
       }
 
+      if (!currentDisplayType && message) {
+        // default to toast if no display type set and there is a message specified
+        currentDisplayType = 'toast';
+      }
+
       if ('toast' === currentDisplayType) {
         ToastAndroid.show(message, ToastAndroid.SHORT);
       }
-- 
2.45.3


From dd5f13a9f58b2f7f49989dd4acbf31c9486a86e3 Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Sat, 25 Aug 2018 11:44:07 +0100
Subject: [PATCH 10/12] use lbrynet daemon v0.21.2

---
 buildozer.spec.sample  | 2 +-
 buildozer.spec.travis  | 2 +-
 buildozer.spec.vagrant | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/buildozer.spec.sample b/buildozer.spec.sample
index e39cd0f6..fbbc5af0 100644
--- a/buildozer.spec.sample
+++ b/buildozer.spec.sample
@@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
 
 # (list) Application requirements
 # comma seperated e.g. requirements = sqlite3,kivy
-requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
+requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git@v0.21.2#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
 
 # (str) Custom source folders for requirements
 # Sets custom source for any requirements with recipes
diff --git a/buildozer.spec.travis b/buildozer.spec.travis
index e39cd0f6..fbbc5af0 100644
--- a/buildozer.spec.travis
+++ b/buildozer.spec.travis
@@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
 
 # (list) Application requirements
 # comma seperated e.g. requirements = sqlite3,kivy
-requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
+requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git@v0.21.2#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
 
 # (str) Custom source folders for requirements
 # Sets custom source for any requirements with recipes
diff --git a/buildozer.spec.vagrant b/buildozer.spec.vagrant
index 228ba3ec..14983804 100644
--- a/buildozer.spec.vagrant
+++ b/buildozer.spec.vagrant
@@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
 
 # (list) Application requirements
 # comma seperated e.g. requirements = sqlite3,kivy
-requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
+requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git@v0.21.2#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
 
 # (str) Custom source folders for requirements
 # Sets custom source for any requirements with recipes
-- 
2.45.3


From 37bd2e4954140dd68dfe1824b3468055f7448a96 Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Mon, 27 Aug 2018 16:41:46 +0100
Subject: [PATCH 11/12] send file/view api call for action-based rewards

---
 app/src/redux/actions/file.js | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/app/src/redux/actions/file.js b/app/src/redux/actions/file.js
index 4b34fbd3..9821c11e 100644
--- a/app/src/redux/actions/file.js
+++ b/app/src/redux/actions/file.js
@@ -9,6 +9,7 @@ import {
     makeSelectMetadataForUri,
     selectDownloadingByOutpoint,
 } from 'lbry-redux';
+import { Lbryio, doClaimEligiblePurchaseRewards } from 'lbryinc';
 import { Alert, NativeModules } from 'react-native';
 import Constants from '../../constants';
 
@@ -139,11 +140,17 @@ export function doStopDownloadingFile(uri, fileInfo) {
 
 export function doDownloadFile(uri, streamInfo) {
   return dispatch => {
-    dispatch(doStartDownload(uri, streamInfo.outpoint));
+    const { outpoint, claim_id: claimId } = streamInfo;
+    dispatch(doStartDownload(uri, outpoint));
 
-    //analytics.apiLog(uri, streamInfo.output, streamInfo.claim_id);
+    // log the view
+    Lbryio.call('file', 'view', {
+      uri,
+      outpoint,
+      claim_id: claimId
+    }).catch(() => {});
 
-    //dispatch(doClaimEligiblePurchaseRewards());
+    dispatch(doClaimEligiblePurchaseRewards());
   };
 }
 
-- 
2.45.3


From 17f39d9c430f65a536c14b615f27b3f956be5e55 Mon Sep 17 00:00:00 2001
From: Akinwale Ariwodola <akinwale@gmail.com>
Date: Tue, 28 Aug 2018 11:36:32 +0100
Subject: [PATCH 12/12] update code formatting per review

---
 app/src/component/deviceIdRewardSubcard/view.js | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/app/src/component/deviceIdRewardSubcard/view.js b/app/src/component/deviceIdRewardSubcard/view.js
index 63479ab4..58ef455e 100644
--- a/app/src/component/deviceIdRewardSubcard/view.js
+++ b/app/src/component/deviceIdRewardSubcard/view.js
@@ -35,9 +35,11 @@ class DeviceIdRewardSubcard extends React.PureComponent {
         <Text style={[rewardStyle.bottomMarginMedium, rewardStyle.subcardText]}>
           The app requires the phone state permission in order to identify your device for reward eligibility.
         </Text>
-        <Button style={rewardStyle.actionButton}
-                text={"Allow Access"}
-                onPress={this.onAllowAccessPressed} />
+        <Button
+          style={rewardStyle.actionButton}
+          text={"Allow Access"}
+          onPress={this.onAllowAccessPressed}
+        />
       </View>
     );
   }
-- 
2.45.3