Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phone Verification #224

Merged
merged 58 commits into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
4139552
fix(NewAccountWizard): Create an OtpUser entry when user clicks Next …
binh-dam-ibigroup Sep 1, 2020
7f1851b
refactor(UserAccountScreen): Update working copy of state.user on com…
binh-dam-ibigroup Sep 1, 2020
1907352
fix(FavoriteLocationsPane): Remove console warning regarding null <in…
binh-dam-ibigroup Sep 3, 2020
3100de8
fix(NotificationPrefsPane): Prepare UI support for phone verification.
binh-dam-ibigroup Sep 3, 2020
12d7afe
fix(Hook to send phone verification number.):
binh-dam-ibigroup Sep 3, 2020
74ab103
feat(NotificationPrefsPane): Add phone verification for existing users.
binh-dam-ibigroup Sep 4, 2020
33686c0
feat(NewAccountWizard): Add phone verification in account setup wizard.
binh-dam-ibigroup Sep 4, 2020
e36a76c
improvement(UserAccountScreen): Try throttling SMS code requests.
binh-dam-ibigroup Sep 4, 2020
106fa6d
refactor(NotificationPrefsPane): Add button to resend code. Rename me…
binh-dam-ibigroup Sep 8, 2020
f821a25
refactor: Rename symbols and tweak comments.
binh-dam-ibigroup Sep 8, 2020
586fa23
refactor(NotificationPrefsPane): Move styles into styled component.
binh-dam-ibigroup Sep 8, 2020
626fdb7
refactor: Address PR comments.
binh-dam-ibigroup Sep 15, 2020
714d795
docs(NotificationPrefsPane): Add comment+link regarding the fake phon…
binh-dam-ibigroup Sep 15, 2020
45a2057
refactor(actions/user): Address PR comments.
binh-dam-ibigroup Oct 2, 2020
2dd7e10
refactor(actions/user): Extract common code from all methods.
binh-dam-ibigroup Oct 2, 2020
495bf04
refactor(actions/user): Refactor middleware methods.
binh-dam-ibigroup Oct 2, 2020
2154b28
refactor(actions/user): Tweak comments per PR feedback.
binh-dam-ibigroup Oct 5, 2020
e8f9d39
fix(actions/user): Do not persist user before sending sms req/after p…
binh-dam-ibigroup Oct 8, 2020
a328c77
refactor(NotificationPrefsPane): Tweak phone number revert functional…
binh-dam-ibigroup Oct 8, 2020
5d5c8a5
refactor(NotificationPrefsPane): Hide Revert Number if no phone numbe…
binh-dam-ibigroup Oct 8, 2020
194b1b8
Merge branch 'dev' into phone-verification
binh-dam-ibigroup Oct 12, 2020
90009cc
refactor(UserAccountScreen): Adapt initial user persistence to Formik…
binh-dam-ibigroup Oct 13, 2020
bdbd857
refactor(config.yml): Make phone number regex configurable.
binh-dam-ibigroup Oct 13, 2020
084f6c7
refactor(NotificationsPrefsPane): Implement new phone verif. flow
binh-dam-ibigroup Oct 13, 2020
eadf635
refactor(NotificationPrefsPane): Various refactors
binh-dam-ibigroup Oct 13, 2020
7f5148d
refactor(NotificationPrefsPane): Update phone numbers following API c…
binh-dam-ibigroup Oct 14, 2020
f79a3f7
refactor(PhoneNumberEditor): Extract component from NotificationPrefs…
binh-dam-ibigroup Oct 15, 2020
79232ce
refactor: Make other light refactors.
binh-dam-ibigroup Oct 15, 2020
adc6897
refactor(UserAccountScreen): Move validation back to const. Add label…
binh-dam-ibigroup Oct 15, 2020
65d708c
Merge branch 'refactor-user-actions' into phone-verification
binh-dam-ibigroup Oct 15, 2020
920e0de
refactor(actions/user): Finish refactor user actions per #246 comments.
binh-dam-ibigroup Oct 15, 2020
7970429
refactor(user/actions): await fetches.
binh-dam-ibigroup Oct 15, 2020
9fa9b3c
refactor(PhoneNumberEditor): Add config for formatting numbers.
binh-dam-ibigroup Oct 15, 2020
5690ac8
refactor(NotificationPrefsPane): Use awesome-phone library.
binh-dam-ibigroup Oct 16, 2020
708f5b7
refactor(actions/user): Tweak error message text.
binh-dam-ibigroup Oct 16, 2020
b3837ec
Merge branch 'dev' into phone-verification
binh-dam-ibigroup Oct 16, 2020
6d2f94a
refactor(PhoneNumberEditor): Use react-phone-number-input
binh-dam-ibigroup Oct 19, 2020
2b7d992
refactor(NotificationPrefsPane): Use static text for email (same as v…
binh-dam-ibigroup Oct 19, 2020
31345ea
refactor(OtpReducer): Move default phone country to OTP reducer.
binh-dam-ibigroup Oct 20, 2020
f842563
test(otp-reducer): Update snapshot
binh-dam-ibigroup Oct 20, 2020
643ae43
refactor: Address PR comments
binh-dam-ibigroup Oct 20, 2020
2c4705c
refactor(PhoneNumberEditor): Exit edit mode if user enters same numbe…
binh-dam-ibigroup Oct 20, 2020
8e09348
refactor(UserAccountScreen): Handle SMS request error.
binh-dam-ibigroup Oct 20, 2020
d078e92
refactor(PhoneNumberEditor): Refactor, keep editing after SMS req err…
binh-dam-ibigroup Oct 21, 2020
4d5fcc8
refactor(PhoneNumberEditor): Refactor more
binh-dam-ibigroup Oct 21, 2020
a9dc87a
refactor(actions/user): Remove unencoutered id checks.
binh-dam-ibigroup Oct 21, 2020
aab4530
fix(PhoneNumberEditor): Update pending number in Formik state.
binh-dam-ibigroup Oct 22, 2020
bc43d29
refactor(Remove excess user event handling.):
binh-dam-ibigroup Oct 23, 2020
3c48027
refactor(UserAccountScreen): Update Formik if new props are passed.
binh-dam-ibigroup Oct 26, 2020
b1d3d7c
refactor: Perofrm light refactor.
binh-dam-ibigroup Oct 26, 2020
cbf2d86
refactor(actions/user): Address throttle and expired code alerts.
binh-dam-ibigroup Oct 27, 2020
705919a
fix(user reducer): Add handling code for last SMS request.
binh-dam-ibigroup Oct 27, 2020
08e40f4
refactor(PhoneNumberEditor): Change code input type to 'text'.
binh-dam-ibigroup Oct 27, 2020
f40f4a3
refactor(PhoneNumberEditor): Add validation for enter key.
binh-dam-ibigroup Oct 27, 2020
a876b71
refactor(PhoneNumberEditor): Use input type='tel' for validation code.
binh-dam-ibigroup Oct 28, 2020
40871d2
fix(actions/user): Fix sms throttling.
binh-dam-ibigroup Oct 28, 2020
4ecd75c
refactor(PhoneNumberEditor): Mention code expiration in SMS instructi…
binh-dam-ibigroup Oct 28, 2020
738e9cf
refactor(PhoneNumberEditor): Tweak expiration text.
binh-dam-ibigroup Oct 28, 2020
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
3 changes: 3 additions & 0 deletions __tests__/reducers/__snapshots__/create-otp-reducer.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Object {
"debouncePlanTimeMs": 0,
"homeTimezone": "America/Los_Angeles",
"language": Object {},
"phoneFormatOptions": Object {
"countryCode": "US",
},
"realtimeEffectsDisplayThreshold": 120,
"routingTypes": Array [],
"stopViewer": Object {
Expand Down
5 changes: 5 additions & 0 deletions example-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,8 @@ itinerary:
# modes:
# - WALK
# - BICYCLE

### If using OTP Middleware, you can define the optional phone number options below.
# phoneFormatOptions:
# # ISO 2-letter country code for phone number formats (defaults to 'US')
# countryCode: US
300 changes: 195 additions & 105 deletions lib/actions/user.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { createAction } from 'redux-actions'

import { routeTo } from './ui'
import {
addTrip,
addUser,
deleteTrip,
fetchUser,
getTrips,
updateTrip,
updateUser
} from '../util/middleware'
import { secureFetch } from '../util/middleware'
import { isNewUser } from '../util/user'

// Middleware API paths.
const API_MONITORTRIP_PATH = '/api/secure/monitoredtrip'
const API_OTPUSER_PATH = '/api/secure/user'
const API_OTPUSER_VERIFYSMS_PATH = '/verify_sms'

const setCurrentUser = createAction('SET_CURRENT_USER')
const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS')
const setLastPhoneSmsRequest = createAction('SET_LAST_PHONE_SMS_REQUEST')
export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')

function createNewUser (auth0User) {
Expand All @@ -28,101 +26,127 @@ function createNewUser (auth0User) {
}
}

/**
* Extracts accessToken, loggedInUser from the redux state,
* and apiBaseUrl, apiKey from the middleware configuration.
* If the middleware configuration does not exist, throws an error.
*/
function getMiddlewareVariables (state) {
const { otp, user } = state
const { otp_middleware: otpMiddleware = null } = otp.config.persistence

if (otpMiddleware) {
const { accessToken, loggedInUser } = user
const { apiBaseUrl, apiKey } = otpMiddleware
return {
accessToken,
apiBaseUrl,
apiKey,
loggedInUser
}
} else {
throw new Error('This action requires config.yml#persistence#otp_middleware to be defined.')
}
}

/**
* Fetches the saved/monitored trips for a user.
* We use the accessToken to fetch the data regardless of
* whether the process to populate state.user is completed or not.
*/
export function fetchUserMonitoredTrips (accessToken) {
return async function (dispatch, getState) {
const { otp } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`

if (otpMiddleware) {
const { data: trips, status: fetchStatus } = await getTrips(otpMiddleware, accessToken)
if (fetchStatus === 'success') {
dispatch(setCurrentUserMonitoredTrips(trips.data))
}
const { data: trips, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')
if (status === 'success') {
dispatch(setCurrentUserMonitoredTrips(trips.data))
}
}
}

/**
* Fetches user preferences to state.user, or set initial values under state.user if no user has been loaded.
* Fetches user preferences to state.user,
* or set initial values under state.user if no user has been loaded.
*/
export function fetchOrInitializeUser (auth) {
return async function (dispatch, getState) {
const { otp } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence

if (otpMiddleware) {
const { accessToken, user: authUser } = auth
const { data: user, status: fetchUserStatus } = await fetchUser(otpMiddleware, accessToken)

// 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 = fetchUserStatus === 'error' || (user && user.result === 'ERR')
if (!isNewAccount) {
// Load user's monitored trips before setting the user state.
await dispatch(fetchUserMonitoredTrips(accessToken))

dispatch(setCurrentUser({ accessToken, user }))
} else {
dispatch(setCurrentUser({ accessToken, user: createNewUser(authUser) }))
}
const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const { accessToken, user: authUser } = auth
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/fromtoken`

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')
if (!isNewAccount) {
// Load user's monitored trips before setting the user state.
await dispatch(fetchUserMonitoredTrips(accessToken))

dispatch(setCurrentUser({ accessToken, user }))
} else {
dispatch(setCurrentUser({ accessToken, user: createNewUser(authUser) }))
}
}
}

/**
* Updates (or creates) a user entry in the middleware,
* then, if that was successful, updates the redux state with that user.
* @param userData the user entry to persist.
* @param silentOnSuccess true to suppress the confirmation if the operation is successful (e.g. immediately after user accepts the terms).
*/
export function createOrUpdateUser (userData) {
export function createOrUpdateUser (userData, silentOnSuccess = false) {
return async function (dispatch, getState) {
const { otp, user } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState())
const { id } = userData // Middleware ID, NOT auth0 (or similar) id.
let requestUrl, method

if (otpMiddleware) {
const { accessToken, loggedInUser } = user
// Determine URL and method to use.
if (isNewUser(loggedInUser)) {
requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}`
method = 'POST'
} else {
requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${id}`
method = 'PUT'
}

let result
if (isNewUser(loggedInUser)) {
result = await addUser(otpMiddleware, accessToken, userData)
} else {
result = await updateUser(otpMiddleware, accessToken, userData)
}
const { data, message, status } = await secureFetch(requestUrl, accessToken, apiKey, method, {
body: JSON.stringify(userData)
})

// TODO: improve the UI feedback messages for this.
if (result.status === 'success' && result.data) {
// TODO: improve the UI feedback messages for this.
if (status === 'success' && data) {
if (!silentOnSuccess) {
alert('Your preferences have been saved.')

// Update application state with the user entry as saved
// (as returned) by the middleware.
const userData = result.data
dispatch(setCurrentUser({ accessToken, user: userData }))
} else {
alert(`An error was encountered:\n${JSON.stringify(result)}`)
}

// Update application state with the user entry as saved
// (as returned) by the middleware.
dispatch(setCurrentUser({ accessToken, user: data }))
} else {
alert(`An error was encountered:\n${JSON.stringify(message)}`)
}
}
}
Expand All @@ -134,56 +158,122 @@ export function createOrUpdateUser (userData) {
*/
export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSuccess) {
return async function (dispatch, getState) {
const { otp, user } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const { id } = tripData
let requestUrl, method

if (otpMiddleware) {
const { accessToken } = user
// Determine URL and method to use.
if (isNew) {
requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`
method = 'POST'
} else {
requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`
method = 'PUT'
}

let result
if (isNew) {
result = await addTrip(otpMiddleware, accessToken, tripData)
} else {
result = await updateTrip(otpMiddleware, accessToken, tripData)
const { data, message, status } = await secureFetch(requestUrl, accessToken, apiKey, method, {
body: JSON.stringify(tripData)
})

// TODO: improve the UI feedback messages for this.
if (status === 'success' && data) {
if (!silentOnSuccess) {
alert('Your preferences have been saved.')
}

// TODO: improve the UI feedback messages for this.
if (result.status === 'success' && result.data) {
if (!silentOnSuccess) {
alert('Your preferences have been saved.')
}
// Reload user's monitored trips after add/update.
await dispatch(fetchUserMonitoredTrips(accessToken))

// Finally, navigate to the saved trips page.
dispatch(routeTo('/savedtrips'))
} else {
alert(`An error was encountered:\n${JSON.stringify(message)}`)
}
}
}

// Reload user's monitored trips after add/update.
await dispatch(fetchUserMonitoredTrips(accessToken))
/**
* Deletes a logged-in user's monitored trip,
* then, if that was successful, refreshes the redux monitoredTrips state.
*/
export function deleteUserMonitoredTrip (tripId) {
return async function (dispatch, getState) {
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${tripId}`

// Finally, navigate to the saved trips page.
dispatch(routeTo('/savedtrips'))
const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'DELETE')
if (status === 'success') {
// Reload user's monitored trips after deletion.
await dispatch(fetchUserMonitoredTrips(accessToken))
} else {
alert(`An error was encountered:\n${JSON.stringify(message)}`)
}
}
}

/**
* Requests a verification code via SMS for the logged-in user.
*/
export function requestPhoneVerificationSms (newPhoneNumber) {
return async function (dispatch, getState) {
const state = getState()
const { number, timestamp } = state.user.lastPhoneSmsRequest
const now = new Date()

// Request a new verification code if we are requesting a different number.
// or enough time has ellapsed since the last request (1 minute?).
// TODO: Should throttling be handled in the middleware?
if (number !== newPhoneNumber || (now - timestamp) >= 60000) {
const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(state)
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${encodeURIComponent(newPhoneNumber)}`

const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')

dispatch(setLastPhoneSmsRequest({
number: newPhoneNumber,
status,
timestamp: now
}))

if (status === 'success') {
// Refetch user and update application state with new phone number and verification status.
// (This also refetches the user's monitored trip, and that's ok.)
await dispatch(fetchOrInitializeUser({ accessToken }))
} else {
alert(`An error was encountered:\n${JSON.stringify(result)}`)
alert(`An error was encountered:\n${JSON.stringify(message)}`)
}
} else {
// Alert user if they have been throttled.
// TODO: improve this alert.
alert('A verification SMS was sent to the indicated phone number less than a minute ago. Please try again in a few moments.')
binh-dam-ibigroup marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

/**
* Deletes a logged-in user's monitored trip,
* then, if that was successful, refreshes the redux monitoredTrips state.
* Validate the phone number verification code for the logged-in user.
*/
export function deleteUserMonitoredTrip (id) {
export function verifyPhoneNumber (code) {
return async function (dispatch, getState) {
const { otp, user } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${code}`

if (otpMiddleware) {
const { accessToken } = user
const deleteResult = await deleteTrip(otpMiddleware, accessToken, id)
const { data, status } = await secureFetch(requestUrl, accessToken, apiKey, 'POST')

if (deleteResult.status === 'success') {
// Reload user's monitored trips after deletion.
await dispatch(fetchUserMonitoredTrips(accessToken))
// If the check is successful, status in the returned data will be "approved".
if (status === 'success' && data) {
if (data.status === 'approved') {
// Refetch user and update application state with new phone number and verification status.
// (This also refetches the user's monitored trip, and that's ok.)
dispatch(fetchOrInitializeUser({ accessToken }))
} else {
alert(`An error was encountered:\n${JSON.stringify(deleteResult)}`)
// Otherwise, the user entered a wrong/incorrect code.
alert('The code you entered is invalid. Please try again.')
}
} else {
// This happens when an error occurs on backend side, especially
// when the code has expired (or was cancelled by Twilio after too many attempts).
alert(`Your phone could not be verified. Perhaps the code you entered has expired. Please request a new code and try again.`)
}
}
}
Loading