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": {
|
||||
"version": "github:lbryio/lbryinc#8e33473daa56ebe80b12509ac5374c5884efef90",
|
||||
"from": "github:lbryio/lbryinc#authentication-flow",
|
||||
"version": "github:lbryio/lbryinc#f5d23dc5ee80198bee8e859bb8487c1137f9fbef",
|
||||
"from": "github:lbryio/lbryinc#rewards",
|
||||
"requires": {
|
||||
"lbry-redux": "github:lbryio/lbry-redux#31f7afa8a37f5741dac01fc1ecdf153f3bed95dc",
|
||||
"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": {
|
||||
|
@ -5277,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="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6843,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",
|
||||
|
@ -6870,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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
@ -99,10 +100,21 @@ const walletStack = StackNavigator({
|
|||
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({
|
||||
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' } }
|
||||
}, {
|
||||
|
@ -189,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) {
|
||||
|
@ -201,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);
|
||||
}
|
||||
|
|
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 }});
|
||||
}} />}
|
||||
</TouchableOpacity>
|
||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
|
||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
searchReducer,
|
||||
walletReducer
|
||||
} from 'lbry-redux';
|
||||
import { authReducer, userReducer } from 'lbryinc';
|
||||
import { authReducer, rewardsReducer, userReducer } from 'lbryinc';
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
|
||||
|
@ -73,12 +73,13 @@ const reducers = combineReducers({
|
|||
claims: claimsReducer,
|
||||
costInfo: costInfoReducer,
|
||||
fileInfo: fileInfoReducer,
|
||||
notifications: notificationsReducer,
|
||||
search: searchReducer,
|
||||
wallet: walletReducer,
|
||||
nav: navigatorReducer,
|
||||
notifications: notificationsReducer,
|
||||
rewards: rewardsReducer,
|
||||
settings: settingsReducer,
|
||||
user: userReducer
|
||||
search: searchReducer,
|
||||
user: userReducer,
|
||||
wallet: walletReducer
|
||||
});
|
||||
|
||||
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 {
|
||||
render() {
|
||||
return (
|
||||
|
|
|
@ -11,9 +11,10 @@ import {
|
|||
import { normalizeURI } from 'lbry-redux';
|
||||
import moment from 'moment';
|
||||
import Colors from '../../styles/colors';
|
||||
import FileItem from '../../component/fileItem';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
import FloatingWalletBalance from '../../component/floatingWalletBalance';
|
||||
import FileItem from '../../component/fileItem';
|
||||
import RewardSummary from '../../component/rewardSummary';
|
||||
import UriBar from '../../component/uriBar';
|
||||
|
||||
class DiscoverPage extends React.PureComponent {
|
||||
|
@ -50,6 +51,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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
this.setState({ showSkip: (!email || email.trim().length === 0) });
|
||||
} 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>
|
||||
|
|
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 => ({
|
||||
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);
|
||||
|
|
|
@ -148,14 +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().then(deviceId => {
|
||||
authenticate(`android-${appVersion}`, deviceId);
|
||||
});
|
||||
} else {
|
||||
authenticate(appVersion);
|
||||
}
|
||||
authenticate(`android-${appVersion}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
|
|
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
|
||||
},
|
||||
warningText: {
|
||||
color: '#ffffff',
|
||||
color: Colors.White,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
lineHeight: 26
|
||||
lineHeight: 24
|
||||
},
|
||||
understand: {
|
||||
marginLeft: 16,
|
||||
|
|
|
@ -175,9 +175,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()));
|
||||
|
@ -197,8 +194,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,
|
||||
|
@ -220,7 +221,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);
|
||||
|
@ -317,10 +317,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);
|
||||
|
@ -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
|
||||
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) {
|
||||
|
|
|
@ -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,88 @@ 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) {
|
||||
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;
|
||||
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