Cross-device sync implementation (#505)
* first run updates for sync * finish sync implementation and fix build for openssl 1.1.1b required for sdk * fix openssl recipe and tweak build * fix NativeModules import on wallet page * display total wallet balance. fix dispatch prop. * add pipeline status to README.md * remove unused build recipes * hide 'No, thanks' button during email new request * bumpversion 0.6.0 --> 0.6.1 * move unclaimed reward amount to the left of floating wallet balance * Upgrade to React Native 0.59.3 (#513) * upgrade to react native 0.59.3 * add FOREGROUND_SERVICE permission for Android 9 Pie (target sdk 28) * put android.permission.FOREGROUND_SERVICE permission directly in AndroidManifest * allow cleartext traffic * minor copy changes * enable secure password input and auto account_unlock on startup
This commit is contained in:
parent
3d1b07f11c
commit
d0226ab4cc
24 changed files with 462 additions and 8038 deletions
7929
app/package-lock.json
generated
7929
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,8 +8,8 @@
|
|||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"@expo/vector-icons": "^8.1.0",
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"lbryinc": "lbryio/lbryinc",
|
||||
"lbry-redux": "lbryio/lbry-redux#sync",
|
||||
"lbryinc": "lbryio/lbryinc#sync",
|
||||
"lodash": ">=4.17.11",
|
||||
"merge": ">=1.2.1",
|
||||
"moment": "^2.22.1",
|
||||
|
|
|
@ -37,6 +37,7 @@ import { doDeleteCompleteBlobs } from 'redux/actions/file';
|
|||
import { selectDrawerStack } from 'redux/selectors/drawer';
|
||||
import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
|
||||
import {
|
||||
doGetSync,
|
||||
doUserCheckEmailVerified,
|
||||
doUserEmailVerify,
|
||||
doUserEmailVerifyFailure,
|
||||
|
@ -280,13 +281,18 @@ class AppWithNavigationState extends React.Component {
|
|||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { user } = this.props;
|
||||
const { dispatch, user } = this.props;
|
||||
if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) {
|
||||
clearInterval(this.emailVerifyCheckInterval);
|
||||
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false');
|
||||
this.setState({ verifyPending: false });
|
||||
|
||||
ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
|
||||
|
||||
// upon successful email verification, check wallet sync
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||
dispatch(doGetSync(walletPassword));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import { selectTotalBalance } from 'lbry-redux';
|
||||
import { selectUnclaimedRewardValue } from 'lbryinc';
|
||||
import FloatingWalletBalance from './view';
|
||||
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
balance: selectTotalBalance(state),
|
||||
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||
});
|
||||
|
||||
|
|
|
@ -27,9 +27,8 @@ class FloatingWalletBalance extends React.PureComponent<Props> {
|
|||
<TouchableOpacity style={floatingButtonStyle.container}
|
||||
onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })}>
|
||||
{isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />}
|
||||
<Text style={floatingButtonStyle.text}>
|
||||
{(balance || balance === 0) && (formatCredits(parseFloat(balance), 2) + ' LBC')}
|
||||
</Text>
|
||||
{(!isNaN(balance) || balance === 0) && (
|
||||
<Text style={floatingButtonStyle.text}>{(formatCredits(parseFloat(balance), 2) + ' LBC')}</Text>)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -101,23 +101,9 @@ class UriBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
let style = [uriBarStyle.overlay];
|
||||
/*if (this.state.focused) {
|
||||
style.push(uriBarStyle.inFocus);
|
||||
}
|
||||
|
||||
{(this.state.focused && !this.state.directSearch) && (
|
||||
<View style={uriBarStyle.suggestions}>
|
||||
<FlatList style={uriBarStyle.suggestionList}
|
||||
data={suggestions}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
keyExtractor={(item, value) => item.value}
|
||||
renderItem={({item}) => (
|
||||
<UriBarItem
|
||||
item={item}
|
||||
navigation={navigation}
|
||||
onPress={() => this.handleItemPress(item)}
|
||||
/>)} />
|
||||
</View>)}*/
|
||||
// TODO: Add optional setting to enable URI / search bar suggestions
|
||||
/*if (this.state.focused) { style.push(uriBarStyle.inFocus); }*/
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
|
@ -166,6 +152,19 @@ class UriBar extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
}}/>
|
||||
{(this.state.focused && !this.state.directSearch) && (
|
||||
<View style={uriBarStyle.suggestions}>
|
||||
<FlatList style={uriBarStyle.suggestionList}
|
||||
data={suggestions}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
keyExtractor={(item, value) => item.value}
|
||||
renderItem={({item}) => (
|
||||
<UriBarItem
|
||||
item={item}
|
||||
navigation={navigation}
|
||||
onPress={() => this.handleItemPress(item)}
|
||||
/>)} />
|
||||
</View>)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import { selectTotalBalance } from 'lbry-redux';
|
||||
import WalletBalance from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
balance: selectTotalBalance(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(WalletBalance);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
import { formatCredits } from 'lbry-redux'
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
import { Lbry, formatCredits } from 'lbry-redux'
|
||||
import Address from 'component/address';
|
||||
import Button from 'component/button';
|
||||
import walletStyle from 'styles/wallet';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
const Constants = {
|
||||
FIRST_RUN_PAGE_WELCOME: "welcome",
|
||||
FIRST_RUN_PAGE_EMAIL_COLLECT: "email-collect",
|
||||
FIRST_RUN_PAGE_WALLET: "wallet",
|
||||
FIRST_RUN_PAGE_SKIP_ACCOUNT: "skip-account",
|
||||
|
||||
KEY_FIRST_RUN_EMAIL: "firstRunEmail",
|
||||
KEY_FIRST_RUN_PASSWORD: "firstRunPassword",
|
||||
KEY_SHOULD_VERIFY_EMAIL: "shouldVerifyEmail",
|
||||
KEY_EMAIL_VERIFY_PENDING: "emailVerifyPending",
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
homepageReducer,
|
||||
rewardsReducer,
|
||||
subscriptionsReducer,
|
||||
syncReducer,
|
||||
userReducer
|
||||
} from 'lbryinc';
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
|
@ -40,6 +41,7 @@ import drawerReducer from 'redux/reducers/drawer';
|
|||
import settingsReducer from 'redux/reducers/settings';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
|
||||
const globalExceptionHandler = (error, isFatal) => {
|
||||
if (error && NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.logException(isFatal, error.message ? error.message : "No message", error);
|
||||
|
@ -47,7 +49,6 @@ const globalExceptionHandler = (error, isFatal) => {
|
|||
};
|
||||
setJSExceptionHandler(globalExceptionHandler, true);
|
||||
|
||||
|
||||
function isFunction(object) {
|
||||
return typeof object === 'function';
|
||||
}
|
||||
|
@ -99,6 +100,7 @@ const reducers = combineReducers({
|
|||
settings: settingsReducer,
|
||||
search: searchReducer,
|
||||
subscriptions: subscriptionsReducer,
|
||||
sync: syncReducer,
|
||||
user: userReducer,
|
||||
wallet: walletReducer
|
||||
});
|
||||
|
|
|
@ -52,7 +52,7 @@ class EmailCollectPage extends React.PureComponent {
|
|||
this.setState({ authenticationStarted: true, authenticationFailed: false });
|
||||
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||
Lbry.status().then(info => {
|
||||
authenticate(appVersion, Platform.OS)
|
||||
authenticate(appVersion, Platform.OS);
|
||||
}).catch(error => {
|
||||
if (this.state.statusTries >= EmailCollectPage.MAX_STATUS_TRIES) {
|
||||
this.setState({ authenticationFailed: true });
|
||||
|
@ -98,9 +98,7 @@ class EmailCollectPage extends React.PureComponent {
|
|||
} else {
|
||||
content = (
|
||||
<View onLayout={onEmailViewLayout}>
|
||||
<Text style={firstRunStyle.title}>Rewards.</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.title}>Setup account</Text>
|
||||
<TextInput style={firstRunStyle.emailInput}
|
||||
placeholder={this.state.placeholder}
|
||||
underlineColorAndroid="transparent"
|
||||
|
@ -117,7 +115,8 @@ class EmailCollectPage extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<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.paragraph}>An account will allow you to earn rewards and keep your content and settings synced.</Text>
|
||||
<Text style={firstRunStyle.infoParagraph}>This information is disclosed only to LBRY, Inc. and not to the LBRY network.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
52
app/src/page/firstRun/internal/skip-account-page.js
Normal file
52
app/src/page/firstRun/internal/skip-account-page.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
Linking,
|
||||
NativeModules,
|
||||
Platform,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
import Constants from 'constants';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import firstRunStyle from 'styles/firstRun';
|
||||
|
||||
class SkipAccountPage extends React.PureComponent {
|
||||
state = {
|
||||
confirmed: false
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onSkipAccountViewLayout, onSkipSwitchChanged } = this.props;
|
||||
|
||||
const content = (
|
||||
<View onLayout={onSkipAccountViewLayout}>
|
||||
<View style={firstRunStyle.row}>
|
||||
<Icon name="exclamation-triangle" style={firstRunStyle.titleIcon} size={32} color={Colors.White} />
|
||||
<Text style={firstRunStyle.title}>Are you sure?</Text>
|
||||
</View>
|
||||
<Text style={firstRunStyle.paragraph}>Without an account, you will not receive rewards, sync and backup services, or security updates.</Text>
|
||||
|
||||
<View style={[firstRunStyle.row, firstRunStyle.confirmContainer]}>
|
||||
<View style={firstRunStyle.rowSwitch}>
|
||||
<Switch value={this.state.confirmed} onValueChange={value => { this.setState({ confirmed: value }); onSkipSwitchChanged(value); }} />
|
||||
</View>
|
||||
<Text style={firstRunStyle.rowParagraph}>I understand that by uninstalling LBRY I will lose any balances or published content with no recovery option.</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={firstRunStyle.container}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SkipAccountPage;
|
75
app/src/page/firstRun/internal/wallet-page.js
Normal file
75
app/src/page/firstRun/internal/wallet-page.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
NativeModules,
|
||||
Platform,
|
||||
Text,
|
||||
TextInput,
|
||||
View
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import Colors from 'styles/colors';
|
||||
import Constants from 'constants';
|
||||
import firstRunStyle from 'styles/firstRun';
|
||||
|
||||
class WalletPage extends React.PureComponent {
|
||||
state = {
|
||||
password: null,
|
||||
placeholder: 'password',
|
||||
statusTries: 0
|
||||
};
|
||||
|
||||
handleChangeText = (text) => {
|
||||
// save the value to the state email
|
||||
const { onPasswordChanged } = this.props;
|
||||
this.setState({ password: text });
|
||||
if (onPasswordChanged) {
|
||||
onPasswordChanged(text);
|
||||
}
|
||||
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, text);
|
||||
// simply set any string value to indicate that a passphrase was set on first run
|
||||
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_PASSWORD, "true");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onPasswordChanged, onWalletViewLayout } = this.props;
|
||||
|
||||
const content = (
|
||||
<View onLayout={onWalletViewLayout}>
|
||||
<Text style={firstRunStyle.title}>Password</Text>
|
||||
<Text style={firstRunStyle.paragraph}>Please enter a password to secure your account and wallet.</Text>
|
||||
<TextInput style={firstRunStyle.passwordInput}
|
||||
placeholder={this.state.placeholder}
|
||||
underlineColorAndroid="transparent"
|
||||
secureTextEntry={true}
|
||||
value={this.state.password}
|
||||
onChangeText={text => this.handleChangeText(text)}
|
||||
onFocus={() => {
|
||||
if (!this.state.password || this.state.password.length === 0) {
|
||||
this.setState({ placeholder: '' });
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!this.state.password || this.state.password.length === 0) {
|
||||
this.setState({ placeholder: 'password' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Text style={firstRunStyle.infoParagraph}>Note: for wallet security purposes, LBRY is unable to reset your password.</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={firstRunStyle.container}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletPage;
|
|
@ -1,18 +1,15 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import { View, Text, Linking } from 'react-native';
|
||||
import Colors from '../../../styles/colors';
|
||||
import firstRunStyle from '../../../styles/firstRun';
|
||||
import Colors from 'styles/colors';
|
||||
import firstRunStyle from 'styles/firstRun';
|
||||
|
||||
class WelcomePage extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<View style={firstRunStyle.container}>
|
||||
<Text style={firstRunStyle.title}>Welcome to LBRY.</Text>
|
||||
<Text style={firstRunStyle.paragraph}>LBRY is a decentralized peer-to-peer content sharing platform where
|
||||
you can upload and download videos, music, ebooks and other forms of digital content.</Text>
|
||||
<Text style={firstRunStyle.paragraph}>We make use of a blockchain which needs to be synchronized before
|
||||
you can use the app. Synchronization may take a while because this is the first app launch.</Text>
|
||||
<Text style={firstRunStyle.paragraph}>LBRY is a community-controlled content platform where you can find and publish videos, music, books, and more.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,14 +12,18 @@ import { NavigationActions, StackActions } from 'react-navigation';
|
|||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import Colors from 'styles/colors';
|
||||
import Constants from 'constants';
|
||||
import WalletPage from './internal/wallet-page';
|
||||
import WelcomePage from './internal/welcome-page';
|
||||
import EmailCollectPage from './internal/email-collect-page';
|
||||
import SkipAccountPage from './internal/skip-account-page';
|
||||
import firstRunStyle from 'styles/firstRun';
|
||||
|
||||
class FirstRunScreen extends React.PureComponent {
|
||||
static pages = [
|
||||
'welcome',
|
||||
'email-collect'
|
||||
Constants.FIRST_RUN_PAGE_WELCOME,
|
||||
Constants.FIRST_RUN_PAGE_EMAIL_COLLECT,
|
||||
Constants.FIRST_RUN_PAGE_WALLET,
|
||||
Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT,
|
||||
];
|
||||
|
||||
state = {
|
||||
|
@ -28,7 +32,9 @@ class FirstRunScreen extends React.PureComponent {
|
|||
isFirstRun: false,
|
||||
launchUrl: null,
|
||||
showSkip: false,
|
||||
showBottomContainer: true
|
||||
skipAccountConfirmed: false,
|
||||
showBottomContainer: true,
|
||||
walletPassword: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -63,8 +69,8 @@ class FirstRunScreen extends React.PureComponent {
|
|||
if (emailNewErrorMessage) {
|
||||
notify ({ message: String(emailNewErrorMessage), isError: true });
|
||||
} else {
|
||||
// Request successful. Navigate to discover.
|
||||
this.closeFinalPage();
|
||||
// Request successful. Navigate to next page (wallet).
|
||||
this.showNextPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,14 +86,40 @@ class FirstRunScreen extends React.PureComponent {
|
|||
navigation.dispatch(resetAction);
|
||||
}
|
||||
|
||||
handleLeftButtonPressed = () => {
|
||||
// Go to setup account page when "Setup account" is pressed
|
||||
if (Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage) {
|
||||
return this.showPage(Constants.FIRST_RUN_PAGE_EMAIL_COLLECT);
|
||||
}
|
||||
|
||||
// Go to skip account page when "No, thanks" is pressed
|
||||
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) {
|
||||
this.showPage(Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
handleContinuePressed = () => {
|
||||
const { notify } = this.props;
|
||||
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
|
||||
if (this.state.currentPage !== 'email-collect' &&
|
||||
pageIndex === (FirstRunScreen.pages.length - 1)) {
|
||||
if (Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage) {
|
||||
if (!this.state.walletPassword || this.state.walletPassword.trim().length < 6) {
|
||||
return notify({ message: 'Your wallet password should be at least 6 characters long' });
|
||||
}
|
||||
|
||||
this.closeFinalPage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage && !this.state.skipAccountConfirmed) {
|
||||
notify({ message: 'Please confirm that you want to use LBRY without creating an account.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT !== this.state.currentPage && pageIndex === (FirstRunScreen.pages.length - 1)) {
|
||||
this.closeFinalPage();
|
||||
} else {
|
||||
// TODO: Actions and page verification for specific pages
|
||||
if (this.state.currentPage === 'email-collect') {
|
||||
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) {
|
||||
// handle email collect
|
||||
this.handleEmailCollectPageContinue();
|
||||
} else {
|
||||
|
@ -98,21 +130,10 @@ class FirstRunScreen extends React.PureComponent {
|
|||
|
||||
handleEmailCollectPageContinue() {
|
||||
const { notify, addUserEmail } = this.props;
|
||||
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
|
||||
|
||||
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
|
||||
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) {
|
||||
if (!email || email.indexOf('@') === -1) {
|
||||
return notify({
|
||||
message: 'Please provide a valid email address to continue.',
|
||||
});
|
||||
|
@ -133,6 +154,13 @@ class FirstRunScreen extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
showPage(pageName) {
|
||||
const pageIndex = FirstRunScreen.pages.indexOf(pageName);
|
||||
if (pageIndex > -1) {
|
||||
this.setState({ currentPage: pageName });
|
||||
}
|
||||
}
|
||||
|
||||
closeFinalPage() {
|
||||
// Final page. Let the app know that first run experience is completed.
|
||||
if (NativeModules.FirstRun) {
|
||||
|
@ -144,7 +172,7 @@ class FirstRunScreen extends React.PureComponent {
|
|||
}
|
||||
|
||||
onEmailChanged = (email) => {
|
||||
if ('email-collect' == this.state.currentPage) {
|
||||
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT == this.state.currentPage) {
|
||||
this.setState({ showSkip: (!email || email.trim().length === 0) });
|
||||
} else {
|
||||
this.setState({ showSkip: false });
|
||||
|
@ -158,6 +186,18 @@ class FirstRunScreen extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
onWalletPasswordChanged = (password) => {
|
||||
this.setState({ walletPassword: password });
|
||||
}
|
||||
|
||||
onWalletViewLayout = () => {
|
||||
this.setState({ showBottomContainer: true });
|
||||
}
|
||||
|
||||
onSkipSwitchChanged = (checked) => {
|
||||
this.setState({ skipAccountConfirmed: checked });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
authenticate,
|
||||
|
@ -169,15 +209,31 @@ class FirstRunScreen extends React.PureComponent {
|
|||
} = this.props;
|
||||
|
||||
let page = null;
|
||||
if (this.state.currentPage === 'welcome') {
|
||||
// show welcome page
|
||||
page = (<WelcomePage />);
|
||||
} else if (this.state.currentPage === 'email-collect') {
|
||||
page = (<EmailCollectPage authenticating={authenticating}
|
||||
authToken={authToken}
|
||||
authenticate={authenticate}
|
||||
onEmailChanged={this.onEmailChanged}
|
||||
onEmailViewLayout={this.onEmailViewLayout} />);
|
||||
switch (this.state.currentPage) {
|
||||
case 'welcome':
|
||||
page = (<WelcomePage />);
|
||||
break;
|
||||
|
||||
case 'email-collect':
|
||||
page = (<EmailCollectPage
|
||||
authenticating={authenticating}
|
||||
authToken={authToken}
|
||||
authenticate={authenticate}
|
||||
onEmailChanged={this.onEmailChanged}
|
||||
onEmailViewLayout={this.onEmailViewLayout} />);
|
||||
break;
|
||||
|
||||
case 'wallet':
|
||||
page = (<WalletPage
|
||||
onWalletViewLayout={this.onWalletViewLayout}
|
||||
onPasswordChanged={this.onWalletPasswordChanged} />);
|
||||
break;
|
||||
|
||||
case 'skip-account':
|
||||
page = (<SkipAccountPage
|
||||
onSkipAccountViewLayout={this.onSkipAccountViewLayout}
|
||||
onSkipSwitchChanged={this.onSkipSwitchChanged} />);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -188,10 +244,25 @@ class FirstRunScreen extends React.PureComponent {
|
|||
{emailNewPending &&
|
||||
<ActivityIndicator size="small" color={Colors.White} style={firstRunStyle.pageWaiting} />}
|
||||
|
||||
{!emailNewPending &&
|
||||
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
|
||||
<Text style={firstRunStyle.buttonText}>{this.state.showSkip ? 'Skip': 'Continue'}</Text>
|
||||
</TouchableOpacity>}
|
||||
<View style={firstRunStyle.buttonRow}>
|
||||
{([Constants.FIRST_RUN_PAGE_WELCOME, Constants.FIRST_RUN_PAGE_WALLET].indexOf(this.state.currentPage) > -1) && <View />}
|
||||
{Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage &&
|
||||
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
|
||||
<Text style={firstRunStyle.buttonText}>« Setup account</Text>
|
||||
</TouchableOpacity>}
|
||||
{!emailNewPending && (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) &&
|
||||
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
|
||||
<Text style={firstRunStyle.smallLeftButtonText}>No, thanks »</Text>
|
||||
</TouchableOpacity>}
|
||||
|
||||
{!emailNewPending &&
|
||||
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
|
||||
{Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage &&
|
||||
<Text style={firstRunStyle.smallButtonText}>Use LBRY »</Text>}
|
||||
{Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT !== this.state.currentPage &&
|
||||
<Text style={firstRunStyle.buttonText}>{Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage ? 'Use LBRY' : 'Continue'} »</Text>}
|
||||
</TouchableOpacity>}
|
||||
</View>
|
||||
</View>}
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -53,12 +53,13 @@ class SearchPage extends React.PureComponent {
|
|||
<UriBar value={searchQuery}
|
||||
navigation={navigation}
|
||||
onSearchSubmitted={this.handleSearchSubmitted} />
|
||||
{!isSearching && (!uris || uris.length === 0) &&
|
||||
<Text style={searchStyle.noResultsText}>No results to display.</Text>}
|
||||
{isSearching &&
|
||||
<View style={searchStyle.busyContainer}>
|
||||
<ActivityIndicator size="large" color={Colors.LbryGreen} style={searchStyle.loading} />
|
||||
</View>}
|
||||
|
||||
{!isSearching && (!uris || uris.length === 0) &&
|
||||
<Text style={searchStyle.noResultsText}>No results to display.</Text>}
|
||||
{!isSearching &&
|
||||
<ScrollView
|
||||
style={searchStyle.scrollContainer}
|
||||
|
@ -73,7 +74,7 @@ class SearchPage extends React.PureComponent {
|
|||
navigation={navigation}
|
||||
onPress={() => navigateToUri(navigation, this.state.currentUri)}
|
||||
/>}
|
||||
{!isSearching && uris && uris.length ? (
|
||||
{(uris && uris.length) ? (
|
||||
uris.map(uri => <FileListItem key={uri}
|
||||
uri={uri}
|
||||
style={searchStyle.resultItem}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doBalanceSubscribe, doUpdateBlockHeight, doToast } from 'lbry-redux';
|
||||
import { doTotalBalanceSubscribe, doUpdateBlockHeight, doToast } from 'lbry-redux';
|
||||
import {
|
||||
doAuthenticate,
|
||||
doBlackListedOutpointsSubscribe,
|
||||
doCheckSubscriptionsInit,
|
||||
doFetchMySubscriptions,
|
||||
doFetchRewardedContent,
|
||||
doGetSync,
|
||||
doUserEmailToVerify,
|
||||
doUserEmailVerify,
|
||||
doUserEmailVerifyFailure,
|
||||
|
@ -22,12 +23,13 @@ const select = state => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
authenticate: (appVersion, os) => dispatch(doAuthenticate(appVersion, os)),
|
||||
balanceSubscribe: () => dispatch(doBalanceSubscribe()),
|
||||
totalBalanceSubscribe: () => dispatch(doTotalBalanceSubscribe()),
|
||||
blacklistedOutpointsSubscribe: () => dispatch(doBlackListedOutpointsSubscribe()),
|
||||
checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()),
|
||||
deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
fetchSubscriptions: (callback) => dispatch(doFetchMySubscriptions(callback)),
|
||||
getSync: password => dispatch(doGetSync(password)),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
|
||||
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { NavigationActions, StackActions } from 'react-navigation';
|
||||
import { decode as atob } from 'base-64';
|
||||
import { navigateToUri } from '../../utils/helper';
|
||||
import { navigateToUri } from 'utils/helper';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import PropTypes from 'prop-types';
|
||||
import Colors from 'styles/colors';
|
||||
|
@ -120,13 +120,13 @@ class SplashScreen extends React.PureComponent {
|
|||
}
|
||||
|
||||
// user is authenticated, navigate to the main view
|
||||
/*if (user.has_verified_email) {
|
||||
if (user.has_verified_email) {
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||
getSync(walletPassword);
|
||||
this.navigateToMain();
|
||||
});
|
||||
return;
|
||||
}*/
|
||||
}
|
||||
|
||||
this.navigateToMain();
|
||||
});
|
||||
|
@ -134,6 +134,31 @@ class SplashScreen extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
finishSplashScreen = () => {
|
||||
const {
|
||||
authenticate,
|
||||
totalBalanceSubscribe,
|
||||
blacklistedOutpointsSubscribe,
|
||||
checkSubscriptionsInit,
|
||||
updateBlockHeight,
|
||||
navigation,
|
||||
notify
|
||||
} = this.props;
|
||||
|
||||
Lbry.resolve({ urls: 'lbry://one' }).then(() => {
|
||||
// Leave the splash screen
|
||||
totalBalanceSubscribe();
|
||||
blacklistedOutpointsSubscribe();
|
||||
checkSubscriptionsInit();
|
||||
updateBlockHeight();
|
||||
setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL);
|
||||
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||
this.setState({ shouldAuthenticate: true });
|
||||
authenticate(appVersion, Platform.OS);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_updateStatusCallback(status) {
|
||||
const { deleteCompleteBlobs, fetchSubscriptions } = this.props;
|
||||
const startupStatus = status.startup_status;
|
||||
|
@ -154,30 +179,37 @@ class SplashScreen extends React.PureComponent {
|
|||
isRunning: true,
|
||||
});
|
||||
|
||||
// fetch subscriptions, so that we can check for new content after resolve
|
||||
Lbry.resolve({ urls: 'lbry://one' }).then(() => {
|
||||
// Leave the splash screen
|
||||
const {
|
||||
authenticate,
|
||||
balanceSubscribe,
|
||||
blacklistedOutpointsSubscribe,
|
||||
checkSubscriptionsInit,
|
||||
updateBlockHeight,
|
||||
navigation,
|
||||
notify
|
||||
} = this.props;
|
||||
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_PASSWORD).then(passwordSet => {
|
||||
if ("true" === passwordSet) {
|
||||
// encrypt the wallet
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
|
||||
if (!password || password.trim().length === 0) {
|
||||
this.finishSplashScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
balanceSubscribe();
|
||||
blacklistedOutpointsSubscribe();
|
||||
checkSubscriptionsInit();
|
||||
updateBlockHeight();
|
||||
setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL);
|
||||
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||
this.setState({ shouldAuthenticate: true });
|
||||
authenticate(appVersion, Platform.OS);
|
||||
Lbry.account_encrypt({ new_password: password }).then((result) => {
|
||||
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_PASSWORD);
|
||||
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, automatically unlock the wallet if a password is set so that downloads work
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
|
||||
if (password && password.trim().length > 0) {
|
||||
// unlock the wallet and then finish the splash screen
|
||||
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
|
||||
return;
|
||||
}
|
||||
|
||||
this.finishSplashScreen();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,17 @@ import { connect } from 'react-redux';
|
|||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doGetSync, selectUser } from 'lbryinc';
|
||||
import Constants from 'constants';
|
||||
import WalletPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
user: selectUser(state),
|
||||
understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
getSync: password => dispatch(doGetSync(password)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_WALLET))
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { ScrollView, Text, View } from 'react-native';
|
||||
import { NativeModules, ScrollView, Text, View } from 'react-native';
|
||||
import TransactionListRecent from 'component/transactionListRecent';
|
||||
import WalletAddress from 'component/walletAddress';
|
||||
import WalletBalance from 'component/walletBalance';
|
||||
|
@ -13,6 +13,11 @@ import walletStyle from 'styles/wallet';
|
|||
class WalletPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.pushDrawerStack();
|
||||
|
||||
const { user, getSync } = this.props;
|
||||
if (user && user.has_verified_email) {
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => getSync(walletPassword));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -2,6 +2,11 @@ import { StyleSheet } from 'react-native';
|
|||
import Colors from './colors';
|
||||
|
||||
const firstRunStyle = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 32,
|
||||
marginRight: 32,
|
||||
},
|
||||
screenContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.LbryGreen
|
||||
|
@ -37,6 +42,21 @@ const firstRunStyle = StyleSheet.create({
|
|||
marginBottom: 20,
|
||||
color: Colors.White
|
||||
},
|
||||
confirmContainer: {
|
||||
marginTop: 36
|
||||
},
|
||||
rowParagraph: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
color: Colors.White,
|
||||
flex: 0.7
|
||||
},
|
||||
rowSwitch: {
|
||||
justifyContent: 'flex-start',
|
||||
flex: 0.2,
|
||||
marginRight: 8
|
||||
},
|
||||
emailInput: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 24,
|
||||
|
@ -46,15 +66,30 @@ const firstRunStyle = StyleSheet.create({
|
|||
marginBottom: 20,
|
||||
textAlign: 'center'
|
||||
},
|
||||
passwordInput: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 24,
|
||||
lineHeight: 24,
|
||||
marginLeft: 32,
|
||||
marginRight: 32,
|
||||
marginBottom: 20,
|
||||
textAlign: 'center'
|
||||
},
|
||||
leftButton: {
|
||||
flex: 1,
|
||||
alignSelf: 'flex-start',
|
||||
alignSelf: 'flex-end',
|
||||
paddingBottom: 16,
|
||||
marginLeft: 32,
|
||||
marginRight: 32
|
||||
},
|
||||
bottomContainer: {
|
||||
flex: 1
|
||||
},
|
||||
buttonRow: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
actionButton: {
|
||||
backgroundColor: Colors.White,
|
||||
alignSelf: 'center',
|
||||
|
@ -71,14 +106,29 @@ const firstRunStyle = StyleSheet.create({
|
|||
},
|
||||
buttonText: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 24,
|
||||
fontSize: 18,
|
||||
color: Colors.White
|
||||
},
|
||||
smallButtonText: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
color: Colors.White,
|
||||
marginBottom: -2
|
||||
},
|
||||
smallLeftButtonText: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
color: Colors.White,
|
||||
marginBottom: 6
|
||||
},
|
||||
waiting: {
|
||||
marginBottom: 24
|
||||
},
|
||||
pageWaiting: {
|
||||
alignSelf: 'center'
|
||||
},
|
||||
titleIcon: {
|
||||
marginTop: 8
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ const uriBarStyle = StyleSheet.create({
|
|||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
backgroundColor: '#cc0000',
|
||||
backgroundColor: 'transparent',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
zIndex: 200,
|
||||
|
|
|
@ -124,6 +124,35 @@ public final class Utils {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static void setSecureValue(String key, String value, Context context, KeyStore keyStore) {
|
||||
try {
|
||||
String encryptedValue = encrypt(value.getBytes(), context, keyStore);
|
||||
SharedPreferences pref = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
editor.putString(key, encryptedValue);
|
||||
editor.commit();
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "utils - Could not set a secure value", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getSecureValue(String key, Context context, KeyStore keyStore) {
|
||||
try {
|
||||
SharedPreferences pref = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
String encryptedValue = pref.getString(key, null);
|
||||
if (encryptedValue == null || encryptedValue.trim().length() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] decoded = Base64.decode(encryptedValue, Base64.DEFAULT);
|
||||
return new String(decrypt(decoded, context, keyStore), Charset.forName("UTF8"));
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "utils - Could not retrieve a secure value", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void setPassword(String serviceName, String username, String password, Context context, KeyStore keyStore) {
|
||||
try {
|
||||
String encryptedUsername = String.format("u_%s_%s", serviceName, encrypt(username.getBytes(), context, keyStore));
|
||||
|
|
|
@ -33,6 +33,7 @@ import java.io.IOException;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import io.lbry.browser.MainActivity;
|
||||
import io.lbry.browser.R;
|
||||
|
@ -52,9 +53,16 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
|||
|
||||
private Context context;
|
||||
|
||||
private KeyStore keyStore;
|
||||
|
||||
public UtilityModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.context = reactContext;
|
||||
try {
|
||||
this.keyStore = Utils.initKeyStore(context);
|
||||
} catch (Exception ex) {
|
||||
// continue without keystore
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -300,4 +308,21 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
|||
(Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|
||||
);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setSecureValue(String key, String value) {
|
||||
if (keyStore != null) {
|
||||
Utils.setSecureValue(key, value, context, keyStore);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getSecureValue(String key, Promise promise) {
|
||||
if (keyStore == null) {
|
||||
promise.reject("no keyStore found");
|
||||
return;
|
||||
}
|
||||
|
||||
promise.resolve(Utils.getSecureValue(key, context, keyStore));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue