From 77a62667920d436cc697699e0504e0943af790d7 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Wed, 15 Aug 2018 16:53:07 +0100 Subject: [PATCH 1/9] complete authentication flow and email verification link implementation --- app/package-lock.json | 8 ++ app/package.json | 2 + app/src/component/AppNavigator.js | 84 +++++++++++++-- app/src/constants.js | 6 +- app/src/index.js | 11 +- app/src/page/about/view.js | 1 - app/src/page/firstRun/index.js | 30 +++++- .../firstRun/internal/email-collect-page.js | 83 ++++++++++++++ app/src/page/firstRun/view.js | 101 +++++++++++++++--- app/src/page/splash/index.js | 17 ++- app/src/page/splash/view.js | 66 +++++++++--- app/src/redux/actions/settings.js | 1 + app/src/redux/reducers/settings.js | 2 - app/src/styles/firstRun.js | 36 ++++++- 14 files changed, 391 insertions(+), 57 deletions(-) create mode 100644 app/src/page/firstRun/internal/email-collect-page.js diff --git a/app/package-lock.json b/app/package-lock.json index 61801120..f81829a1 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -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", diff --git a/app/package.json b/app/package.json index 16f2b6b2..45169703 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/component/AppNavigator.js b/app/src/component/AppNavigator.js index 735cbe3c..60d401c1 100644 --- a/app/src/component/AppNavigator.js +++ b/app/src/component/AppNavigator.js @@ -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); diff --git a/app/src/constants.js b/app/src/constants.js index a0c3d277..6cc5685f 100644 --- a/app/src/constants.js +++ b/app/src/constants.js @@ -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; diff --git a/app/src/index.js b/app/src/index.js index d3939553..fa0ee2c8 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -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 }; diff --git a/app/src/page/about/view.js b/app/src/page/about/view.js index b2196ca2..7766f2fd 100644 --- a/app/src/page/about/view.js +++ b/app/src/page/about/view.js @@ -24,7 +24,6 @@ class AboutPage extends React.PureComponent { }); }); Lbry.status().then(info => { - console.log(info); this.setState({ lbryId: info.installation_id, }); diff --git a/app/src/page/firstRun/index.js b/app/src/page/firstRun/index.js index 5e9ec0c1..05937d02 100644 --- a/app/src/page/firstRun/index.js +++ b/app/src/page/firstRun/index.js @@ -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); \ No newline at end of file +const perform = dispatch => ({ + addUserEmail: email => dispatch(doUserEmailNew(email)), + generateAuthToken: installationId => dispatch(doGenerateAuthToken(installationId)), + notify: data => dispatch(doNotify(data)) +}); + +export default connect(select, perform)(FirstRun); \ No newline at end of file diff --git a/app/src/page/firstRun/internal/email-collect-page.js b/app/src/page/firstRun/internal/email-collect-page.js new file mode 100644 index 00000000..a96fdf55 --- /dev/null +++ b/app/src/page/firstRun/internal/email-collect-page.js @@ -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 = ( + + + Please wait while we get some things ready... + + ) + } else if (authenticationFailed) { + // Ask the user to try again + } else { + content = ( + + Rewards. + You can earn LBRY Credits (LBC) rewards by completing various tasks in the app. + Please provide a valid email address below to be able to claim your rewards. + this.handleChangeText(text)} + /> + 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. + + ) + } + + return ( + + {content} + + ); + } +} + +export default EmailCollectPage; diff --git a/app/src/page/firstRun/view.js b/app/src/page/firstRun/view.js index 2e868376..6917ef9f 100644 --- a/app/src/page/firstRun/view.js +++ b/app/src/page/firstRun/view.js @@ -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 = (); + } else if (this.state.currentPage === 'email-collect') { + page = ( this.setState({ showBottomContainer: true })} />); } return ( {page} - {this.state.currentPage && - - Continue - } + {this.state.currentPage && this.state.showBottomContainer && + + {emailNewPending && + } + + {!emailNewPending && + + Continue + } + } ) } diff --git a/app/src/page/splash/index.js b/app/src/page/splash/index.js index d50dc5a0..f1818532 100644 --- a/app/src/page/splash/index.js +++ b/app/src/page/splash/index.js @@ -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); \ No newline at end of file +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); diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js index 45173cfa..f2f72548 100644 --- a/app/src/page/splash/view.js +++ b/app/src/page/splash/view.js @@ -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; } diff --git a/app/src/redux/actions/settings.js b/app/src/redux/actions/settings.js index aa9f9e61..9a80bc77 100644 --- a/app/src/redux/actions/settings.js +++ b/app/src/redux/actions/settings.js @@ -9,3 +9,4 @@ export function doSetClientSetting(key, value) { }, }; } + diff --git a/app/src/redux/reducers/settings.js b/app/src/redux/reducers/settings.js index bab57f18..1e6f70f1 100644 --- a/app/src/redux/reducers/settings.js +++ b/app/src/redux/reducers/settings.js @@ -1,7 +1,5 @@ -import { AsyncStorage } from 'react-native'; import { ACTIONS } from 'lbry-redux'; - const reducers = {}; const defaultState = { clientSettings: {} diff --git a/app/src/styles/firstRun.js b/app/src/styles/firstRun.js index c5d3f4c6..b4f14b12 100644 --- a/app/src/styles/firstRun.js +++ b/app/src/styles/firstRun.js @@ -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' } }); -- 2.45.2 From 3138d04382f1453eff2891811bc6612bd6da0352 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 07:06:09 +0100 Subject: [PATCH 2/9] some changes to improve the email collection experience --- app/package-lock.json | 11 ++++- app/src/page/firstRun/index.js | 4 -- .../firstRun/internal/email-collect-page.js | 45 ++++++++++++++----- app/src/page/firstRun/view.js | 23 +++++++--- app/src/styles/firstRun.js | 8 ++++ 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index f81829a1..5b17c504 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -3969,8 +3969,17 @@ "version": "github:lbryio/lbryinc#8e33473daa56ebe80b12509ac5374c5884efef90", "from": "github:lbryio/lbryinc#authentication-flow", "requires": { - "lbry-redux": "github:lbryio/lbry-redux#467e48c77b8004cef738e950bdcc67654748ae9f", "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": { diff --git a/app/src/page/firstRun/index.js b/app/src/page/firstRun/index.js index 05937d02..14ad9603 100644 --- a/app/src/page/firstRun/index.js +++ b/app/src/page/firstRun/index.js @@ -7,8 +7,6 @@ import { selectEmailNewErrorMessage, selectEmailNewIsPending, selectEmailToVerify, - selectEmailVerifyErrorMessage, - selectEmailVerifyIsPending, selectIsAuthenticating } from 'lbryinc'; import FirstRun from './view'; @@ -19,8 +17,6 @@ const select = (state) => ({ emailToVerify: selectEmailToVerify(state), emailNewErrorMessage: selectEmailNewErrorMessage(state), emailNewPending: selectEmailNewIsPending(state), - emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state), - emailVerifyPending: selectEmailVerifyIsPending(state) }); const perform = dispatch => ({ diff --git a/app/src/page/firstRun/internal/email-collect-page.js b/app/src/page/firstRun/internal/email-collect-page.js index a96fdf55..58d6c4cc 100644 --- a/app/src/page/firstRun/internal/email-collect-page.js +++ b/app/src/page/firstRun/internal/email-collect-page.js @@ -8,6 +8,7 @@ import { TextInput, View } from 'react-native'; +import Button from '../../../component/button'; import Colors from '../../../styles/colors'; import firstRunStyle from '../../../styles/firstRun'; @@ -15,17 +16,25 @@ class EmailCollectPage extends React.PureComponent { constructor() { super(); this.state = { - email: null + email: null, + authenticationStarted: false, + authenticationFailed: false }; } + componentWillReceiveProps(nextProps) { + const { authenticating, authToken } = this.props; + + if (this.state.authenticationStarted && !authenticating && authToken === null) { + this.setState({ authenticationFailed: true, authenticationStarted: false }); + } + } + componentDidMount() { // call user/new const { generateAuthToken, authenticating, authToken } = this.props; if (!authToken && !authenticating) { - Lbry.status().then(info => { - generateAuthToken(info.installation_id) - }); + this.startAuthenticating(true); } AsyncStorage.getItem('firstRunEmail').then(email => { @@ -35,6 +44,18 @@ class EmailCollectPage extends React.PureComponent { }); } + startAuthenticating = (useTimeout) => { + const { generateAuthToken } = this.props; + this.setState({ authenticationStarted: true, authenticationFailed: false }); + setTimeout(() => { + Lbry.status().then(info => { + generateAuthToken(info.installation_id) + }).catch(error => { + this.setState({ authenticationFailed: true }); + }); + }, useTimeout ? 5000 : 0); // if useTimeout is set, wait 5s to give the daemon some time to start + } + handleChangeText = (text) => { // save the value to the state email this.setState({ email: text }); @@ -42,19 +63,23 @@ class EmailCollectPage extends React.PureComponent { } render() { - let authenticationFailed = false; const { authenticating, authToken, onEmailViewLayout, emailToVerify } = this.props; let content; - if (!authToken || authenticating) { + if (this.state.authenticationFailed) { + // Ask the user to try again + content = ( + + The LBRY servers were unreachable at this time. Please check your Internet connection and then restart the app to try again. + + ); + } else if (!authToken || authenticating) { content = ( Please wait while we get some things ready... - ) - } else if (authenticationFailed) { - // Ask the user to try again + ); } else { content = ( @@ -69,7 +94,7 @@ class EmailCollectPage extends React.PureComponent { /> 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. - ) + ); } return ( diff --git a/app/src/page/firstRun/view.js b/app/src/page/firstRun/view.js index 6917ef9f..0a43ca8a 100644 --- a/app/src/page/firstRun/view.js +++ b/app/src/page/firstRun/view.js @@ -14,7 +14,6 @@ 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 { @@ -27,8 +26,9 @@ class FirstRunScreen extends React.PureComponent { super(); this.state = { currentPage: null, - launchUrl: null, + emailSubmitted: false, isFirstRun: false, + launchUrl: null, showBottomContainer: true }; } @@ -56,6 +56,21 @@ class FirstRunScreen extends React.PureComponent { } } + componentWillReceiveProps(nextProps) { + const { emailNewErrorMessage, emailNewPending } = nextProps; + const { notify } = this.props; + + if (this.state.emailSubmitted && !emailNewPending) { + this.setState({ emailSubmitted: false }); + if (emailNewErrorMessage) { + notify ({ message: String(emailNewErrorMessage), displayType: ['toast']}); + } else { + // Request successful. Navigate to discover. + this.closeFinalPage(); + } + } + } + launchSplashScreen() { const { navigation } = this.props; const resetAction = NavigationActions.reset({ @@ -95,9 +110,7 @@ class FirstRunScreen extends React.PureComponent { } addUserEmail(email); - - // treat as the final page - this.closeFinalPage(); + this.setState({ emailSubmitted: true }); }); } diff --git a/app/src/styles/firstRun.js b/app/src/styles/firstRun.js index b4f14b12..562e2268 100644 --- a/app/src/styles/firstRun.js +++ b/app/src/styles/firstRun.js @@ -55,6 +55,14 @@ const firstRunStyle = StyleSheet.create({ bottomContainer: { flex: 1 }, + actionButton: { + backgroundColor: Colors.White, + alignSelf: 'center', + fontFamily: 'Metropolis-Regular', + fontSize: 12, + paddingLeft: 16, + paddingRight: 16 + }, button: { alignSelf: 'flex-end', marginLeft: 32, -- 2.45.2 From 1edc112cad946435cb76abf645a0d0b7cc48177d Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 07:17:36 +0100 Subject: [PATCH 3/9] missing semicolon --- app/src/page/firstRun/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/page/firstRun/view.js b/app/src/page/firstRun/view.js index 0a43ca8a..e3d24d64 100644 --- a/app/src/page/firstRun/view.js +++ b/app/src/page/firstRun/view.js @@ -169,7 +169,7 @@ class FirstRunScreen extends React.PureComponent { } } - ) + ); } } -- 2.45.2 From 8ee06970562feee1b0648794a55113d8b5c748fc Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 09:09:44 +0100 Subject: [PATCH 4/9] send appVersion and deviceId with authentication request --- .../firstRun/internal/email-collect-page.js | 2 +- app/src/page/splash/view.js | 19 ++++++++++++++++--- .../java/io/lbry/browser/MainActivity.java | 17 ++++------------- .../browser/reactmodules/UtilityModule.java | 11 +++++++++++ 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/app/src/page/firstRun/internal/email-collect-page.js b/app/src/page/firstRun/internal/email-collect-page.js index 58d6c4cc..eeabc52d 100644 --- a/app/src/page/firstRun/internal/email-collect-page.js +++ b/app/src/page/firstRun/internal/email-collect-page.js @@ -53,7 +53,7 @@ class EmailCollectPage extends React.PureComponent { }).catch(error => { this.setState({ authenticationFailed: true }); }); - }, useTimeout ? 5000 : 0); // if useTimeout is set, wait 5s to give the daemon some time to start + }, useTimeout ? 10000 : 0); // if useTimeout is set, wait 10s to give the daemon some time to start } handleChangeText = (text) => { diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js index f2f72548..4767bce3 100644 --- a/app/src/page/splash/view.js +++ b/app/src/page/splash/view.js @@ -22,13 +22,15 @@ class SplashScreen extends React.PureComponent { componentWillMount() { this.setState({ + daemonReady: false, details: 'Starting daemon', message: 'Connecting', isRunning: false, isLagging: false, launchUrl: null, isDownloadingHeaders: false, - headersDownloadProgress: 0 + headersDownloadProgress: 0, + shouldAuthenticate: false }); if (NativeModules.DaemonServiceControl) { @@ -56,7 +58,7 @@ class SplashScreen extends React.PureComponent { componentWillUpdate(nextProps) { const { navigation, verifyUserEmail, verifyUserEmailFailure } = this.props; const { user } = nextProps; - if (user && user.id) { + if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) { // user is authenticated, navigate to the main view const resetAction = NavigationActions.reset({ index: 0, @@ -106,6 +108,7 @@ class SplashScreen extends React.PureComponent { // TODO: This is a hack, and the logic should live in the daemon // to give us a better sense of when we are actually started this.setState({ + daemonReady: true, message: 'Testing Network', details: 'Waiting for name resolution', isLagging: false, @@ -121,8 +124,18 @@ class SplashScreen extends React.PureComponent { notify } = this.props; - authenticate(null); balanceSubscribe(); + NativeModules.VersionInfo.getAppVersion().then(appVersion => { + 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); + } + this.setState({ shouldAuthenticate: true }); + }); }); return; } diff --git a/src/main/java/io/lbry/browser/MainActivity.java b/src/main/java/io/lbry/browser/MainActivity.java index 9bca57bf..f27a8d8b 100644 --- a/src/main/java/io/lbry/browser/MainActivity.java +++ b/src/main/java/io/lbry/browser/MainActivity.java @@ -172,19 +172,10 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand Toast.makeText(context, "Rewards cannot be claimed because we could not identify your device.", Toast.LENGTH_LONG).show(); } - try { - MessageDigest md = MessageDigest.getInstance("SHA-384"); - md.update(id.getBytes("UTF-8")); - String hash = new BigInteger(1, md.digest()).toString(16); - - SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putString(DEVICE_ID_KEY, hash); - editor.commit(); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { - // SHA-384 not found, UTF-8 encoding not supported - Toast.makeText(context, "Rewards cannot be claimed because we could not identify your device.", Toast.LENGTH_LONG).show(); - } + SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(DEVICE_ID_KEY, id); + editor.commit(); return id; } diff --git a/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java b/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java index e3804aa2..7cc6595c 100644 --- a/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java +++ b/src/main/java/io/lbry/browser/reactmodules/UtilityModule.java @@ -2,13 +2,17 @@ package io.lbry.browser.reactmodules; import android.app.Activity; import android.content.Context; +import android.content.SharedPreferences; import android.view.View; import android.view.WindowManager; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import io.lbry.browser.MainActivity; + public class UtilityModule extends ReactContextBaseJavaModule { private Context context; @@ -71,4 +75,11 @@ public class UtilityModule extends ReactContextBaseJavaModule { View.SYSTEM_UI_FLAG_VISIBLE); } } + + @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); + } } -- 2.45.2 From 2f9616a48710ac66c461bb8a7f47e547fb3222f1 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 09:40:12 +0100 Subject: [PATCH 5/9] tweak status check on email collection page --- app/src/component/AppNavigator.js | 16 +++++++---- app/src/constants.js | 1 + .../firstRun/internal/email-collect-page.js | 27 ++++++++++++------- app/src/page/splash/view.js | 2 ++ 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/app/src/component/AppNavigator.js b/app/src/component/AppNavigator.js index 60d401c1..479f90db 100644 --- a/app/src/component/AppNavigator.js +++ b/app/src/component/AppNavigator.js @@ -39,9 +39,10 @@ import { import { makeSelectClientSetting } from '../redux/selectors/settings'; import { decode as atob } from 'base-64'; import Icon from 'react-native-vector-icons/FontAwesome5'; +import Constants from '../constants'; import discoverStyle from '../styles/discover'; import searchStyle from '../styles/search'; -import SearchRightHeaderIcon from "../component/searchRightHeaderIcon"; +import SearchRightHeaderIcon from '../component/searchRightHeaderIcon'; const discoverStack = StackNavigator({ Discover: { @@ -210,10 +211,14 @@ class AppWithNavigationState extends React.Component { !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'] })); + AsyncStorage.getItem(Constants.KEY_SHOULD_VERIFY_EMAIL).then(shouldVerify => { + if ('true' === shouldVerify) { + this.setState({ emailVerifyDone: true }); + const message = emailVerifyErrorMessage ? + String(emailVerifyErrorMessage) : 'Your email address was successfully verified.'; + dispatch(doNotify({ message, displayType: ['toast'] })); + } + }); } } @@ -243,6 +248,7 @@ class AppWithNavigationState extends React.Component { } if (verification.token && verification.recaptcha) { + AsyncStorage.setItem(Constants.KEY_SHOULD_VERIFY_EMAIL, 'true'); try { dispatch(doUserEmailVerify(verification.token, verification.recaptcha)); } catch (error) { diff --git a/app/src/constants.js b/app/src/constants.js index 6cc5685f..2bb046e3 100644 --- a/app/src/constants.js +++ b/app/src/constants.js @@ -1,5 +1,6 @@ const Constants = { KEY_FIRST_RUN_EMAIL: "firstRunEmail", + KEY_SHOULD_VERIFY_EMAIL: "shouldVerifyEmail", SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS", diff --git a/app/src/page/firstRun/internal/email-collect-page.js b/app/src/page/firstRun/internal/email-collect-page.js index eeabc52d..179a1e25 100644 --- a/app/src/page/firstRun/internal/email-collect-page.js +++ b/app/src/page/firstRun/internal/email-collect-page.js @@ -10,15 +10,19 @@ import { } from 'react-native'; import Button from '../../../component/button'; import Colors from '../../../styles/colors'; +import Constants from '../../../constants'; import firstRunStyle from '../../../styles/firstRun'; class EmailCollectPage extends React.PureComponent { + static MAX_STATUS_TRIES = 15; + constructor() { super(); this.state = { email: null, authenticationStarted: false, - authenticationFailed: false + authenticationFailed: false, + statusTries: 0 }; } @@ -34,7 +38,7 @@ class EmailCollectPage extends React.PureComponent { // call user/new const { generateAuthToken, authenticating, authToken } = this.props; if (!authToken && !authenticating) { - this.startAuthenticating(true); + this.startAuthenticating(); } AsyncStorage.getItem('firstRunEmail').then(email => { @@ -44,22 +48,27 @@ class EmailCollectPage extends React.PureComponent { }); } - startAuthenticating = (useTimeout) => { + startAuthenticating = () => { const { generateAuthToken } = this.props; this.setState({ authenticationStarted: true, authenticationFailed: false }); - setTimeout(() => { - Lbry.status().then(info => { + Lbry.status().then(info => { generateAuthToken(info.installation_id) - }).catch(error => { + }).catch(error => { + if (this.state.statusTries >= EmailCollectPage.MAX_STATUS_TRIES) { this.setState({ authenticationFailed: true }); - }); - }, useTimeout ? 10000 : 0); // if useTimeout is set, wait 10s to give the daemon some time to start + } else { + setTimeout(() => { + this.startAuthenticating(); + this.setState({ statusTries: this.state.statusTries + 1 }); + }, 1000); // Retry every second for a maximum of MAX_STATUS_TRIES tries (15 seconds) + } + }); } handleChangeText = (text) => { // save the value to the state email this.setState({ email: text }); - AsyncStorage.setItem('firstRunEmail', text); + AsyncStorage.setItem(Constants.KEY_FIRST_RUN_EMAIL, text); } render() { diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js index 4767bce3..4dfabe1d 100644 --- a/app/src/page/splash/view.js +++ b/app/src/page/splash/view.js @@ -13,6 +13,7 @@ import { NavigationActions } from 'react-navigation'; import { decode as atob } from 'base-64'; import PropTypes from 'prop-types'; import Colors from '../../styles/colors'; +import Constants from '../../constants'; import splashStyle from '../../styles/splash'; class SplashScreen extends React.PureComponent { @@ -78,6 +79,7 @@ class SplashScreen extends React.PureComponent { console.log(error); } if (verification.token && verification.recaptcha) { + AsyncStorage.setItem(Constants.KEY_SHOULD_VERIFY_EMAIL, 'true'); try { verifyUserEmail(verification.token, verification.recaptcha); } catch (error) { -- 2.45.2 From c9d7924bf636cc49c9ceca96db99bce1be18b295 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 09:46:41 +0100 Subject: [PATCH 6/9] remove constructors per review --- .../firstRun/internal/email-collect-page.js | 15 ++++++--------- app/src/page/firstRun/view.js | 17 +++++++---------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/app/src/page/firstRun/internal/email-collect-page.js b/app/src/page/firstRun/internal/email-collect-page.js index 179a1e25..3eced95a 100644 --- a/app/src/page/firstRun/internal/email-collect-page.js +++ b/app/src/page/firstRun/internal/email-collect-page.js @@ -16,15 +16,12 @@ import firstRunStyle from '../../../styles/firstRun'; class EmailCollectPage extends React.PureComponent { static MAX_STATUS_TRIES = 15; - constructor() { - super(); - this.state = { - email: null, - authenticationStarted: false, - authenticationFailed: false, - statusTries: 0 - }; - } + state = { + email: null, + authenticationStarted: false, + authenticationFailed: false, + statusTries: 0 + }; componentWillReceiveProps(nextProps) { const { authenticating, authToken } = this.props; diff --git a/app/src/page/firstRun/view.js b/app/src/page/firstRun/view.js index e3d24d64..e668083e 100644 --- a/app/src/page/firstRun/view.js +++ b/app/src/page/firstRun/view.js @@ -22,16 +22,13 @@ class FirstRunScreen extends React.PureComponent { 'email-collect' ]; - constructor() { - super(); - this.state = { - currentPage: null, - emailSubmitted: false, - isFirstRun: false, - launchUrl: null, - showBottomContainer: true - }; - } + state = { + currentPage: null, + emailSubmitted: false, + isFirstRun: false, + launchUrl: null, + showBottomContainer: true + }; componentDidMount() { Linking.getInitialURL().then((url) => { -- 2.45.2 From 113704a8c1fd1762bfb22535b1c275d183c093c7 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 09:52:17 +0100 Subject: [PATCH 7/9] change SETTING_ALPHA_UNDERSTANDS_RISKS string value to camel case --- app/src/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/constants.js b/app/src/constants.js index 2bb046e3..c58f0a85 100644 --- a/app/src/constants.js +++ b/app/src/constants.js @@ -2,7 +2,7 @@ const Constants = { KEY_FIRST_RUN_EMAIL: "firstRunEmail", KEY_SHOULD_VERIFY_EMAIL: "shouldVerifyEmail", - SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS", + SETTING_ALPHA_UNDERSTANDS_RISKS: "alphaUnderstandRisks", ACTION_FIRST_RUN_PAGE_CHANGED: "FIRST_RUN_PAGE_CHANGED", }; -- 2.45.2 From 7347793f8465553f4db4103b815954f6088133fa Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 09:59:24 +0100 Subject: [PATCH 8/9] remove unused import --- app/src/page/firstRun/internal/email-collect-page.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/page/firstRun/internal/email-collect-page.js b/app/src/page/firstRun/internal/email-collect-page.js index 3eced95a..0f8aac2a 100644 --- a/app/src/page/firstRun/internal/email-collect-page.js +++ b/app/src/page/firstRun/internal/email-collect-page.js @@ -8,7 +8,6 @@ import { TextInput, View } from 'react-native'; -import Button from '../../../component/button'; import Colors from '../../../styles/colors'; import Constants from '../../../constants'; import firstRunStyle from '../../../styles/firstRun'; -- 2.45.2 From 559b9d6d28a483ff38abe2cea6ba556f51c9423a Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Thu, 16 Aug 2018 10:04:51 +0100 Subject: [PATCH 9/9] import AsyncStorage on splash screen view --- app/src/page/splash/view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js index 4dfabe1d..c8394079 100644 --- a/app/src/page/splash/view.js +++ b/app/src/page/splash/view.js @@ -2,6 +2,7 @@ import React from 'react'; import { Lbry } from 'lbry-redux'; import { ActivityIndicator, + AsyncStorage, Linking, NativeModules, Platform, -- 2.45.2