diff --git a/example.js b/example.js index 086eddcff..41142d151 100644 --- a/example.js +++ b/example.js @@ -11,21 +11,15 @@ import { Provider } from 'react-redux' import thunk from 'redux-thunk' import createLogger from 'redux-logger' -// Auth0 -import { Auth0Provider } from 'use-auth0-hooks' -import { accountLinks, getAuth0Callbacks, getAuth0Config } from './lib/util/auth' -import { AUTH0_AUDIENCE, AUTH0_SCOPE, URL_ROOT } from './lib/util/constants' - // import Bootstrap Grid components for layout -import { Nav, Navbar, Grid, Row, Col } from 'react-bootstrap' +import { Grid, Row, Col } from 'react-bootstrap' // import OTP-RR components import { - AppMenu, DefaultMainPanel, + DesktopNav, Map, MobileMain, - NavLoginButtonAuth0, ResponsiveWebapp, createOtpReducer, createUserReducer @@ -81,37 +75,13 @@ const store = createStore( compose(applyMiddleware(...middleware)) ) -// Auth0 config and callbacks. -const auth0Config = getAuth0Config(otpConfig.persistence) -const auth0Callbacks = getAuth0Callbacks(store) - // define a simple responsive UI using Bootstrap and OTP-RR class OtpRRExample extends Component { render () { /** desktop view **/ const desktopView = (
- - - -
- -
-
OpenTripPlanner
-
-
- - {auth0Config && ( - - - - )} -
+ @@ -146,34 +116,20 @@ class OtpRRExample extends Component { } } -const innerProvider = ( - - { /** +// render the app +render( + ( + + { /** * If not using router history, simply include OtpRRExample here: * e.g. * */ - } - - -) - -// render the app -render(auth0Config - ? ( - - {innerProvider} - + } + + ) - : innerProvider -, + , -document.getElementById('root') + document.getElementById('root') ) diff --git a/lib/actions/auth.js b/lib/actions/auth.js new file mode 100644 index 000000000..cf5478e0f --- /dev/null +++ b/lib/actions/auth.js @@ -0,0 +1,59 @@ +import { push } from 'connected-react-router' + +import { setPathBeforeSignIn } from '../actions/user' + +/** + * This function is called by the Auth0Provider component, with the described parameter(s), + * when a new access token could not be retrieved. + * @param {Error} err + * @param {AccessTokenRequestOptions} options + */ +export function showAccessTokenError (err, options) { + return function (dispatch, getState) { + // TODO: improve this. + console.error('Failed to retrieve access token: ', err) + } +} + +/** + * This function is called by the Auth0Provider component, with the described parameter(s), + * when signing-in fails for some reason. + * @param {Error} err + */ +export function showLoginError (err) { + return function (dispatch, getState) { + // TODO: improve this. + if (err) dispatch(push('/oops')) + } +} + +/** + * This function is called by the Auth0Provider component, with the described parameter(s), + * after the user signs in. + * @param {Object} appState The state that was stored when calling useAuth().login(). + */ +export function processSignIn (appState) { + return function (dispatch, getState) { + if (appState && appState.urlHash) { + // At this stage after login, Auth0 has already redirected to /signedin (Auth0-whitelisted) + // which shows the AfterLoginScreen. + // + // Here, we save the URL hash prior to login (contains a combination of itinerary search, stop/trip view, etc.), + // so that the AfterLoginScreen can redirect back there when logged-in user info is fetched. + // (For routing, it is easier to deal with the path without the hash sign.) + const hashIndex = appState.urlHash.indexOf('#') + const urlHashWithoutHash = hashIndex >= 0 + ? appState.urlHash.substr(hashIndex + 1) + : '/' + dispatch(setPathBeforeSignIn(urlHashWithoutHash)) + } else if (appState && appState.returnTo) { + // TODO: Handle other after-login situations. + // Note that when redirecting from a login-protected (e.g. account) page while logged out, + // then returnTo is set by Auth0 to this object format: + // { + // pathname: "/" + // query: { ... } + // } + } + } +} diff --git a/lib/components/app/app-nav.js b/lib/components/app/app-nav.js deleted file mode 100644 index fc98a06b8..000000000 --- a/lib/components/app/app-nav.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import { Nav, Navbar } from 'react-bootstrap' -import { connect } from 'react-redux' - -import NavLoginButtonAuth0 from '../user/nav-login-button-auth0.js' -import { accountLinks, getAuth0Config } from '../../util/auth' - -import AppMenu from './app-menu' - -const AppNav = ({ otpConfig }) => { - const { branding, persistence } = otpConfig - const showLogin = Boolean(getAuth0Config(persistence)) - - return ( - - - -
- -
-
- - - - {showLogin && ( - - - - )} - - ) -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return { - otpConfig: state.otp.config - } -} - -const mapDispatchToProps = { -} - -export default connect(mapStateToProps, mapDispatchToProps)(AppNav) diff --git a/lib/components/app/desktop-nav.js b/lib/components/app/desktop-nav.js new file mode 100644 index 000000000..558f166aa --- /dev/null +++ b/lib/components/app/desktop-nav.js @@ -0,0 +1,81 @@ +import React from 'react' +import { Nav, Navbar } from 'react-bootstrap' +import { connect } from 'react-redux' + +import NavLoginButtonAuth0 from '../user/nav-login-button-auth0.js' +import { accountLinks, getAuth0Config } from '../../util/auth' +import { DEFAULT_APP_TITLE } from '../../util/constants' +import AppMenu from './app-menu' + +/** + * The desktop navigation bar, featuring a `branding` logo or a `title` text + * defined in config.yml, and a sign-in button/menu with account links. + * + * The `branding` and `title` parameters in config.yml are handled + * and shown in this order in the navigation bar: + * 1. If `branding` is defined, it is shown, and no title is displayed. + * 2. If `branding` is not defined but if `title` is, then `title` is shown. + * 3. If neither is defined, just show 'OpenTripPlanner' (DEFAULT_APP_TITLE). + * + * TODO: merge with the mobile navigation bar. + */ +const DesktopNav = ({ otpConfig }) => { + const { branding, persistence, title = DEFAULT_APP_TITLE } = otpConfig + const showLogin = Boolean(getAuth0Config(persistence)) + + // Display branding and title in the order as described in the class summary. + let brandingOrTitle + if (branding) { + brandingOrTitle = ( +
+ ) + } else { + brandingOrTitle = ( +
{title}
+ ) + } + + return ( + + + + {/* TODO: Reconcile CSS class and inline style. */} +
+ +
+ + {brandingOrTitle} + +
+
+ + {showLogin && ( + + + + )} +
+ ) +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { + otpConfig: state.otp.config + } +} + +const mapDispatchToProps = { +} + +export default connect(mapStateToProps, mapDispatchToProps)(DesktopNav) diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 2738aa27d..8ff54780b 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -6,15 +6,20 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import { connect } from 'react-redux' import { Route, Switch, withRouter } from 'react-router' +import { Auth0Provider } from 'use-auth0-hooks' import PrintLayout from './print-layout' +import * as authActions from '../../actions/auth' import { setMapCenter, setMapZoom } from '../../actions/config' import { formChanged, parseUrlQueryString } from '../../actions/form' import { getCurrentPosition, receivedPositionResponse } from '../../actions/location' import { setLocationToCurrent } from '../../actions/map' import { handleBackButtonPress, matchContentToUrl } from '../../actions/ui' +import { getAuth0Config } from '../../util/auth' +import { AUTH0_AUDIENCE, AUTH0_SCOPE, URL_ROOT } from '../../util/constants' import { getActiveItinerary, getTitle } from '../../util/state' import AfterSignInScreen from '../user/after-signin-screen' +import BeforeSignInScreen from '../user/before-signin-screen' import UserAccountScreen from '../user/user-account-screen' import withLoggedInUserSupport from '../user/with-logged-in-user-support' @@ -168,7 +173,13 @@ const WebappWithRouter = withRouter( ) ) -class RouterWrapper extends Component { +/** + * The routing component for the application. + * This is the top-most "standard" component, + * and we initialize the Auth0Provider here + * so that Auth0 services are available everywhere. + */ +class RouterWrapperWithAuth0 extends Component { /** * Combine the router props with the other props that get * passed to the exported component. This way, it is possible for @@ -180,8 +191,15 @@ class RouterWrapper extends Component { } render () { - const { routerConfig } = this.props - return ( + const { + auth0Config, + processSignIn, + routerConfig, + showAccessTokenError, + showLoginError + } = this.props + + const router = ( @@ -241,7 +259,38 @@ class RouterWrapper extends Component {
) + + return ( + auth0Config + ? ( + + {router} + + ) + : router + ) } } -const mapStateToWrapperProps = (state, ownProps) => ({ routerConfig: state.otp.config.reactRouter }) -export default connect(mapStateToWrapperProps)(RouterWrapper) + +const mapStateToWrapperProps = (state, ownProps) => ({ + auth0Config: getAuth0Config(state.otp.config.persistence), + routerConfig: state.otp.config.reactRouter +}) + +const mapWrapperDispatchToProps = { + processSignIn: authActions.processSignIn, + showAccessTokenError: authActions.showAccessTokenError, + showLoginError: authActions.showLoginError +} + +export default connect(mapStateToWrapperProps, mapWrapperDispatchToProps)(RouterWrapperWithAuth0) diff --git a/lib/components/mobile/navigation-bar.js b/lib/components/mobile/navigation-bar.js index 81f6e5910..cac315e1c 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -26,8 +26,13 @@ class MobileNavigationBar extends Component { } render () { - const { headerAction, headerText, persistence, showBackButton, title } = this.props - const auth0Config = getAuth0Config(persistence) + const { + auth0Config, + headerAction, + headerText, + showBackButton, + title + } = this.props return ( @@ -75,7 +80,7 @@ class MobileNavigationBar extends Component { const mapStateToProps = (state, ownProps) => { return { - persistence: state.otp.config.persistence + auth0Config: getAuth0Config(state.otp.config.persistence) } } diff --git a/lib/components/user/before-signin-screen.js b/lib/components/user/before-signin-screen.js new file mode 100644 index 000000000..b6f2e7e96 --- /dev/null +++ b/lib/components/user/before-signin-screen.js @@ -0,0 +1,17 @@ +import React from 'react' + +/** + * This screen is flashed just before the Auth0 login page is shown. + * TODO: improve this screen. + */ +const BeforeSignInScreen = () => ( +
+

Signing you in

+

+ In order to access this page you will need to sign in. + Please wait while we redirect you to the login page... +

+
+) + +export default BeforeSignInScreen diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index 6dd48772e..0c77fa85d 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -6,7 +6,7 @@ import { withLoginRequired } from 'use-auth0-hooks' import { routeTo } from '../../actions/ui' import { createOrUpdateUser } from '../../actions/user' import { isNewUser } from '../../util/user' -import AppNav from '../app/app-nav' +import DesktopNav from '../app/desktop-nav' import AccountSetupFinishPane from './account-setup-finish-pane' import ExistingAccountDisplay from './existing-account-display' import FavoriteLocationsPane from './favorite-locations-pane' @@ -121,7 +121,7 @@ class UserAccountScreen extends Component { return (
{/* TODO: Do mobile view. */} - +
{formContents}
diff --git a/lib/index.js b/lib/index.js index 8a12bb97a..8c513a2c8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -37,6 +37,7 @@ import ViewerContainer from './components/viewers/viewer-container' import ResponsiveWebapp from './components/app/responsive-webapp' import AppMenu from './components/app/app-menu' +import DesktopNav from './components/app/desktop-nav' import DefaultMainPanel from './components/app/default-main-panel' import { setAutoPlan, setMapCenter } from './actions/config' @@ -97,6 +98,7 @@ export { // app components, ResponsiveWebapp, AppMenu, + DesktopNav, DefaultMainPanel, // actions diff --git a/lib/util/auth.js b/lib/util/auth.js index 3356926a7..5b6e06268 100644 --- a/lib/util/auth.js +++ b/lib/util/auth.js @@ -1,6 +1,3 @@ -import { push } from 'connected-react-router' - -import { setPathBeforeSignIn } from '../actions/user' import { PERSISTENCE_STRATEGY_OTP_MIDDLEWARE } from './constants' /** @@ -37,69 +34,3 @@ export function getAuth0Config (persistence) { } return null } - -/** - * Gets the callback methods for Auth0. - * Note: Methods inside are originally copied from https://github.com/sandrinodimattia/use-auth0-hooks#readme - * and some methods from that example may still be untouched. - * @param reduxStore The redux store to carry out the navigation action. - */ -export function getAuth0Callbacks (reduxStore) { - return { - /** - * When it hasn't been possible to retrieve a new access token. - * @param {Error} err - * @param {AccessTokenRequestOptions} options - */ - onAccessTokenError: (err, options) => { - console.error('Failed to retrieve access token: ', err) - }, - /** - * When signing in fails for some reason, we want to show it here. - * TODO: Implement the error URL. - * @param {Error} err - */ - onLoginError: err => { - if (err) reduxStore.dispatch(push(`/oops`)) - }, - /** - * Where to send the user after they have signed in. - */ - onRedirectCallback: appState => { - if (appState && appState.urlHash) { - // At this stage after login, Auth0 has already redirected to /signedin (Auth0-whitelisted) - // which shows the AfterLoginScreen. - // - // Here, we save the URL hash prior to login (contains a combination of itinerary search, stop/trip view, etc.), - // so that the AfterLoginScreen can redirect back there when logged-in user info is fetched. - // (For routing, it is easier to deal with the path without the hash sign.) - const urlHashWithoutHash = (appState.urlHash.split('#')[1] || '/') - reduxStore.dispatch(setPathBeforeSignIn(urlHashWithoutHash)) - } else if (appState && appState.returnTo) { - // TODO: Handle other after-login situations. - // Careful! - // - When redirecting from a login-protected (e.g. account) page while logged out, - // then returnTo is set by Auth0 to this object format: - // { - // pathname: "/" - // query: { ... } - // } - } - }, - /** - * When redirecting to the login page you'll end up in this state where the login page is still loading. - * You can render a message to show that the user is being redirected. - */ - onRedirecting: () => { - return ( -
-

Signing you in

-

- In order to access this page you will need to sign in. - Please wait while we redirect you to the login page... -

-
- ) - } - } -} diff --git a/lib/util/constants.js b/lib/util/constants.js index d5591b214..3f16df29e 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -1,5 +1,6 @@ export const AUTH0_AUDIENCE = 'https://otp-middleware' export const AUTH0_SCOPE = '' +export const DEFAULT_APP_TITLE = 'OpenTripPlanner' export const PERSISTENCE_STRATEGY_OTP_MIDDLEWARE = 'otp_middleware' // Gets the root URL, e.g. https://otp-instance.example.com:8080, computed once for all.