Mobile rewards (#251)
This commit is contained in:
parent
050fc2b29e
commit
c25785ccf4
27 changed files with 961 additions and 78 deletions
35
app/package-lock.json
generated
35
app/package-lock.json
generated
|
@ -3966,20 +3966,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lbryinc": {
|
"lbryinc": {
|
||||||
"version": "github:lbryio/lbryinc#8e33473daa56ebe80b12509ac5374c5884efef90",
|
"version": "github:lbryio/lbryinc#f5d23dc5ee80198bee8e859bb8487c1137f9fbef",
|
||||||
"from": "github:lbryio/lbryinc#authentication-flow",
|
"from": "github:lbryio/lbryinc#rewards",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"lbry-redux": "github:lbryio/lbry-redux#31f7afa8a37f5741dac01fc1ecdf153f3bed95dc",
|
||||||
"reselect": "^3.0.0"
|
"reselect": "^3.0.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"lbry-redux": {
|
|
||||||
"version": "github:lbryio/lbry-redux#467e48c77b8004cef738e950bdcc67654748ae9f",
|
|
||||||
"from": "github:lbryio/lbry-redux#467e48c77b8004cef738e950bdcc67654748ae9f",
|
|
||||||
"requires": {
|
|
||||||
"proxy-polyfill": "0.1.6",
|
|
||||||
"reselect": "^3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lcid": {
|
"lcid": {
|
||||||
|
@ -5277,6 +5268,13 @@
|
||||||
"which-module": "^2.0.0",
|
"which-module": "^2.0.0",
|
||||||
"y18n": "^3.2.1",
|
"y18n": "^3.2.1",
|
||||||
"yargs-parser": "^7.0.0"
|
"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="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6843,9 +6841,9 @@
|
||||||
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
|
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
|
||||||
},
|
},
|
||||||
"y18n": {
|
"y18n": {
|
||||||
"version": "3.2.1",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||||
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
|
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
@ -6870,6 +6868,13 @@
|
||||||
"which-module": "^2.0.0",
|
"which-module": "^2.0.0",
|
||||||
"y18n": "^3.2.1",
|
"y18n": "^3.2.1",
|
||||||
"yargs-parser": "^7.0.0"
|
"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": {
|
"yargs-parser": {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base-64": "^0.1.0",
|
"base-64": "^0.1.0",
|
||||||
"lbry-redux": "lbryio/lbry-redux",
|
"lbry-redux": "lbryio/lbry-redux",
|
||||||
"lbryinc": "lbryio/lbryinc#authentication-flow",
|
"lbryinc": "lbryio/lbryinc#rewards",
|
||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"react": "16.2.0",
|
"react": "16.2.0",
|
||||||
"react-native": "0.55.3",
|
"react-native": "0.55.3",
|
||||||
|
|
|
@ -3,8 +3,9 @@ import AboutPage from '../page/about';
|
||||||
import DiscoverPage from '../page/discover';
|
import DiscoverPage from '../page/discover';
|
||||||
import FilePage from '../page/file';
|
import FilePage from '../page/file';
|
||||||
import FirstRunScreen from '../page/firstRun';
|
import FirstRunScreen from '../page/firstRun';
|
||||||
import SearchPage from '../page/search';
|
import RewardsPage from '../page/rewards';
|
||||||
import TrendingPage from '../page/trending';
|
import TrendingPage from '../page/trending';
|
||||||
|
import SearchPage from '../page/search';
|
||||||
import SettingsPage from '../page/settings';
|
import SettingsPage from '../page/settings';
|
||||||
import SplashScreen from '../page/splash';
|
import SplashScreen from '../page/splash';
|
||||||
import TransactionHistoryPage from '../page/transactionHistory';
|
import TransactionHistoryPage from '../page/transactionHistory';
|
||||||
|
@ -99,10 +100,21 @@ const walletStack = StackNavigator({
|
||||||
headerMode: 'screen'
|
headerMode: 'screen'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rewardsStack = StackNavigator({
|
||||||
|
Rewards: {
|
||||||
|
screen: RewardsPage,
|
||||||
|
navigationOptions: ({ navigation }) => ({
|
||||||
|
title: 'Rewards',
|
||||||
|
headerLeft: <Icon name="bars" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const drawer = DrawerNavigator({
|
const drawer = DrawerNavigator({
|
||||||
DiscoverStack: { screen: discoverStack },
|
DiscoverStack: { screen: discoverStack },
|
||||||
TrendingStack: { screen: trendingStack },
|
TrendingStack: { screen: trendingStack },
|
||||||
WalletStack: { screen: walletStack },
|
WalletStack: { screen: walletStack },
|
||||||
|
Rewards: { screen: rewardsStack },
|
||||||
Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } },
|
Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } },
|
||||||
About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } }
|
About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } }
|
||||||
}, {
|
}, {
|
||||||
|
@ -189,7 +201,7 @@ class AppWithNavigationState extends React.Component {
|
||||||
if (notification) {
|
if (notification) {
|
||||||
const { displayType, message } = notification;
|
const { displayType, message } = notification;
|
||||||
let currentDisplayType;
|
let currentDisplayType;
|
||||||
if (displayType.length) {
|
if (displayType && displayType.length) {
|
||||||
for (let i = 0; i < displayType.length; i++) {
|
for (let i = 0; i < displayType.length; i++) {
|
||||||
const type = displayType[i];
|
const type = displayType[i];
|
||||||
if (AppWithNavigationState.supportedDisplayTypes.indexOf(type) > -1) {
|
if (AppWithNavigationState.supportedDisplayTypes.indexOf(type) > -1) {
|
||||||
|
@ -201,6 +213,11 @@ class AppWithNavigationState extends React.Component {
|
||||||
currentDisplayType = displayType;
|
currentDisplayType = displayType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentDisplayType && message) {
|
||||||
|
// default to toast if no display type set and there is a message specified
|
||||||
|
currentDisplayType = 'toast';
|
||||||
|
}
|
||||||
|
|
||||||
if ('toast' === currentDisplayType) {
|
if ('toast' === currentDisplayType) {
|
||||||
ToastAndroid.show(message, ToastAndroid.SHORT);
|
ToastAndroid.show(message, ToastAndroid.SHORT);
|
||||||
}
|
}
|
||||||
|
|
9
app/src/component/deviceIdRewardSubcard/index.js
Normal file
9
app/src/component/deviceIdRewardSubcard/index.js
Normal file
|
@ -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);
|
48
app/src/component/deviceIdRewardSubcard/view.js
Normal file
48
app/src/component/deviceIdRewardSubcard/view.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// @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;
|
22
app/src/component/emailRewardSubcard/index.js
Normal file
22
app/src/component/emailRewardSubcard/index.js
Normal file
|
@ -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);
|
97
app/src/component/emailRewardSubcard/view.js
Normal file
97
app/src/component/emailRewardSubcard/view.js
Normal file
|
@ -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;
|
|
@ -69,7 +69,7 @@ class FileItem extends React.PureComponent {
|
||||||
navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
|
navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
|
||||||
}} />}
|
}} />}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
|
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,9 +54,7 @@ export default class Link extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text style={styles} onPress={onPress ? onPress : this.handlePress}>
|
<Text style={styles} onPress={onPress ? onPress : this.handlePress}>{text}</Text>
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
29
app/src/component/rewardCard/index.js
Normal file
29
app/src/component/rewardCard/index.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doNotify } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
doClaimRewardType,
|
||||||
|
doClaimRewardClearError,
|
||||||
|
makeSelectClaimRewardError,
|
||||||
|
makeSelectIsRewardClaimPending,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import RewardCard from './view';
|
||||||
|
|
||||||
|
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);
|
95
app/src/component/rewardCard/view.js
Normal file
95
app/src/component/rewardCard/view.js
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
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';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
canClaim: bool,
|
||||||
|
onClaimPress: object,
|
||||||
|
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> {
|
||||||
|
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, isPending, onClaimPress, reward } = this.props;
|
||||||
|
const claimed = !!reward.transaction_id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[rewardStyle.rewardCard, rewardStyle.row]}>
|
||||||
|
<View style={rewardStyle.leftCol}>
|
||||||
|
{!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>}
|
||||||
|
{isPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
|
||||||
|
</View>
|
||||||
|
<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>
|
||||||
|
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RewardCard;
|
15
app/src/component/rewardSummary/index.js
Normal file
15
app/src/component/rewardSummary/index.js
Normal file
|
@ -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);
|
55
app/src/component/rewardSummary/view.js
Normal file
55
app/src/component/rewardSummary/view.js
Normal file
|
@ -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 (this.state.actionsLeft === 0 || 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;
|
|
@ -51,7 +51,7 @@ class SearchResultItem extends React.PureComponent {
|
||||||
{!isResolvingUri && channel && <Text style={searchStyle.publisher}>{channel}</Text>}
|
{!isResolvingUri && channel && <Text style={searchStyle.publisher}>{channel}</Text>}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
|
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
searchReducer,
|
searchReducer,
|
||||||
walletReducer
|
walletReducer
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { authReducer, userReducer } from 'lbryinc';
|
import { authReducer, rewardsReducer, userReducer } from 'lbryinc';
|
||||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||||
import { createLogger } from 'redux-logger';
|
import { createLogger } from 'redux-logger';
|
||||||
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
|
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
|
||||||
|
@ -73,12 +73,13 @@ const reducers = combineReducers({
|
||||||
claims: claimsReducer,
|
claims: claimsReducer,
|
||||||
costInfo: costInfoReducer,
|
costInfo: costInfoReducer,
|
||||||
fileInfo: fileInfoReducer,
|
fileInfo: fileInfoReducer,
|
||||||
notifications: notificationsReducer,
|
|
||||||
search: searchReducer,
|
|
||||||
wallet: walletReducer,
|
|
||||||
nav: navigatorReducer,
|
nav: navigatorReducer,
|
||||||
|
notifications: notificationsReducer,
|
||||||
|
rewards: rewardsReducer,
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
user: userReducer
|
search: searchReducer,
|
||||||
|
user: userReducer,
|
||||||
|
wallet: walletReducer
|
||||||
});
|
});
|
||||||
|
|
||||||
const bulkThunk = createBulkThunkMiddleware();
|
const bulkThunk = createBulkThunkMiddleware();
|
||||||
|
@ -120,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 {
|
class LBRYApp extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -11,9 +11,10 @@ import {
|
||||||
import { normalizeURI } from 'lbry-redux';
|
import { normalizeURI } from 'lbry-redux';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Colors from '../../styles/colors';
|
import Colors from '../../styles/colors';
|
||||||
import FileItem from '../../component/fileItem';
|
|
||||||
import discoverStyle from '../../styles/discover';
|
import discoverStyle from '../../styles/discover';
|
||||||
import FloatingWalletBalance from '../../component/floatingWalletBalance';
|
import FloatingWalletBalance from '../../component/floatingWalletBalance';
|
||||||
|
import FileItem from '../../component/fileItem';
|
||||||
|
import RewardSummary from '../../component/rewardSummary';
|
||||||
import UriBar from '../../component/uriBar';
|
import UriBar from '../../component/uriBar';
|
||||||
|
|
||||||
class DiscoverPage extends React.PureComponent {
|
class DiscoverPage extends React.PureComponent {
|
||||||
|
@ -50,6 +51,7 @@ class DiscoverPage extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={discoverStyle.container}>
|
<View style={discoverStyle.container}>
|
||||||
|
<RewardSummary navigation={navigation} />
|
||||||
{!hasContent && fetchingFeaturedUris && (
|
{!hasContent && fetchingFeaturedUris && (
|
||||||
<View style={discoverStyle.busyContainer}>
|
<View style={discoverStyle.busyContainer}>
|
||||||
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
||||||
|
|
|
@ -19,6 +19,7 @@ class EmailCollectPage extends React.PureComponent {
|
||||||
email: null,
|
email: null,
|
||||||
authenticationStarted: false,
|
authenticationStarted: false,
|
||||||
authenticationFailed: false,
|
authenticationFailed: false,
|
||||||
|
placeholder: 'you@example.com',
|
||||||
statusTries: 0
|
statusTries: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,12 +64,16 @@ class EmailCollectPage extends React.PureComponent {
|
||||||
|
|
||||||
handleChangeText = (text) => {
|
handleChangeText = (text) => {
|
||||||
// save the value to the state email
|
// save the value to the state email
|
||||||
|
const { onEmailChanged } = this.props;
|
||||||
this.setState({ email: text });
|
this.setState({ email: text });
|
||||||
|
if (onEmailChanged) {
|
||||||
|
onEmailChanged(text);
|
||||||
|
}
|
||||||
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_EMAIL, text);
|
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_EMAIL, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { authenticating, authToken, onEmailViewLayout, emailToVerify } = this.props;
|
const { authenticating, authToken, onEmailChanged, onEmailViewLayout, emailToVerify } = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (this.state.authenticationFailed) {
|
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}>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>
|
<Text style={firstRunStyle.paragraph}>Please provide a valid email address below to be able to claim your rewards.</Text>
|
||||||
<TextInput style={firstRunStyle.emailInput}
|
<TextInput style={firstRunStyle.emailInput}
|
||||||
placeholder="you@example.com"
|
placeholder={this.state.placeholder}
|
||||||
underlineColorAndroid="transparent"
|
underlineColorAndroid="transparent"
|
||||||
value={this.state.email}
|
value={this.state.email}
|
||||||
onChangeText={text => this.handleChangeText(text)}
|
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>
|
<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>
|
</View>
|
||||||
|
|
|
@ -27,6 +27,7 @@ class FirstRunScreen extends React.PureComponent {
|
||||||
emailSubmitted: false,
|
emailSubmitted: false,
|
||||||
isFirstRun: false,
|
isFirstRun: false,
|
||||||
launchUrl: null,
|
launchUrl: null,
|
||||||
|
showSkip: false,
|
||||||
showBottomContainer: true
|
showBottomContainer: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -97,9 +98,21 @@ class FirstRunScreen extends React.PureComponent {
|
||||||
|
|
||||||
handleEmailCollectPageContinue() {
|
handleEmailCollectPageContinue() {
|
||||||
const { notify, addUserEmail } = this.props;
|
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 => {
|
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({
|
return notify({
|
||||||
message: 'Please provide a valid email address to continue.',
|
message: 'Please provide a valid email address to continue.',
|
||||||
displayType: ['toast'],
|
displayType: ['toast'],
|
||||||
|
@ -131,6 +144,14 @@ class FirstRunScreen extends React.PureComponent {
|
||||||
this.launchSplashScreen();
|
this.launchSplashScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEmailChanged = (email) => {
|
||||||
|
if ('email-collect' == this.state.currentPage) {
|
||||||
|
this.setState({ showSkip: (!email || email.trim().length === 0) });
|
||||||
|
} else {
|
||||||
|
this.setState({ showSkip: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
authenticating,
|
authenticating,
|
||||||
|
@ -149,7 +170,8 @@ class FirstRunScreen extends React.PureComponent {
|
||||||
page = (<EmailCollectPage authenticating={authenticating}
|
page = (<EmailCollectPage authenticating={authenticating}
|
||||||
authToken={authToken}
|
authToken={authToken}
|
||||||
generateAuthToken={generateAuthToken}
|
generateAuthToken={generateAuthToken}
|
||||||
onEmailViewLayout={() => this.setState({ showBottomContainer: true })} />);
|
onEmailChanged={this.onEmailChanged}
|
||||||
|
onEmailViewLayout={() => this.setState({ showBottomContainer: true, showSkip: true })} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -162,7 +184,7 @@ class FirstRunScreen extends React.PureComponent {
|
||||||
|
|
||||||
{!emailNewPending &&
|
{!emailNewPending &&
|
||||||
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
|
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
|
||||||
<Text style={firstRunStyle.buttonText}>Continue</Text>
|
<Text style={firstRunStyle.buttonText}>{this.state.showSkip ? 'Skip': 'Continue'}</Text>
|
||||||
</TouchableOpacity>}
|
</TouchableOpacity>}
|
||||||
</View>}
|
</View>}
|
||||||
</View>
|
</View>
|
||||||
|
|
28
app/src/page/rewards/index.js
Normal file
28
app/src/page/rewards/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doRewardList,
|
||||||
|
selectEmailVerifyErrorMessage,
|
||||||
|
selectEmailVerifyIsPending,
|
||||||
|
selectFetchingRewards,
|
||||||
|
selectUnclaimedRewards,
|
||||||
|
selectClaimedRewards,
|
||||||
|
selectUser,
|
||||||
|
} from 'lbryinc';
|
||||||
|
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),
|
||||||
|
user: selectUser(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchRewards: () => dispatch(doRewardList()),
|
||||||
|
notify: data => dispatch(doNotify(data)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(RewardsPage);
|
172
app/src/page/rewards/view.js
Normal file
172
app/src/page/rewards/view.js
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Lbry } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
DeviceEventEmitter,
|
||||||
|
ActivityIndicator,
|
||||||
|
NativeModules,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
View
|
||||||
|
} from 'react-native';
|
||||||
|
import { doInstallNew } from 'lbryinc';
|
||||||
|
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() {
|
||||||
|
if (!this.state.isRewardApproved) {
|
||||||
|
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>
|
||||||
|
{!this.state.canAcquireDeviceId && <DeviceIdRewardSubcard />}
|
||||||
|
{!this.state.isEmailVerified && <EmailRewardSubcard />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
phoneStatePermissionGranted = () => {
|
||||||
|
const { install, 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 => {
|
||||||
|
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||||
|
doInstallNew(`android-${appVersion}`, deviceId);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
notify({ message: error, displayType: ['toast'] });
|
||||||
|
this.setState({ canAcquireDeviceId: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUnclaimedRewards() {
|
||||||
|
const { claimed, fetching, rewards, user } = this.props;
|
||||||
|
|
||||||
|
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 app 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}
|
||||||
|
canClaim={!isNotEligible}
|
||||||
|
reward={reward}
|
||||||
|
reward_type={reward.reward_type} />)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderClaimedRewards() {
|
||||||
|
const { claimed } = this.props;
|
||||||
|
if (claimed && claimed.length) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{claimed.map(reward => <RewardCard key={reward.reward_type} reward={reward} />)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { user } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={rewardStyle.container}>
|
||||||
|
{this.renderVerification()}
|
||||||
|
<View style={rewardStyle.rewardsContainer}>
|
||||||
|
<ScrollView style={rewardStyle.scrollContainer} contentContainerStyle={rewardStyle.scrollContentContainer}>
|
||||||
|
{this.renderUnclaimedRewards()}
|
||||||
|
{this.renderClaimedRewards()}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RewardsPage;
|
|
@ -17,13 +17,13 @@ const select = state => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
authenticate: (appVersion, deviceId) => dispatch(doAuthenticate(appVersion, deviceId)),
|
authenticate: (appVersion, deviceId) => dispatch(doAuthenticate(appVersion, deviceId)),
|
||||||
deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
|
deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
|
||||||
balanceSubscribe: () => dispatch(doBalanceSubscribe()),
|
balanceSubscribe: () => dispatch(doBalanceSubscribe()),
|
||||||
notify: data => dispatch(doNotify(data)),
|
notify: data => dispatch(doNotify(data)),
|
||||||
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
|
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
|
||||||
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
|
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
|
||||||
verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
|
verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(SplashScreen);
|
export default connect(select, perform)(SplashScreen);
|
||||||
|
|
|
@ -148,14 +148,7 @@ class SplashScreen extends React.PureComponent {
|
||||||
balanceSubscribe();
|
balanceSubscribe();
|
||||||
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||||
this.setState({ shouldAuthenticate: true });
|
this.setState({ shouldAuthenticate: true });
|
||||||
if (NativeModules.UtilityModule) {
|
authenticate(`android-${appVersion}`);
|
||||||
// authenticate with the device ID if the method is available
|
|
||||||
NativeModules.UtilityModule.getDeviceId().then(deviceId => {
|
|
||||||
authenticate(`android-${appVersion}`, deviceId);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
authenticate(appVersion);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
makeSelectMetadataForUri,
|
makeSelectMetadataForUri,
|
||||||
selectDownloadingByOutpoint,
|
selectDownloadingByOutpoint,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
|
import { Lbryio, doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||||
import { Alert, NativeModules } from 'react-native';
|
import { Alert, NativeModules } from 'react-native';
|
||||||
import Constants from '../../constants';
|
import Constants from '../../constants';
|
||||||
|
|
||||||
|
@ -139,11 +140,17 @@ export function doStopDownloadingFile(uri, fileInfo) {
|
||||||
|
|
||||||
export function doDownloadFile(uri, streamInfo) {
|
export function doDownloadFile(uri, streamInfo) {
|
||||||
return dispatch => {
|
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());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
159
app/src/styles/reward.js
Normal file
159
app/src/styles/reward.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import Colors from './colors';
|
||||||
|
|
||||||
|
const rewardStyle = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
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,
|
||||||
|
marginBottom: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
rewardsContainer: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
scrollContentContainer: {
|
||||||
|
paddingBottom: 16
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
backgroundColor: Colors.White,
|
||||||
|
marginTop: 16,
|
||||||
|
marginLeft: 16,
|
||||||
|
marginRight: 16,
|
||||||
|
padding: 16
|
||||||
|
},
|
||||||
|
rewardCard: {
|
||||||
|
backgroundColor: Colors.White,
|
||||||
|
marginTop: 16,
|
||||||
|
marginLeft: 16,
|
||||||
|
marginRight: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
paddingBottom: 16
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: 'Metropolis-Regular',
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 24
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
fontFamily: 'Metropolis-Regular',
|
||||||
|
fontSize: 18,
|
||||||
|
marginLeft: 12
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
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
|
||||||
|
},
|
||||||
|
bottomMarginMedium: {
|
||||||
|
marginBottom: 16
|
||||||
|
},
|
||||||
|
bottomMarginLarge: {
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
color: Colors.LbryGreen,
|
||||||
|
fontFamily: 'Metropolis-Regular',
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
leftCol: {
|
||||||
|
width: '15%',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 6
|
||||||
|
},
|
||||||
|
midCol: {
|
||||||
|
width: '65%'
|
||||||
|
},
|
||||||
|
rightCol: {
|
||||||
|
width: '18%',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
rewardAmount: {
|
||||||
|
fontFamily: 'Metropolis-Regular',
|
||||||
|
fontSize: 26,
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
rewardCurrency: {
|
||||||
|
fontFamily: 'Metropolis-Regular'
|
||||||
|
},
|
||||||
|
rewardTitle: {
|
||||||
|
fontFamily: 'Metropolis-Regular',
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.LbryGreen,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
rewardDescription: {
|
||||||
|
fontFamily: 'Metropolis-Regular',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 18,
|
||||||
|
marginBottom: 4
|
||||||
|
},
|
||||||
|
claimed: {
|
||||||
|
color: Colors.LbryGreen,
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rewardStyle;
|
|
@ -128,10 +128,10 @@ const walletStyle = StyleSheet.create({
|
||||||
margin: 16
|
margin: 16
|
||||||
},
|
},
|
||||||
warningText: {
|
warningText: {
|
||||||
color: '#ffffff',
|
color: Colors.White,
|
||||||
fontFamily: 'Metropolis-Regular',
|
fontFamily: 'Metropolis-Regular',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
lineHeight: 26
|
lineHeight: 24
|
||||||
},
|
},
|
||||||
understand: {
|
understand: {
|
||||||
marginLeft: 16,
|
marginLeft: 16,
|
||||||
|
|
|
@ -175,9 +175,6 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case STORAGE_PERMISSION_REQ_CODE:
|
case STORAGE_PERMISSION_REQ_CODE:
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
// Request for the READ_PHONE_STATE permission
|
|
||||||
checkPhoneStatePermission(this);
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG && !Settings.canDrawOverlays(this)) {
|
if (BuildConfig.DEBUG && !Settings.canDrawOverlays(this)) {
|
||||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||||
Uri.parse("package:" + getPackageName()));
|
Uri.parse("package:" + getPackageName()));
|
||||||
|
@ -197,8 +194,12 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
||||||
|
|
||||||
case PHONE_STATE_PERMISSION_REQ_CODE:
|
case PHONE_STATE_PERMISSION_REQ_CODE:
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
// Permission granted
|
// Permission granted. Emit an onPhoneStatePermissionGranted event
|
||||||
acquireDeviceId(this);
|
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||||
|
if (reactContext != null) {
|
||||||
|
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("onPhoneStatePermissionGranted", null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Permission not granted. Simply show a message.
|
// Permission not granted. Simply show a message.
|
||||||
Toast.makeText(this,
|
Toast.makeText(this,
|
||||||
|
@ -220,7 +221,6 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
||||||
} else {
|
} else {
|
||||||
id = telephonyManager.getDeviceId();
|
id = telephonyManager.getDeviceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (SecurityException ex) {
|
} catch (SecurityException ex) {
|
||||||
// Maybe the permission was not granted? Try to acquire permission
|
// Maybe the permission was not granted? Try to acquire permission
|
||||||
checkPhoneStatePermission(context);
|
checkPhoneStatePermission(context);
|
||||||
|
@ -317,10 +317,10 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
||||||
super.onNewIntent(intent);
|
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) {
|
if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
// Should we show an explanation?
|
// 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();
|
Toast.makeText(context, rationale, Toast.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, requestCode);
|
ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, requestCode);
|
||||||
|
@ -328,13 +328,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
|
// Request read phone state permission
|
||||||
checkPermission(Manifest.permission.READ_PHONE_STATE,
|
checkPermission(Manifest.permission.READ_PHONE_STATE,
|
||||||
PHONE_STATE_PERMISSION_REQ_CODE,
|
PHONE_STATE_PERMISSION_REQ_CODE,
|
||||||
"LBRY requires optional access to be able to identify your device for rewards. " +
|
"LBRY requires optional access to be able to identify your device for rewards. " +
|
||||||
"You cannot claim rewards without this permission.",
|
"You cannot claim rewards without this permission.",
|
||||||
context);
|
context,
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isServiceRunning(Class<?> serviceClass) {
|
private boolean isServiceRunning(Class<?> serviceClass) {
|
||||||
|
|
|
@ -3,6 +3,9 @@ package io.lbry.browser.reactmodules;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.Manifest;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
@ -87,9 +90,88 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void getDeviceId(final Promise promise) {
|
public void getDeviceId(boolean requestPermission, final Promise promise) {
|
||||||
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
|
if (isEmulator()) {
|
||||||
String deviceId = sp.getString(MainActivity.DEVICE_ID_KEY, null);
|
promise.reject("Rewards cannot be claimed from an emulator nor virtual device.");
|
||||||
promise.resolve(deviceId);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (isEmulator()) {
|
||||||
|
promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue