Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 67 additions & 51 deletions lib/actions/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,65 +49,63 @@ function getMiddlewareVariables (state) {
}
}

/**
* Attempts to fetch the auth0 access token and store it in the redux state, under state.user.
* @param auth0 The auth0 context used to obtain the access token for subsequent middleware fetches.
*/
export function fetchAuth0Token (auth0) {
return async function (dispatch, getState) {
try {
const accessToken = await auth0.getAccessTokenSilently()

dispatch(setCurrentUser({ accessToken }))
} catch (error) {
// TODO: improve UI if there is an errror.
alert('Error obtaining an authorization token.')
}
}
}

/**
* Attempts to fetch user preferences (or set initial values if the user is being created)
* into the redux state, under state.user.
* @param auth0 If provided, the auth0 login object used to initially obtain the auth0 access token
* for subsequent middleware fetches (and also to initialize new users from auth0 email and id).
* If absent, state.user.accessToken will be used for fetches.
* @param auth0User If provided, the auth0.user object used to initialize the default user object (with email and auth0 id).
*/
export function fetchOrInitializeUser (auth0) {
export function fetchOrInitializeUser (auth0User) {
return async function (dispatch, getState) {
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/fromtoken`

// Get the Auth0 access token. If one is in state.user, use it,
// otherwise if auth0 is provided, fetch it.
let token
if (accessToken) {
token = accessToken
} else if (auth0) {
try {
token = await auth0.getAccessTokenSilently()
} catch (error) {
// TODO: improve UI if there is an errror.
alert('Error obtaining an authorization token.')
}
}

// Once accessToken is available, proceed to fetch or initialize loggedInUser.
if (token) {
const { data: user, status } = await secureFetch(requestUrl, token, apiKey)

// Beware! On AWS API gateway, if a user is not found in the middleware
// (e.g. they just created their Auth0 password but have not completed the account setup form yet),
// the call above will return, for example:
// {
// status: 'success',
// data: {
// "result": "ERR",
// "message": "No user with id=000000 found.",
// "code": 404,
// "detail": null
// }
// }
//
// The same call to a middleware instance that is not behind an API gateway
// will return:
// {
// status: 'error',
// message: 'Error get-ing user...'
// }
// TODO: Improve AWS response.

const isNewAccount = status === 'error' || (user && user.result === 'ERR')
const userData = isNewAccount ? createNewUser(auth0.user) : user
dispatch(setCurrentUser({ accessToken: token, user: userData }))

// Also load monitored trips for existing users.
if (!isNewAccount) {
dispatch(fetchUserMonitoredTrips())
}
const { data: user, status } = await secureFetch(requestUrl, accessToken, apiKey)

// Beware! On AWS API gateway, if a user is not found in the middleware
// (e.g. they just created their Auth0 password but have not completed the account setup form yet),
// the call above will return, for example:
// {
// status: 'success',
// data: {
// "result": "ERR",
// "message": "No user with id=000000 found.",
// "code": 404,
// "detail": null
// }
// }
//
// The same call to a middleware instance that is not behind an API gateway
// will return:
// {
// status: 'error',
// message: 'Error get-ing user...'
// }
// TODO: Improve AWS response.

const isNewAccount = status === 'error' || (user && user.result === 'ERR')
const userData = isNewAccount ? createNewUser(auth0User) : user
dispatch(setCurrentUser({ accessToken, user: userData }))

// Also load monitored trips for existing users.
if (!isNewAccount) {
dispatch(fetchUserMonitoredTrips())
}
}
}
Expand Down Expand Up @@ -152,6 +150,24 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) {
}
}

/**
* Requests the verification email for the new user to be resent.
*/
export function resendVerificationEmail () {
return async function (dispatch, getState) {
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())

// TODO: add any throttling.
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/verification-email`
const { status } = await secureFetch(requestUrl, accessToken, apiKey)

// TODO: improve the UI feedback messages for this.
if (status === 'success') {
alert('The email verification message has been resent.')
}
}
}

/**
* Fetches the saved/monitored trips for a user.
* We use the accessToken to fetch the data regardless of
Expand Down
6 changes: 1 addition & 5 deletions lib/components/user/user-account-screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,7 @@ class UserAccountScreen extends Component {
// Capture whether user is a new user at this stage, and retain that value as long as this screen is active.
// Reminder: When a new user progresses through the account steps,
// isNewUser(loggedInUser) will change to false as the database gets updated.
isNewUser: isNewUser(props.loggedInUser),

// Last number and last time we requested a code for (to avoid repeat SMS and not waste SMS quota).
lastPhoneNumberRequested: null,
lastPhoneRequestTime: null
isNewUser: isNewUser(props.loggedInUser)
}
}

Expand Down
82 changes: 58 additions & 24 deletions lib/components/user/verify-email-screen.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from 'react'
import React, { Component } from 'react'
import { Button } from 'react-bootstrap'
import { connect } from 'react-redux'
import styled from 'styled-components'

const _handleClick = () => window.location.reload()
import * as userActions from '../../actions/user'

const DivSpacer = styled.div`
margin-top: ${props => props.space || 2}em;
`

/**
* This component contains the prompt for the user to verify their email address.
Expand All @@ -10,25 +16,53 @@ const _handleClick = () => window.location.reload()
* (One way to make sure the parent page fetches the latest email verification status
* is to simply reload the page.)
*/
const VerifyEmailScreen = () => (
<div>
<h1>Verify your email address</h1>
<p>
Please check your email inbox and follow the link in the message
to verify your email address before finishing your account setup.
</p>
<p>
Once you're verified, click the button below to continue.
</p>

<Button
bsSize='large'
bsStyle='primary'
onClick={_handleClick}
>
My email is verified!
</Button>
</div>
)

export default VerifyEmailScreen
class VerifyEmailScreen extends Component {
_handleEmailVerified = () => window.location.reload()

render () {
const { resendVerificationEmail } = this.props
return (
<div>
<h1>Verify your email address</h1>
<p>
Please check your email inbox and follow the link in the message
to verify your email address before finishing your account setup.
</p>
<p>
Once you're verified, click the button below to continue.
</p>
<DivSpacer>
<Button
bsSize='large'
bsStyle='primary'
onClick={this._handleEmailVerified}
>
My email is verified!
</Button>
</DivSpacer>

<DivSpacer space={1.5}>
<Button
bsStyle='link'
onClick={resendVerificationEmail}
style={{padding: '0px'}}
>
Resend verification email
</Button>
</DivSpacer>
</div>
)
}
}

// connect to the redux store

const mapStateToProps = () => {
return {}
}

const mapDispatchToProps = {
resendVerificationEmail: userActions.resendVerificationEmail
}

export default connect(mapStateToProps, mapDispatchToProps)(VerifyEmailScreen)
27 changes: 23 additions & 4 deletions lib/components/user/with-logged-in-user-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,27 @@ class UserLoaderScreen extends Component {
return auth0 && auth0.isAuthenticated && !loggedInUser
}

/**
* Determines whether an auth0 token should be fetched.
* @returns true if the logged-in user has passed Auth0 authentication
* and state.user.accessToken has not been set; false otherwise.
*/
acccessTokenIsUnfetched = () => {
const { accessToken, auth0 } = this.props
return auth0 && auth0.isAuthenticated && !accessToken
}

componentDidUpdate () {
const { auth0, fetchOrInitializeUser } = this.props
const {
auth0,
fetchAuth0Token,
fetchOrInitializeUser
} = this.props

if (this.loggedInUserIsUnfetched()) {
fetchOrInitializeUser(auth0)
if (this.acccessTokenIsUnfetched()) {
fetchAuth0Token(auth0)
} else if (this.loggedInUserIsUnfetched()) {
fetchOrInitializeUser(auth0.user)
}
}

Expand All @@ -77,12 +93,15 @@ class UserLoaderScreen extends Component {
// connect to the redux store

const mapStateToProps = (state, ownProps) => {
const { accessToken, loggedInUser } = state.user
return {
loggedInUser: state.user.loggedInUser
accessToken,
loggedInUser
}
}

const mapDispatchToProps = {
fetchAuth0Token: userActions.fetchAuth0Token,
fetchOrInitializeUser: userActions.fetchOrInitializeUser
}

Expand Down