Skip to content

Commit cf221f6

Browse files
authored
Merge pull request #308 from opentripplanner/refactor-account-pages
Refactor user account pages
2 parents 1a6c5f3 + ce2e609 commit cf221f6

32 files changed

+720
-654
lines changed

Diff for: __tests__/components/viewers/__snapshots__/stop-viewer.js.snap

+65-65
Large diffs are not rendered by default.

Diff for: example.css

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.otp .navbar {
22
border-radius: 0;
33
background-color: #444;
4+
margin: 0px;
45
}
56

67
.otp .navbar .container {

Diff for: lib/actions/user.js

+29-13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createAction } from 'redux-actions'
88
import { routingQuery } from './api'
99
import { setQueryParam } from './form'
1010
import { routeTo } from './ui'
11+
import { TRIPS_PATH } from '../util/constants'
1112
import { secureFetch } from '../util/middleware'
1213
import { isNewUser } from '../util/user'
1314

@@ -16,6 +17,7 @@ const API_MONITORED_TRIP_PATH = '/api/secure/monitoredtrip'
1617
const API_OTPUSER_PATH = '/api/secure/user'
1718
const API_OTPUSER_VERIFY_SMS_SUBPATH = '/verify_sms'
1819

20+
const setAccessToken = createAction('SET_ACCESS_TOKEN')
1921
const setCurrentUser = createAction('SET_CURRENT_USER')
2022
const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS')
2123
const setLastPhoneSmsRequest = createAction('SET_LAST_PHONE_SMS_REQUEST')
@@ -67,14 +69,30 @@ export function fetchAuth0Token (auth0) {
6769
try {
6870
const accessToken = await auth0.getAccessTokenSilently()
6971

70-
dispatch(setCurrentUser({ accessToken }))
72+
dispatch(setAccessToken(accessToken))
7173
} catch (error) {
7274
// TODO: improve UI if there is an error.
7375
alert('Error obtaining an authorization token.')
7476
}
7577
}
7678
}
7779

80+
/**
81+
* Updates the redux state with the provided user data,
82+
* and also fetches monitored trips if requested, i.e. when
83+
* - initializing the user state with an existing persisted user, or
84+
* - POST-ing a user for the first time.
85+
*/
86+
function setUser (user, fetchTrips) {
87+
return function (dispatch, getState) {
88+
dispatch(setCurrentUser(user))
89+
90+
if (fetchTrips) {
91+
dispatch(fetchMonitoredTrips())
92+
}
93+
}
94+
}
95+
7896
/**
7997
* Attempts to fetch user preferences (or set initial values if the user is being created)
8098
* into the redux state, under state.user.
@@ -110,12 +128,9 @@ export function fetchOrInitializeUser (auth0User) {
110128

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

115-
// Also load monitored trips for existing users.
116-
if (!isNewAccount) {
117-
dispatch(fetchUserMonitoredTrips())
118-
}
132+
// Set uset in redux state. (Fetch trips for existing users.)
133+
dispatch(setUser(userData, !isNewAccount))
119134
}
120135
}
121136

@@ -132,7 +147,8 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) {
132147
let requestUrl, method
133148

134149
// Determine URL and method to use.
135-
if (isNewUser(loggedInUser)) {
150+
const isCreatingUser = isNewUser(loggedInUser)
151+
if (isCreatingUser) {
136152
requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}`
137153
method = 'POST'
138154
} else {
@@ -151,8 +167,8 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) {
151167
}
152168

153169
// Update application state with the user entry as saved
154-
// (as returned) by the middleware.
155-
dispatch(setCurrentUser({ accessToken, user: data }))
170+
// (as returned) by the middleware. (Fetch trips if creating user.)
171+
dispatch(setUser(data, isCreatingUser))
156172
} else {
157173
alert(`An error was encountered:\n${JSON.stringify(message)}`)
158174
}
@@ -182,7 +198,7 @@ export function resendVerificationEmail () {
182198
* We use the accessToken to fetch the data regardless of
183199
* whether the process to populate state.user is completed or not.
184200
*/
185-
export function fetchUserMonitoredTrips () {
201+
export function fetchMonitoredTrips () {
186202
return async function (dispatch, getState) {
187203
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
188204
const requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}`
@@ -230,12 +246,12 @@ export function createOrUpdateUserMonitoredTrip (
230246
}
231247

232248
// Reload user's monitored trips after add/update.
233-
await dispatch(fetchUserMonitoredTrips())
249+
await dispatch(fetchMonitoredTrips())
234250

235251
if (noRedirect) return
236252

237253
// Finally, navigate to the saved trips page.
238-
dispatch(routeTo('/savedtrips'))
254+
dispatch(routeTo(TRIPS_PATH))
239255
} else {
240256
alert(`An error was encountered:\n${JSON.stringify(message)}`)
241257
}
@@ -281,7 +297,7 @@ export function confirmAndDeleteUserMonitoredTrip (tripId) {
281297
const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'DELETE')
282298
if (status === 'success') {
283299
// Reload user's monitored trips after deletion.
284-
dispatch(fetchUserMonitoredTrips())
300+
dispatch(fetchMonitoredTrips())
285301
} else {
286302
alert(`An error was encountered:\n${JSON.stringify(message)}`)
287303
}

Diff for: lib/components/app/call-taker-panel.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ class CallTakerAdvancedOptions extends Component {
373373

374374
static contextType = ComponentContext
375375

376-
componentWillMount () {
376+
componentDidMount () {
377377
// Fetch routes for banned/preferred routes selectors.
378378
this.props.findRoutes()
379379
}

Diff for: lib/components/app/responsive-webapp.js

+31-52
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import coreUtils from '@opentripplanner/core-utils'
66
import PropTypes from 'prop-types'
77
import React, { Component } from 'react'
88
import { connect } from 'react-redux'
9-
import { Route, Switch, withRouter } from 'react-router'
9+
import { Redirect, Route, Switch, withRouter } from 'react-router'
1010

1111
import PrintLayout from './print-layout'
1212
import * as authActions from '../../actions/auth'
@@ -17,7 +17,15 @@ import * as locationActions from '../../actions/location'
1717
import * as mapActions from '../../actions/map'
1818
import * as uiActions from '../../actions/ui'
1919
import { getAuth0Config } from '../../util/auth'
20-
import { AUTH0_AUDIENCE, AUTH0_SCOPE, URL_ROOT } from '../../util/constants'
20+
import {
21+
ACCOUNT_PATH,
22+
AUTH0_AUDIENCE,
23+
AUTH0_SCOPE,
24+
ACCOUNT_SETTINGS_PATH,
25+
CREATE_ACCOUNT_PATH,
26+
TRIPS_PATH,
27+
URL_ROOT
28+
} from '../../util/constants'
2129
import { ComponentContext } from '../../util/contexts'
2230
import { getActiveItinerary, getTitle } from '../../util/state'
2331
import AfterSignInScreen from '../user/after-signin-screen'
@@ -137,12 +145,8 @@ class ResponsiveWebapp extends Component {
137145
}
138146

139147
render () {
140-
const { components, desktopView, mobileView } = this.props
141-
return (
142-
<ComponentContext.Provider value={components}>
143-
{isMobile() ? mobileView : desktopView}
144-
</ComponentContext.Provider>
145-
)
148+
const { desktopView, mobileView } = this.props
149+
return isMobile() ? mobileView : desktopView
146150
}
147151
}
148152

@@ -191,30 +195,21 @@ const WebappWithRouter = withRouter(
191195
* so that Auth0 services are available everywhere.
192196
*/
193197
class RouterWrapperWithAuth0 extends Component {
194-
/**
195-
* Combine the router props with the other props that get
196-
* passed to the exported component. This way, it is possible for
197-
* the PrintLayout, UserAccountScreen, and other components to
198-
* receive the LegIcon or other needed props.
199-
*/
200-
_combineProps = routerProps => {
201-
return { ...this.props, ...routerProps }
202-
}
203-
204198
render () {
205199
const {
206200
auth0Config,
201+
components,
207202
processSignIn,
208203
routerConfig,
209204
showAccessTokenError,
210205
showLoginError
211206
} = this.props
212207

213208
const router = (
214-
<ConnectedRouter
215-
basename={routerConfig && routerConfig.basename}
216-
history={history}>
217-
<div>
209+
<ComponentContext.Provider value={components}>
210+
<ConnectedRouter
211+
basename={routerConfig && routerConfig.basename}
212+
history={history}>
218213
<Switch>
219214
<Route
220215
exact
@@ -237,56 +232,40 @@ class RouterWrapperWithAuth0 extends Component {
237232
render={() => <WebappWithRouter {...this.props} />}
238233
/>
239234
<Route
240-
// This route lets new or existing users edit or set up their account.
241-
path={'/account'}
242-
component={routerProps => {
243-
const props = this._combineProps(routerProps)
244-
return <UserAccountScreen {...props} />
245-
}}
246-
/>
247-
<Route
248-
path={'/savetrip'}
249-
component={(routerProps) => {
250-
const props = this._combineProps(routerProps)
251-
return <SavedTripScreen isCreating {...props} />
252-
}}
235+
component={SavedTripScreen}
236+
path={`${TRIPS_PATH}/:id`}
253237
/>
238+
<Route exact path={ACCOUNT_PATH}>
239+
<Redirect to={TRIPS_PATH} />
240+
</Route>
254241
<Route
255-
path={'/savedtrips/:id'}
256-
component={(routerProps) => {
257-
const props = this._combineProps(routerProps)
258-
return <SavedTripScreen {...props} />
259-
}}
242+
// This route lets new or existing users edit or set up their account.
243+
component={UserAccountScreen}
244+
path={[CREATE_ACCOUNT_PATH, ACCOUNT_SETTINGS_PATH]}
260245
/>
261246
<Route
262-
path={'/savedtrips'}
263247
component={SavedTripList}
248+
path={TRIPS_PATH}
264249
/>
265250
<Route
266251
// This route is called immediately after login by Auth0
267252
// and by the onRedirectCallback function from /lib/util/auth.js.
268253
// For new users, it displays the account setup form.
269254
// For existing users, it takes the browser back to the itinerary search prior to login.
270-
path={'/signedin'}
271-
component={routerProps => {
272-
const props = this._combineProps(routerProps)
273-
return <AfterSignInScreen {...props} />
274-
}}
255+
component={AfterSignInScreen}
256+
path='/signedin'
275257
/>
276258
<Route
259+
component={PrintLayout}
277260
path='/print'
278-
component={routerProps => {
279-
const props = this._combineProps(routerProps)
280-
return <PrintLayout {...props} />
281-
}}
282261
/>
283262
{/* For any other route, simply return the web app. */}
284263
<Route
285264
render={() => <WebappWithRouter {...this.props} />}
286265
/>
287266
</Switch>
288-
</div>
289-
</ConnectedRouter>
267+
</ConnectedRouter>
268+
</ComponentContext.Provider>
290269
)
291270

292271
return (

Diff for: lib/components/form/date-time-preview.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class DateTimePreview extends Component {
1919
date: PropTypes.string,
2020
departArrive: PropTypes.string,
2121
editButtonText: PropTypes.element,
22-
hideButton: PropTypes.boolean,
22+
hideButton: PropTypes.bool,
2323
onClick: PropTypes.func,
2424
routingType: PropTypes.string,
2525
time: PropTypes.string

Diff for: lib/components/narrative/save-trip-button.js

+28-20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react'
22
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
33
import { connect } from 'react-redux'
4+
import { LinkContainer } from 'react-router-bootstrap'
45

5-
import LinkButton from '../user/link-button'
6+
import { CREATE_TRIP_PATH } from '../../util/constants'
67
import { itineraryCanBeMonitored } from '../../util/itinerary'
78
import { getActiveItinerary } from '../../util/state'
89

@@ -13,7 +14,8 @@ import { getActiveItinerary } from '../../util/state'
1314
const SaveTripButton = ({
1415
itinerary,
1516
loggedInUser,
16-
persistence
17+
persistence,
18+
queryParams
1719
}) => {
1820
// We are dealing with the following states:
1921
// 1. Persistence disabled => just return null
@@ -41,30 +43,35 @@ const SaveTripButton = ({
4143
buttonText = 'Save trip'
4244
icon = 'fa fa-plus-circle'
4345
}
44-
46+
const button = (
47+
<button
48+
// Apply pull-right style class so that the element is flush right,
49+
// even with LineItineraries.
50+
className='clear-button-formatting pull-right'
51+
disabled={buttonDisabled}
52+
style={buttonDisabled ? { pointerEvents: 'none' } : {}}
53+
>
54+
<i className={icon} /> {buttonText}
55+
</button>
56+
)
57+
// Show tooltip with help text if button is disabled.
4558
if (buttonDisabled) {
46-
const tooltip = <Tooltip id='disabled-save-tooltip'>{tooltipText}</Tooltip>
47-
4859
return (
49-
// Apply disabled bootstrap button styles to a non-input element
50-
// to keep Tooltip and OverlayTrigger functional.
51-
// Also apply pull-right style class so that the element is flush right, even with LineItineraries.
52-
<OverlayTrigger placement='top' overlay={tooltip}>
53-
<span className='btn disabled clear-button-formatting pull-right'>
54-
<i className={icon} /> {buttonText}
55-
</span>
60+
<OverlayTrigger
61+
overlay={<Tooltip id='disabled-save-tooltip'>{tooltipText}</Tooltip>}
62+
placement='top'
63+
>
64+
<div style={{ cursor: 'not-allowed' }}>
65+
{button}
66+
</div>
5667
</OverlayTrigger>
5768
)
5869
}
5970

6071
return (
61-
<LinkButton
62-
// Also apply pull-right style class so that the element is flush right, even with LineItineraries.
63-
className='clear-button-formatting pull-right'
64-
to='/savetrip'
65-
>
66-
<i className={icon} /> {buttonText}
67-
</LinkButton>
72+
<LinkContainer to={{ pathname: CREATE_TRIP_PATH, search: queryParams }}>
73+
{button}
74+
</LinkContainer>
6875
)
6976
}
7077

@@ -75,7 +82,8 @@ const mapStateToProps = (state, ownProps) => {
7582
return {
7683
itinerary: getActiveItinerary(state.otp),
7784
loggedInUser: state.user.loggedInUser,
78-
persistence
85+
persistence,
86+
queryParams: state.router.location.search
7987
}
8088
}
8189

0 commit comments

Comments
 (0)