complete authentication flow and email verification link implementation

This commit is contained in:
Akinwale Ariwodola 2018-08-15 16:53:07 +01:00
parent 0db7e51d69
commit 77a6266792
14 changed files with 391 additions and 57 deletions

8
app/package-lock.json generated
View file

@ -3965,6 +3965,14 @@
"reselect": "^3.0.0"
}
},
"lbryinc": {
"version": "github:lbryio/lbryinc#8e33473daa56ebe80b12509ac5374c5884efef90",
"from": "github:lbryio/lbryinc#authentication-flow",
"requires": {
"lbry-redux": "github:lbryio/lbry-redux#467e48c77b8004cef738e950bdcc67654748ae9f",
"reselect": "^3.0.0"
}
},
"lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",

View file

@ -6,7 +6,9 @@
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"base-64": "^0.1.0",
"lbry-redux": "lbryio/lbry-redux",
"lbryinc": "lbryio/lbryinc#authentication-flow",
"moment": "^2.22.1",
"react": "16.2.0",
"react-native": "0.55.3",

View file

@ -27,8 +27,17 @@ import {
TextInput,
ToastAndroid
} from 'react-native';
import { SETTINGS, doHideNotification, selectNotification } from 'lbry-redux';
import { SETTINGS, doHideNotification, doNotify, selectNotification } from 'lbry-redux';
import {
doUserEmailVerify,
doUserEmailVerifyFailure,
selectEmailToVerify,
selectEmailVerifyIsPending,
selectEmailVerifyErrorMessage,
selectUser
} from 'lbryinc';
import { makeSelectClientSetting } from '../redux/selectors/settings';
import { decode as atob } from 'base-64';
import Icon from 'react-native-vector-icons/FontAwesome5';
import discoverStyle from '../styles/discover';
import searchStyle from '../styles/search';
@ -122,6 +131,13 @@ export const AppNavigator = new StackNavigator({
class AppWithNavigationState extends React.Component {
static supportedDisplayTypes = ['toast'];
constructor() {
super();
this.state = {
emailVerifyDone: false
};
}
componentWillMount() {
AppState.addEventListener('change', this._handleAppStateChange);
BackHandler.addEventListener('hardwareBackPress', function() {
@ -144,7 +160,7 @@ class AppWithNavigationState extends React.Component {
}
}
}
return false;fo
return false;
}.bind(this));
}
@ -160,7 +176,14 @@ class AppWithNavigationState extends React.Component {
componentWillUpdate(nextProps) {
const { dispatch } = this.props;
const { notification } = nextProps;
const {
notification,
emailToVerify,
emailVerifyPending,
emailVerifyErrorMessage,
user
} = nextProps;
if (notification) {
const { displayType, message } = notification;
let currentDisplayType;
@ -182,6 +205,16 @@ class AppWithNavigationState extends React.Component {
dispatch(doHideNotification());
}
if (user &&
!emailVerifyPending &&
!this.state.emailVerifyDone &&
(emailToVerify || emailVerifyErrorMessage)) {
this.setState({ emailVerifyDone: true });
const message = emailVerifyErrorMessage ?
String(emailVerifyErrorMessage) : 'Your email address was successfully verified.';
dispatch(doNotify({ message, displayType: ['toast'] }));
}
}
_handleAppStateChange = (nextAppState) => {
@ -200,12 +233,37 @@ class AppWithNavigationState extends React.Component {
_handleUrl = (evt) => {
const { dispatch } = this.props;
if (evt.url) {
const navigateAction = NavigationActions.navigate({
routeName: 'File',
key: evt.url,
params: { uri: evt.url }
});
dispatch(navigateAction);
if (evt.url.startsWith('lbry://?verify=')) {
this.setState({ emailVerifyDone: false });
let verification = {};
try {
verification = JSON.parse(atob(evt.url.substring(15)));
} catch (error) {
console.log(error);
}
if (verification.token && verification.recaptcha) {
try {
dispatch(doUserEmailVerify(verification.token, verification.recaptcha));
} catch (error) {
const message = 'Invalid Verification Token';
dispatch(doUserEmailVerifyFailure(message));
dispatch(doNotify({ message, displayType: ['toast'] }));
}
} else {
dispatch(doNotify({
message: 'Invalid Verification URI',
displayType: ['toast'],
}));
}
} else {
const navigateAction = NavigationActions.navigate({
routeName: 'File',
key: evt.url,
params: { uri: evt.url }
});
dispatch(navigateAction);
}
}
}
@ -224,10 +282,14 @@ class AppWithNavigationState extends React.Component {
}
const mapStateToProps = state => ({
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
nav: state.nav,
notification: selectNotification(state),
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state)
emailToVerify: selectEmailToVerify(state),
emailVerifyPending: selectEmailVerifyIsPending(state),
emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),
user: selectUser(state),
});
export default connect(mapStateToProps)(AppWithNavigationState);

View file

@ -1,5 +1,9 @@
const Constants = {
SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS"
KEY_FIRST_RUN_EMAIL: "firstRunEmail",
SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS",
ACTION_FIRST_RUN_PAGE_CHANGED: "FIRST_RUN_PAGE_CHANGED",
};
export default Constants;

View file

@ -17,6 +17,7 @@ import {
searchReducer,
walletReducer
} from 'lbry-redux';
import { authReducer, userReducer } from 'lbryinc';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import { createLogger } from 'redux-logger';
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
@ -68,6 +69,7 @@ const navigatorReducer = (state = initialNavState, action) => {
};
const reducers = combineReducers({
auth: authReducer,
claims: claimsReducer,
costInfo: costInfoReducer,
fileInfo: fileInfoReducer,
@ -75,7 +77,8 @@ const reducers = combineReducers({
search: searchReducer,
wallet: walletReducer,
nav: navigatorReducer,
settings: settingsReducer
settings: settingsReducer,
user: userReducer
});
const bulkThunk = createBulkThunkMiddleware();
@ -93,18 +96,20 @@ const store = createStore(
applyMiddleware(...middleware)
)
);
window.store = store;
const compressor = createCompressor();
const authFilter = createFilter('auth', ['authToken']);
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
const settingsFilter = createFilter('settings', ['clientSettings']);
const walletFilter = createFilter('wallet', ['receiveAddress']);
const persistOptions = {
whitelist: ['claims', 'subscriptions', 'settings', 'wallet'],
whitelist: ['auth', 'claims', 'subscriptions', 'settings', 'wallet'],
// Order is important. Needs to be compressed last or other transforms can't
// read the data
transforms: [saveClaimsFilter, subscriptionsFilter, settingsFilter, walletFilter, compressor],
transforms: [authFilter, saveClaimsFilter, subscriptionsFilter, settingsFilter, walletFilter, compressor],
debounce: 10000,
storage: FilesystemStorage
};

View file

@ -24,7 +24,6 @@ class AboutPage extends React.PureComponent {
});
});
Lbry.status().then(info => {
console.log(info);
this.setState({
lbryId: info.installation_id,
});

View file

@ -1,6 +1,32 @@
import { connect } from 'react-redux';
import { doNotify } from 'lbry-redux';
import {
doGenerateAuthToken,
doUserEmailNew,
selectAuthToken,
selectEmailNewErrorMessage,
selectEmailNewIsPending,
selectEmailToVerify,
selectEmailVerifyErrorMessage,
selectEmailVerifyIsPending,
selectIsAuthenticating
} from 'lbryinc';
import FirstRun from './view';
const perform = dispatch => ({});
const select = (state) => ({
authenticating: selectIsAuthenticating(state),
authToken: selectAuthToken(state),
emailToVerify: selectEmailToVerify(state),
emailNewErrorMessage: selectEmailNewErrorMessage(state),
emailNewPending: selectEmailNewIsPending(state),
emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
emailVerifyPending: selectEmailVerifyIsPending(state)
});
export default connect(null, perform)(FirstRun);
const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)),
generateAuthToken: installationId => dispatch(doGenerateAuthToken(installationId)),
notify: data => dispatch(doNotify(data))
});
export default connect(select, perform)(FirstRun);

View file

@ -0,0 +1,83 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
AsyncStorage,
Linking,
Text,
TextInput,
View
} from 'react-native';
import Colors from '../../../styles/colors';
import firstRunStyle from '../../../styles/firstRun';
class EmailCollectPage extends React.PureComponent {
constructor() {
super();
this.state = {
email: null
};
}
componentDidMount() {
// call user/new
const { generateAuthToken, authenticating, authToken } = this.props;
if (!authToken && !authenticating) {
Lbry.status().then(info => {
generateAuthToken(info.installation_id)
});
}
AsyncStorage.getItem('firstRunEmail').then(email => {
if (email) {
this.setState({ email });
}
});
}
handleChangeText = (text) => {
// save the value to the state email
this.setState({ email: text });
AsyncStorage.setItem('firstRunEmail', text);
}
render() {
let authenticationFailed = false;
const { authenticating, authToken, onEmailViewLayout, emailToVerify } = this.props;
let content;
if (!authToken || authenticating) {
content = (
<View>
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
<Text style={firstRunStyle.paragraph}>Please wait while we get some things ready...</Text>
</View>
)
} else if (authenticationFailed) {
// Ask the user to try again
} 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>
<TextInput style={firstRunStyle.emailInput}
placeholder="you@example.com"
underlineColorAndroid="transparent"
value={this.state.email}
onChangeText={text => this.handleChangeText(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>
)
}
return (
<View style={firstRunStyle.container}>
{content}
</View>
);
}
}
export default EmailCollectPage;

View file

@ -1,6 +1,8 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
AsyncStorage,
Linking,
NativeModules,
Text,
@ -9,19 +11,26 @@ import {
} from 'react-native';
import { NavigationActions } from 'react-navigation';
import Colors from '../../styles/colors';
import Constants from '../../constants';
import WelcomePage from './internal/welcome-page';
import EmailCollectPage from './internal/email-collect-page';
//import EmailVerifyPage from '../internal/email-verify-page';
import firstRunStyle from '../../styles/firstRun';
class FirstRunScreen extends React.PureComponent {
static pages = ['welcome'];
static pages = [
'welcome',
'email-collect'
];
constructor() {
super();
this.state = {
currentPage: null,
launchUrl: null,
isFirstRun: false
}
isFirstRun: false,
showBottomContainer: true
};
}
componentDidMount() {
@ -60,34 +69,92 @@ class FirstRunScreen extends React.PureComponent {
handleContinuePressed = () => {
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
if (pageIndex === (FirstRunScreen.pages.length - 1)) {
// Final page. Let the app know that first run experience is completed.
if (NativeModules.FirstRun) {
NativeModules.FirstRun.firstRunCompleted();
}
// Navigate to the splash screen
this.launchSplashScreen();
if (this.state.currentPage !== 'email-collect' &&
pageIndex === (FirstRunScreen.pages.length - 1)) {
this.closeFinalPage();
} else {
// TODO: Page transition animation?
this.state.currentPage = FirstRunScreen.pages[pageIndex + 1];
// TODO: Actions and page verification for specific pages
if (this.state.currentPage === 'email-collect') {
// handle email collect
this.handleEmailCollectPageContinue();
} else {
this.showNextPage();
}
}
}
handleEmailCollectPageContinue() {
const { notify, addUserEmail } = this.props;
// validate the email
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
if (!email || email.trim().length === 0 || email.indexOf('@') === -1) {
return notify({
message: 'Please provide a valid email address to continue.',
displayType: ['toast'],
});
}
addUserEmail(email);
// treat as the final page
this.closeFinalPage();
});
}
showNextPage() {
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
const nextPage = FirstRunScreen.pages[pageIndex + 1];
this.setState({ currentPage: nextPage });
if (nextPage === 'email-collect') {
// do not show the buttons (because we're waiting to get things ready)
this.setState({ showBottomContainer: false });
}
}
closeFinalPage() {
// Final page. Let the app know that first run experience is completed.
if (NativeModules.FirstRun) {
NativeModules.FirstRun.firstRunCompleted();
}
// Navigate to the splash screen
this.launchSplashScreen();
}
render() {
const {
authenticating,
authToken,
generateAuthToken,
emailNewErrorMessage,
emailNewPending,
emailToVerify
} = 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}
generateAuthToken={generateAuthToken}
onEmailViewLayout={() => this.setState({ showBottomContainer: true })} />);
}
return (
<View style={firstRunStyle.screenContainer}>
{page}
{this.state.currentPage &&
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
<Text style={firstRunStyle.buttonText}>Continue</Text>
</TouchableOpacity>}
{this.state.currentPage && this.state.showBottomContainer &&
<View style={firstRunStyle.bottomContainer}>
{emailNewPending &&
<ActivityIndicator size="small" color={Colors.White} style={firstRunStyle.pageWaiting} />}
{!emailNewPending &&
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
<Text style={firstRunStyle.buttonText}>Continue</Text>
</TouchableOpacity>}
</View>}
</View>
)
}

View file

@ -1,9 +1,18 @@
import { connect } from 'react-redux';
import { doBalanceSubscribe } from 'lbry-redux';
import { doBalanceSubscribe, doNotify } from 'lbry-redux';
import { doAuthenticate, doUserEmailVerify, doUserEmailVerifyFailure, selectUser } from 'lbryinc';
import SplashScreen from './view';
const perform = dispatch => ({
balanceSubscribe: () => dispatch(doBalanceSubscribe())
const select = state => ({
user: selectUser(state),
});
export default connect(null, perform)(SplashScreen);
const perform = dispatch => ({
authenticate: (appVersion, deviceId) => dispatch(doAuthenticate(appVersion, deviceId)),
balanceSubscribe: () => dispatch(doBalanceSubscribe()),
notify: data => dispatch(doNotify(data)),
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
});
export default connect(select, perform)(SplashScreen);

View file

@ -10,6 +10,7 @@ import {
View
} from 'react-native';
import { NavigationActions } from 'react-navigation';
import { decode as atob } from 'base-64';
import PropTypes from 'prop-types';
import Colors from '../../styles/colors';
import splashStyle from '../../styles/splash';
@ -52,6 +53,49 @@ class SplashScreen extends React.PureComponent {
});
}
componentWillUpdate(nextProps) {
const { navigation, verifyUserEmail, verifyUserEmailFailure } = this.props;
const { user } = nextProps;
if (user && user.id) {
// user is authenticated, navigate to the main view
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Main'})
]
});
navigation.dispatch(resetAction);
const launchUrl = navigation.state.params.launchUrl || this.state.launchUrl;
if (launchUrl) {
if (launchUrl.startsWith('lbry://?verify=')) {
let verification = {};
try {
verification = JSON.parse(atob(launchUrl.substring(15)));
} catch (error) {
console.log(error);
}
if (verification.token && verification.recaptcha) {
try {
verifyUserEmail(verification.token, verification.recaptcha);
} catch (error) {
const message = 'Invalid Verification Token';
verifyUserEmailFailure(message);
notify({ message, displayType: ['toast'] });
}
} else {
notify({
message: 'Invalid Verification URI',
displayType: ['toast'],
});
}
} else {
navigation.navigate({ routeName: 'File', key: launchUrl, params: { uri: launchUrl } });
}
}
}
}
_updateStatusCallback(status) {
const startupStatus = status.startup_status;
// At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve
@ -70,21 +114,15 @@ class SplashScreen extends React.PureComponent {
Lbry.resolve({ uri: 'lbry://one' }).then(() => {
// Leave the splash screen
const { balanceSubscribe, navigation } = this.props;
const {
authenticate,
balanceSubscribe,
navigation,
notify
} = this.props;
authenticate(null);
balanceSubscribe();
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Main'})
]
});
navigation.dispatch(resetAction);
const launchUrl = navigation.state.params.launchUrl || this.state.launchUrl;
if (launchUrl) {
navigation.navigate({ routeName: 'File', key: launchUrl, params: { uri: launchUrl } });
}
});
return;
}

View file

@ -9,3 +9,4 @@ export function doSetClientSetting(key, value) {
},
};
}

View file

@ -1,7 +1,5 @@
import { AsyncStorage } from 'react-native';
import { ACTIONS } from 'lbry-redux';
const reducers = {};
const defaultState = {
clientSettings: {}

View file

@ -28,16 +28,48 @@ const firstRunStyle = StyleSheet.create({
marginBottom: 20,
color: Colors.White
},
button: {
infoParagraph: {
fontFamily: 'Metropolis-Regular',
fontSize: 14,
lineHeight: 20,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
color: Colors.White
},
emailInput: {
fontFamily: 'Metropolis-Regular',
fontSize: 24,
lineHeight: 24,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
textAlign: 'center'
},
leftButton: {
flex: 1,
alignSelf: 'flex-start',
marginLeft: 32,
marginRight: 32
},
bottomContainer: {
flex: 1
},
button: {
alignSelf: 'flex-end',
marginLeft: 32,
marginRight: 32
},
buttonText: {
fontFamily: 'Metropolis-Regular',
fontSize: 28,
fontSize: 24,
color: Colors.White
},
waiting: {
marginBottom: 24
},
pageWaiting: {
alignSelf: 'center'
}
});