From e412ed853e8a958948e8605b27d7f76e02bcd564 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Mon, 9 Oct 2023 16:48:29 -0400
Subject: [PATCH 01/21] refactor(stacked-panes): Separate layout code from
save/cancel buttons.
---
.../user/existing-account-display.tsx | 32 ++++++++----------
.../user/monitored-trip/saved-trip-editor.tsx | 6 ++--
...isplay.tsx => stacked-panes-with-save.tsx} | 27 ++++-----------
lib/components/user/stacked-panes.tsx | 33 +++++++++++++++++++
4 files changed, 56 insertions(+), 42 deletions(-)
rename lib/components/user/{stacked-pane-display.tsx => stacked-panes-with-save.tsx} (72%)
create mode 100644 lib/components/user/stacked-panes.tsx
diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx
index c253b2234..15e3aad92 100644
--- a/lib/components/user/existing-account-display.tsx
+++ b/lib/components/user/existing-account-display.tsx
@@ -2,6 +2,8 @@ import { connect } from 'react-redux'
import { FormattedMessage, useIntl } from 'react-intl'
import React from 'react'
+import { AppReduxState } from '../../util/state-types'
+import { TransitModeConfig } from '../../util/config-types'
import PageTitle from '../util/page-title'
import A11yPrefs from './a11y-prefs'
@@ -9,22 +11,19 @@ import BackToTripPlanner from './back-to-trip-planner'
import DeleteUser from './delete-user'
import FavoritePlaceList from './places/favorite-place-list'
import NotificationPrefsPane from './notification-prefs-pane'
-import StackedPaneDisplay from './stacked-pane-display'
+import StackedPanes from './stacked-panes'
import TermsOfUsePane from './terms-of-use-pane'
/**
* This component handles the existing account display.
*/
-const ExistingAccountDisplay = (props: {
- onCancel: () => void
- wheelchairEnabled: boolean
-}) => {
+const ExistingAccountDisplay = (props: { wheelchairEnabled: boolean }) => {
// The props include Formik props that provide access to the current user data
// and to its own blur/change/submit event handlers that automate the state.
// We forward the props to each pane so that their individual controls
// can be wired to be managed by Formik.
- const { onCancel, wheelchairEnabled } = props
- const paneSequence = [
+ const { wheelchairEnabled } = props
+ const panes = [
{
pane: FavoritePlaceList,
props,
@@ -67,9 +66,8 @@ const ExistingAccountDisplay = (props: {
-
}
@@ -77,15 +75,11 @@ const ExistingAccountDisplay = (props: {
)
}
-// TODO: state type
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const mapStateToProps = (state: any) => {
- const { accessModes } = state.otp.config?.modes
- const wheelchairEnabled =
- accessModes &&
- accessModes.some(
- (mode: { showWheelchairSetting: boolean }) => mode.showWheelchairSetting
- )
+const mapStateToProps = (state: AppReduxState) => {
+ const { accessModes } = state.otp.config.modes
+ const wheelchairEnabled = accessModes?.some(
+ (mode: TransitModeConfig) => mode.showWheelchairSetting
+ )
return {
wheelchairEnabled
}
diff --git a/lib/components/user/monitored-trip/saved-trip-editor.tsx b/lib/components/user/monitored-trip/saved-trip-editor.tsx
index 83d1af185..2dbd00a1c 100644
--- a/lib/components/user/monitored-trip/saved-trip-editor.tsx
+++ b/lib/components/user/monitored-trip/saved-trip-editor.tsx
@@ -3,7 +3,7 @@ import React, { ComponentType } from 'react'
import BackLink from '../back-link'
import PageTitle from '../../util/page-title'
-import StackedPaneDisplay from '../stacked-pane-display'
+import StackedPanesWithSave from '../stacked-panes-with-save'
interface Props {
isCreating: boolean
@@ -50,9 +50,9 @@ const SavedTripEditor = (props: Props): JSX.Element => {
<>
-
>
diff --git a/lib/components/user/stacked-pane-display.tsx b/lib/components/user/stacked-panes-with-save.tsx
similarity index 72%
rename from lib/components/user/stacked-pane-display.tsx
rename to lib/components/user/stacked-panes-with-save.tsx
index 2d27bbe75..b7970ff01 100644
--- a/lib/components/user/stacked-pane-display.tsx
+++ b/lib/components/user/stacked-panes-with-save.tsx
@@ -3,13 +3,11 @@ import React, { useEffect, useState } from 'react'
import { InlineLoading } from '../narrative/loading'
-import { PageHeading, StackedPaneContainer } from './styled'
import FormNavigationButtons from './form-navigation-buttons'
+import StackedPanes, { Props as StackedPanesProps } from './stacked-panes'
-type Props = {
+interface Props extends StackedPanesProps {
onCancel: () => void
- paneSequence: any[]
- title?: string | JSX.Element
}
/**
@@ -17,9 +15,9 @@ type Props = {
*
* TODO: add types once Pane type exists
*/
-const StackedPaneDisplay = ({
+const StackedPanesWithSave = ({
onCancel,
- paneSequence,
+ panes,
title
}: Props): JSX.Element => {
// Create indicator of if cancel button was clicked so that child components can know
@@ -28,22 +26,11 @@ const StackedPaneDisplay = ({
useEffect(() => {
setButtonClicked('')
- }, [paneSequence])
+ }, [panes])
return (
<>
- {title && {title}}
- {paneSequence.map(
- ({ hidden, pane: Pane, props, title }, index) =>
- !hidden && (
-
- {title}
-
-
- )
- )}
+
)
}
-export default StackedPaneDisplay
+export default StackedPanesWithSave
diff --git a/lib/components/user/stacked-panes.tsx b/lib/components/user/stacked-panes.tsx
new file mode 100644
index 000000000..c9fd84c07
--- /dev/null
+++ b/lib/components/user/stacked-panes.tsx
@@ -0,0 +1,33 @@
+import React from 'react'
+
+import { PageHeading, StackedPaneContainer } from './styled'
+
+export interface Props {
+ canceling?: boolean
+ panes: any[]
+ title: string | JSX.Element
+}
+
+/**
+ * This component handles the flow between screens for new OTP user accounts.
+ *
+ * TODO: add types once Pane type exists
+ */
+const StackedPanes = ({ canceling, panes, title }: Props): JSX.Element => (
+ <>
+ {title}
+ {panes.map(
+ ({ hidden, pane: Pane, props, title }, index) =>
+ !hidden && (
+
+ {title}
+
+
+ )
+ )}
+ >
+)
+
+export default StackedPanes
From c93a6ca599b809701c78c639d427aa17dc854414 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 11 Oct 2023 11:38:48 -0400
Subject: [PATCH 02/21] refactor(stacked-panes): Add more types to component.
---
lib/components/user/stacked-panes.tsx | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/lib/components/user/stacked-panes.tsx b/lib/components/user/stacked-panes.tsx
index c9fd84c07..e555cd507 100644
--- a/lib/components/user/stacked-panes.tsx
+++ b/lib/components/user/stacked-panes.tsx
@@ -2,16 +2,21 @@ import React from 'react'
import { PageHeading, StackedPaneContainer } from './styled'
+export interface PaneAttributes {
+ hidden?: boolean
+ pane: React.ElementType
+ props?: any
+ title?: string | JSX.Element
+}
+
export interface Props {
canceling?: boolean
- panes: any[]
+ panes: PaneAttributes[]
title: string | JSX.Element
}
/**
- * This component handles the flow between screens for new OTP user accounts.
- *
- * TODO: add types once Pane type exists
+ * Stacked layout of panes, each supporting a title and a cancel state.
*/
const StackedPanes = ({ canceling, panes, title }: Props): JSX.Element => (
<>
@@ -20,7 +25,7 @@ const StackedPanes = ({ canceling, panes, title }: Props): JSX.Element => (
({ hidden, pane: Pane, props, title }, index) =>
!hidden && (
- {title}
+ {title && {title}
}
From 2874d675688a0dd7b5b574b394cd16d27c0a8662 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 11 Oct 2023 17:09:31 -0400
Subject: [PATCH 03/21] refactor(existing-account-display): Submit changes
right away.
---
.../user/existing-account-display.tsx | 19 ++++++++++++++++---
lib/components/user/user-account-screen.js | 4 +++-
2 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx
index 15e3aad92..ed090daa5 100644
--- a/lib/components/user/existing-account-display.tsx
+++ b/lib/components/user/existing-account-display.tsx
@@ -1,6 +1,6 @@
import { connect } from 'react-redux'
import { FormattedMessage, useIntl } from 'react-intl'
-import React from 'react'
+import React, { useCallback } from 'react'
import { AppReduxState } from '../../util/state-types'
import { TransitModeConfig } from '../../util/config-types'
@@ -17,12 +17,25 @@ import TermsOfUsePane from './terms-of-use-pane'
/**
* This component handles the existing account display.
*/
-const ExistingAccountDisplay = (props: { wheelchairEnabled: boolean }) => {
+const ExistingAccountDisplay = (parentProps: {
+ wheelchairEnabled: boolean
+}) => {
// The props include Formik props that provide access to the current user data
// and to its own blur/change/submit event handlers that automate the state.
// We forward the props to each pane so that their individual controls
// can be wired to be managed by Formik.
- const { wheelchairEnabled } = props
+ const { handleChange, submitForm, wheelchairEnabled } = parentProps
+ const props = {
+ ...parentProps,
+ handleChange: useCallback(
+ (e) => {
+ // Apply changes and submit the form right away to update the user profile.
+ handleChange(e)
+ submitForm()
+ },
+ [handleChange, submitForm]
+ )
+ }
const panes = [
{
pane: FavoritePlaceList,
diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js
index b99cbf794..93d3d60c3 100644
--- a/lib/components/user/user-account-screen.js
+++ b/lib/components/user/user-account-screen.js
@@ -128,7 +128,9 @@ class UserAccountScreen extends Component {
// Force Formik to reload initialValues when we update them (e.g. user gets assigned an id).
enableReinitialize
initialValues={loggedInUserWithNotificationArray}
- onSubmit={this._handleSaveAndExit}
+ onSubmit={
+ isCreating ? this._handleSaveAndExit : this._updateUserPrefs
+ }
// Avoid validating on change as it is annoying. Validating on blur is enough.
validateOnBlur
validateOnChange={false}
From ae94f94d443cc03867961b6526c3f77709c2171b Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 11 Oct 2023 17:25:31 -0400
Subject: [PATCH 04/21] refactor(existing-account-display): Fix types
---
lib/components/user/existing-account-display.tsx | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx
index ed090daa5..e94da81f5 100644
--- a/lib/components/user/existing-account-display.tsx
+++ b/lib/components/user/existing-account-display.tsx
@@ -1,11 +1,13 @@
import { connect } from 'react-redux'
import { FormattedMessage, useIntl } from 'react-intl'
+import { FormikProps } from 'formik'
import React, { useCallback } from 'react'
import { AppReduxState } from '../../util/state-types'
import { TransitModeConfig } from '../../util/config-types'
import PageTitle from '../util/page-title'
+import { User } from './types'
import A11yPrefs from './a11y-prefs'
import BackToTripPlanner from './back-to-trip-planner'
import DeleteUser from './delete-user'
@@ -14,12 +16,14 @@ import NotificationPrefsPane from './notification-prefs-pane'
import StackedPanes from './stacked-panes'
import TermsOfUsePane from './terms-of-use-pane'
+interface Props extends FormikProps {
+ wheelchairEnabled: boolean
+}
+
/**
* This component handles the existing account display.
*/
-const ExistingAccountDisplay = (parentProps: {
- wheelchairEnabled: boolean
-}) => {
+function ExistingAccountDisplay(parentProps: Props) {
// The props include Formik props that provide access to the current user data
// and to its own blur/change/submit event handlers that automate the state.
// We forward the props to each pane so that their individual controls
From 1af40ceae555c91b578ab62662b1649429d3385a Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 11 Oct 2023 17:40:21 -0400
Subject: [PATCH 05/21] refactor(notification-prefs-pane): Set onChange handler
explicitly.
---
lib/components/user/notification-prefs-pane.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/lib/components/user/notification-prefs-pane.tsx b/lib/components/user/notification-prefs-pane.tsx
index 67f3fb091..bca6a0ce5 100644
--- a/lib/components/user/notification-prefs-pane.tsx
+++ b/lib/components/user/notification-prefs-pane.tsx
@@ -56,6 +56,7 @@ const NotificationOption = styled(ListGroupItem)`
*/
const NotificationPrefsPane = ({
allowedNotificationChannels,
+ handleChange,
onRequestPhoneVerificationCode,
onSendPhoneVerificationCode,
phoneFormatOptions,
@@ -70,8 +71,6 @@ const NotificationPrefsPane = ({
{allowedNotificationChannels.map((type) => {
- // TODO: If removing the Save/Cancel buttons on the account screen,
- // persist changes immediately when onChange is triggered.
const inputId = `notification-channel-${type}`
const inputDescriptionId = `${inputId}-description`
return (
@@ -79,10 +78,12 @@ const NotificationPrefsPane = ({
From 263c94083a06e0eaf9cd8053cea433f84a6119c7 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 11 Oct 2023 17:55:45 -0400
Subject: [PATCH 06/21] refactor(notification-prefs-pane): Add missing i18n
---
i18n/en-US.yml | 1 +
i18n/fr.yml | 3 +++
lib/components/user/notification-prefs-pane.tsx | 10 +++++++---
3 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/i18n/en-US.yml b/i18n/en-US.yml
index 32c9b46f1..e768bf6cd 100644
--- a/i18n/en-US.yml
+++ b/i18n/en-US.yml
@@ -362,6 +362,7 @@ components:
description: The content you requested is not available.
header: Content not found
NotificationPrefsPane:
+ devicesRegistered: "{count, plural, one {# device} other {# devices}} registered"
noDeviceForPush: Register your device using the mobile app to access push notifications.
notificationChannelPrompt: "Receive notifications about your saved trips via:"
OTP2ErrorRenderer:
diff --git a/i18n/fr.yml b/i18n/fr.yml
index 6e7c7d6ba..2ef6b2967 100644
--- a/i18n/fr.yml
+++ b/i18n/fr.yml
@@ -375,6 +375,9 @@ components:
description: Le contenu que vous avez demandé n'est pas disponible.
header: Contenu introuvable
NotificationPrefsPane:
+ devicesRegistered: >-
+ {count, plural, one {# appareil enregistré} other {# appareils
+ enregistrés}}
noDeviceForPush: Inscrivez-vous avec l'application mobile pour accéder à ce paramètre.
notificationChannelPrompt: "Recevoir des notifications sur vos trajets par :"
OTP2ErrorRenderer:
diff --git a/lib/components/user/notification-prefs-pane.tsx b/lib/components/user/notification-prefs-pane.tsx
index bca6a0ce5..12eeee8f9 100644
--- a/lib/components/user/notification-prefs-pane.tsx
+++ b/lib/components/user/notification-prefs-pane.tsx
@@ -56,7 +56,7 @@ const NotificationOption = styled(ListGroupItem)`
*/
const NotificationPrefsPane = ({
allowedNotificationChannels,
- handleChange,
+ handleChange, // Formik or custom handler
onRequestPhoneVerificationCode,
onSendPhoneVerificationCode,
phoneFormatOptions,
@@ -106,8 +106,12 @@ const NotificationPrefsPane = ({
) : (
{pushDevices ? (
- // TODO: i18n
- `${pushDevices} devices registered`
+
) : (
)}
From 9a5ffc7f1a88269c25eba75bf67218e1ea3e3e5a Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 11 Oct 2023 18:16:10 -0400
Subject: [PATCH 07/21] refactor(user-account-screen): Remove form element when
viewing existing account.
---
lib/components/user/new-account-wizard.tsx | 10 +++----
lib/components/user/user-account-screen.js | 34 ++++++++++------------
2 files changed, 21 insertions(+), 23 deletions(-)
diff --git a/lib/components/user/new-account-wizard.tsx b/lib/components/user/new-account-wizard.tsx
index 8676f5e9c..9cc45c7b9 100644
--- a/lib/components/user/new-account-wizard.tsx
+++ b/lib/components/user/new-account-wizard.tsx
@@ -1,5 +1,5 @@
+import { Form, FormikProps } from 'formik'
import { FormattedMessage, useIntl } from 'react-intl'
-import { FormikProps } from 'formik'
import React, { useCallback } from 'react'
import PageTitle from '../util/page-title'
@@ -46,11 +46,11 @@ const NewAccountWizard = ({
id: 'components.NewAccountWizard.verify'
})
return (
- <>
+
)
}
@@ -93,14 +93,14 @@ const NewAccountWizard = ({
]
return (
- <>
+
)
}
diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js
index 93d3d60c3..1bc6d6a98 100644
--- a/lib/components/user/user-account-screen.js
+++ b/lib/components/user/user-account-screen.js
@@ -2,7 +2,7 @@
/* eslint-disable react/prop-types */
import * as yup from 'yup'
import { connect } from 'react-redux'
-import { Form, Formik } from 'formik'
+import { Formik } from 'formik'
import { injectIntl } from 'react-intl'
import { withAuthenticationRequired } from '@auth0/auth0-react'
import clone from 'clone'
@@ -143,23 +143,21 @@ class UserAccountScreen extends Component {
// We pass the Formik props below to the components rendered so that individual controls
// can be wired to be managed by Formik.
(formikProps) => (
-
+
)
}
From 06b9bc6bba3559f03c701c97209a6665a7168444 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 11 Oct 2023 18:23:01 -0400
Subject: [PATCH 08/21] refactor(user-account-screen): Remove unused
validation.
---
lib/components/user/user-account-screen.js | 24 ----------------------
1 file changed, 24 deletions(-)
diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js
index 1bc6d6a98..f04831841 100644
--- a/lib/components/user/user-account-screen.js
+++ b/lib/components/user/user-account-screen.js
@@ -1,6 +1,5 @@
// TODO: typescript
/* eslint-disable react/prop-types */
-import * as yup from 'yup'
import { connect } from 'react-redux'
import { Formik } from 'formik'
import { injectIntl } from 'react-intl'
@@ -19,25 +18,6 @@ import ExistingAccountDisplay from './existing-account-display'
import NewAccountWizard from './new-account-wizard'
import withLoggedInUserSupport from './with-logged-in-user-support'
-// The validation schema for the form fields.
-// FIXME: validationSchema is not really directly used, so the text below is never shown.
-// Also, this may be removed depending on fate of the Save button on this screen.
-const validationSchema = yup.object({
- accessibilityRoutingByDefault: yup.boolean(),
- email: yup.string().email(),
- hasConsentedToTerms: yup
- .boolean()
- .oneOf([true], 'You must agree to the terms to continue.'),
- savedLocations: yup.array().of(
- yup.object({
- address: yup.string(),
- icon: yup.string(),
- type: yup.string()
- })
- ),
- storeTripHistory: yup.boolean()
-})
-
/**
* This screen handles creating/updating OTP user account settings.
*/
@@ -131,10 +111,6 @@ class UserAccountScreen extends Component {
onSubmit={
isCreating ? this._handleSaveAndExit : this._updateUserPrefs
}
- // Avoid validating on change as it is annoying. Validating on blur is enough.
- validateOnBlur
- validateOnChange={false}
- validationSchema={validationSchema}
>
{
// Formik props provide access to the current user data state and errors,
From 9783e83a1dc961039c094900a2305ed3c3df918c Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Thu, 12 Oct 2023 12:43:11 -0400
Subject: [PATCH 09/21] improvement(existing-account-display): Display toast to
confirm field change.
---
i18n/en-US.yml | 3 ++
i18n/fr.yml | 3 ++
.../user/existing-account-display.tsx | 28 +++++++++++++++----
lib/components/user/user-account-screen.js | 16 ++++++++++-
lib/components/util/toasts.tsx | 26 +++++++++++------
5 files changed, 62 insertions(+), 14 deletions(-)
diff --git a/i18n/en-US.yml b/i18n/en-US.yml
index e768bf6cd..e209b325c 100644
--- a/i18n/en-US.yml
+++ b/i18n/en-US.yml
@@ -274,6 +274,9 @@ components:
warning: Warning
ExistingAccountDisplay:
a11y: Accessibility
+ fieldUpdated: This setting has been updated.
+ fields:
+ storeTripHistory: Store trip history
mainTitle: My settings
notifications: Notifications
places: Favorite places
diff --git a/i18n/fr.yml b/i18n/fr.yml
index 2ef6b2967..09c40b89b 100644
--- a/i18n/fr.yml
+++ b/i18n/fr.yml
@@ -287,6 +287,9 @@ components:
warning: Attention
ExistingAccountDisplay:
a11y: Accessibilité
+ fieldUpdated: Ce paramètre a été mis à jour.
+ fields:
+ storeTripHistory: Enregistrement des recherches
mainTitle: Mes préférences
notifications: Notifications
places: Lieux favoris
diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx
index e94da81f5..5a875cb03 100644
--- a/lib/components/user/existing-account-display.tsx
+++ b/lib/components/user/existing-account-display.tsx
@@ -1,9 +1,10 @@
import { connect } from 'react-redux'
import { FormattedMessage, useIntl } from 'react-intl'
import { FormikProps } from 'formik'
-import React, { useCallback } from 'react'
+import React, { FormEventHandler, useCallback } from 'react'
import { AppReduxState } from '../../util/state-types'
+import { toastSuccess } from '../util/toasts'
import { TransitModeConfig } from '../../util/config-types'
import PageTitle from '../util/page-title'
@@ -29,15 +30,32 @@ function ExistingAccountDisplay(parentProps: Props) {
// We forward the props to each pane so that their individual controls
// can be wired to be managed by Formik.
const { handleChange, submitForm, wheelchairEnabled } = parentProps
+ const intl = useIntl()
const props = {
...parentProps,
handleChange: useCallback(
- (e) => {
+ async (e) => {
// Apply changes and submit the form right away to update the user profile.
handleChange(e)
- submitForm()
+ try {
+ await submitForm()
+ // Display a toast notification on success.
+ toastSuccess(
+ intl.formatMessage({
+ // Use a summary text for the field, if defined (e.g. to replace long labels),
+ // otherwise, fall back on the first label of the input.
+ defaultMessage: e.target.labels[0]?.innerText,
+ id: `components.ExistingAccountDisplay.fields.${e.target.name}`
+ }),
+ intl.formatMessage({
+ id: 'components.ExistingAccountDisplay.fieldUpdated'
+ })
+ )
+ } catch {
+ alert('Error updating profile')
+ }
},
- [handleChange, submitForm]
+ [intl, handleChange, submitForm]
)
}
const panes = [
@@ -71,7 +89,6 @@ function ExistingAccountDisplay(parentProps: Props) {
}
]
- const intl = useIntl()
// Repeat text from the SubNav component in the title bar for brevity.
const settings = intl.formatMessage({
id: 'components.SubNav.settings'
@@ -92,6 +109,7 @@ function ExistingAccountDisplay(parentProps: Props) {
)
}
+
const mapStateToProps = (state: AppReduxState) => {
const { accessModes } = state.otp.config.modes
const wheelchairEnabled = accessModes?.some(
diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js
index f04831841..986c5dc85 100644
--- a/lib/components/user/user-account-screen.js
+++ b/lib/components/user/user-account-screen.js
@@ -40,6 +40,8 @@ class UserAccountScreen extends Component {
if (result === userActions.UserActionResult.SUCCESS && !silentOnSucceed) {
toast.success(intl.formatMessage({ id: 'actions.user.preferencesSaved' }))
}
+
+ return result
}
/**
@@ -88,6 +90,18 @@ class UserAccountScreen extends Component {
this._handleExit()
}
+ /**
+ * Persist changes immediately (for existing account display)
+ * @param {*} userData The user edited state to be saved, provided by Formik.
+ */
+ _handleFieldChange = async (userData) => {
+ // Turn off the default toast, so we can display a toast per field.
+ const result = await this._updateUserPrefs(userData, true)
+ if (result !== userActions.UserActionResult.SUCCESS) {
+ throw new Error('User update failed.')
+ }
+ }
+
_handleSendPhoneVerificationCode = async ({ validationCode: code }) => {
const { intl, verifyPhoneNumber } = this.props
await verifyPhoneNumber(code, intl)
@@ -109,7 +123,7 @@ class UserAccountScreen extends Component {
enableReinitialize
initialValues={loggedInUserWithNotificationArray}
onSubmit={
- isCreating ? this._handleSaveAndExit : this._updateUserPrefs
+ isCreating ? this._handleSaveAndExit : this._handleFieldChange
}
>
{
diff --git a/lib/components/util/toasts.tsx b/lib/components/util/toasts.tsx
index e1a635453..9bf0ec12e 100644
--- a/lib/components/util/toasts.tsx
+++ b/lib/components/util/toasts.tsx
@@ -8,6 +8,19 @@ import { UserSavedLocation } from '../user/types'
// Note: the HTML for toasts is rendered outside of the IntlProvider context,
// so intl.formatMessage and others have to be used instead of tags.
+/**
+ * Helper for displaying formatted toasts.
+ */
+export function toastSuccess(title: string, description: string): void {
+ toast.success(
+
+ {title}
+
+ {description}
+
+ )
+}
+
/**
* Helper that will display a toast notification when a place is saved.
*/
@@ -15,13 +28,10 @@ export function toastOnPlaceSaved(
place: UserSavedLocation,
intl: IntlShape
): void {
- toast.success(
-
- {getPlaceMainText(place, intl)}
-
- {intl.formatMessage({
- id: 'actions.user.placeRemembered'
- })}
-
+ toastSuccess(
+ getPlaceMainText(place, intl),
+ intl.formatMessage({
+ id: 'actions.user.placeRemembered'
+ })
)
}
From 99049968a3160302d1c5efb0fb04b931b29b6a8c Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Thu, 12 Oct 2023 12:58:29 -0400
Subject: [PATCH 10/21] style(existing-account-display): Clean up code.
---
lib/components/user/existing-account-display.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx
index 5a875cb03..fe59a83d7 100644
--- a/lib/components/user/existing-account-display.tsx
+++ b/lib/components/user/existing-account-display.tsx
@@ -1,7 +1,7 @@
import { connect } from 'react-redux'
import { FormattedMessage, useIntl } from 'react-intl'
import { FormikProps } from 'formik'
-import React, { FormEventHandler, useCallback } from 'react'
+import React, { useCallback } from 'react'
import { AppReduxState } from '../../util/state-types'
import { toastSuccess } from '../util/toasts'
@@ -24,7 +24,7 @@ interface Props extends FormikProps {
/**
* This component handles the existing account display.
*/
-function ExistingAccountDisplay(parentProps: Props) {
+const ExistingAccountDisplay = (parentProps: Props) => {
// The props include Formik props that provide access to the current user data
// and to its own blur/change/submit event handlers that automate the state.
// We forward the props to each pane so that their individual controls
From 57a9b6849007db240e48b05fae84a0a5c6fec5d3 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Thu, 12 Oct 2023 16:02:31 -0400
Subject: [PATCH 11/21] refactor(user-account-screen): Convert to TypeScript
---
lib/actions/ui.js | 2 +-
...ount-screen.js => user-account-screen.tsx} | 73 ++++++++++++++-----
.../user/with-logged-in-user-support.js | 2 +-
3 files changed, 55 insertions(+), 22 deletions(-)
rename lib/components/user/{user-account-screen.js => user-account-screen.tsx} (73%)
diff --git a/lib/actions/ui.js b/lib/actions/ui.js
index 3b254fa61..75ad9b3fa 100644
--- a/lib/actions/ui.js
+++ b/lib/actions/ui.js
@@ -51,7 +51,7 @@ export const resetSessionTimeout = createAction('RESET_SESSION_TIMEOUT')
* that preserves the current search or, if
* replaceSearch is provided (including an empty string), replaces the search
* when routing to a new URL path.
- * @param {[type]} url path to route to
+ * @param {string} url path to route to
* @param {string} [replaceSearch] optional search string to replace current one
* @param {func} [routingMethod] the connected-react-router method to execute (defaults to push).
*/
diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.tsx
similarity index 73%
rename from lib/components/user/user-account-screen.js
rename to lib/components/user/user-account-screen.tsx
index 986c5dc85..1bdd1c6c7 100644
--- a/lib/components/user/user-account-screen.js
+++ b/lib/components/user/user-account-screen.tsx
@@ -1,11 +1,13 @@
-// TODO: typescript
-/* eslint-disable react/prop-types */
+import {
+ Auth0ContextInterface,
+ withAuthenticationRequired
+} from '@auth0/auth0-react'
import { connect } from 'react-redux'
import { Formik } from 'formik'
-import { injectIntl } from 'react-intl'
-import { withAuthenticationRequired } from '@auth0/auth0-react'
+import { injectIntl, IntlShape } from 'react-intl'
+import { RouteComponentProps } from 'react-router'
import clone from 'clone'
-import React, { Component } from 'react'
+import React, { Component, FormEvent } from 'react'
import toast from 'react-hot-toast'
import * as uiActions from '../../actions/ui'
@@ -13,28 +15,51 @@ import * as userActions from '../../actions/user'
import { CREATE_ACCOUNT_PATH } from '../../util/constants'
import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui'
+import { User } from './types'
import AccountPage from './account-page'
import ExistingAccountDisplay from './existing-account-display'
import NewAccountWizard from './new-account-wizard'
import withLoggedInUserSupport from './with-logged-in-user-support'
+interface Props {
+ auth0: Auth0ContextInterface
+ createOrUpdateUser: (user: User, intl: IntlShape) => Promise
+ deleteUser: (
+ user: User,
+ auth0: Auth0ContextInterface,
+ intl: IntlShape
+ ) => void
+ intl: IntlShape
+ isCreating: boolean
+ itemId: string
+ loggedInUser: User
+ requestPhoneVerificationSms: (phoneNum: string, intl: IntlShape) => void
+ routeTo: (to: string) => void
+ verifyPhoneNumber: (code: string, intl: IntlShape) => void
+}
+
+type EditedUser = Omit & {
+ notificationChannel?: string | string[]
+}
+
/**
* This screen handles creating/updating OTP user account settings.
*/
-class UserAccountScreen extends Component {
- _updateUserPrefs = async (userData, silentOnSucceed = false) => {
+class UserAccountScreen extends Component {
+ _updateUserPrefs = async (userData: EditedUser, silentOnSucceed = false) => {
const { createOrUpdateUser, intl } = this.props
// Convert the notification attributes from array to comma-separated string.
const passedUserData = clone(userData)
- const { notificationChannel } = passedUserData
+ const { notificationChannel } = userData
if (
+ notificationChannel &&
typeof notificationChannel === 'object' &&
typeof notificationChannel.length === 'number'
) {
passedUserData.notificationChannel = notificationChannel.join(',')
}
- const result = await createOrUpdateUser(passedUserData, intl)
+ const result = await createOrUpdateUser(passedUserData as User, intl)
// If needed, display a toast notification on success.
if (result === userActions.UserActionResult.SUCCESS && !silentOnSucceed) {
@@ -52,18 +77,17 @@ class UserAccountScreen extends Component {
* @param {*} userData The user data state to persist.
* @returns The new user id the the caller can use.
*/
- _handleCreateNewUser = (userData) => {
+ _handleCreateNewUser = (userData: EditedUser) => {
this._updateUserPrefs(userData, true)
}
- _handleDeleteUser = (evt) => {
+ _handleDeleteUser = (evt: FormEvent) => {
const { auth0, deleteUser, intl, loggedInUser } = this.props
// Avoid triggering onsubmit with formik (which would result in a save user
// call).
evt.preventDefault()
if (
- // eslint-disable-next-line no-restricted-globals
- confirm(
+ window.confirm(
intl.formatMessage({ id: 'components.UserAccountScreen.confirmDelete' })
)
) {
@@ -76,7 +100,7 @@ class UserAccountScreen extends Component {
this.props.routeTo('/')
}
- _handleRequestPhoneVerificationCode = (newPhoneNumber) => {
+ _handleRequestPhoneVerificationCode = (newPhoneNumber: string) => {
const { intl, requestPhoneVerificationSms } = this.props
requestPhoneVerificationSms(newPhoneNumber, intl)
}
@@ -85,7 +109,7 @@ class UserAccountScreen extends Component {
* Save changes and return to the planner.
* @param {*} userData The user edited state to be saved, provided by Formik.
*/
- _handleSaveAndExit = async (userData) => {
+ _handleSaveAndExit = async (userData: EditedUser) => {
await this._updateUserPrefs(userData)
this._handleExit()
}
@@ -94,7 +118,7 @@ class UserAccountScreen extends Component {
* Persist changes immediately (for existing account display)
* @param {*} userData The user edited state to be saved, provided by Formik.
*/
- _handleFieldChange = async (userData) => {
+ _handleFieldChange = async (userData: EditedUser) => {
// Turn off the default toast, so we can display a toast per field.
const result = await this._updateUserPrefs(userData, true)
if (result !== userActions.UserActionResult.SUCCESS) {
@@ -102,7 +126,11 @@ class UserAccountScreen extends Component {
}
}
- _handleSendPhoneVerificationCode = async ({ validationCode: code }) => {
+ _handleSendPhoneVerificationCode = async ({
+ validationCode: code
+ }: {
+ validationCode: string
+ }) => {
const { intl, verifyPhoneNumber } = this.props
await verifyPhoneNumber(code, intl)
}
@@ -114,7 +142,8 @@ class UserAccountScreen extends Component {
: ExistingAccountDisplay
const loggedInUserWithNotificationArray = {
...loggedInUser,
- notificationChannel: loggedInUser.notificationChannel.split(',')
+ notificationChannel: loggedInUser.notificationChannel?.split(','),
+ pushDevices: 2
}
return (
@@ -136,7 +165,8 @@ class UserAccountScreen extends Component {
{
+const mapStateToProps = (
+ state: any,
+ ownProps: RouteComponentProps<{ step: string }>
+) => {
const { params, url } = ownProps.match
const isCreating = url.startsWith(CREATE_ACCOUNT_PATH)
const { step } = params
diff --git a/lib/components/user/with-logged-in-user-support.js b/lib/components/user/with-logged-in-user-support.js
index 204a52d68..9895e6939 100644
--- a/lib/components/user/with-logged-in-user-support.js
+++ b/lib/components/user/with-logged-in-user-support.js
@@ -24,7 +24,7 @@ import AwaitingScreen from './awaiting-screen'
* but will display extra functionality if so.
* For such components, omit requireLoggedInUser parameter (or set to false).
* The wrapped component is shown immediately, and no awaiting screen is displayed while state.user is being retrieved.
- * @param {React.Component} WrappedComponent The component to be wrapped to that uses state.user from the redux store.
+ * @param {React.ComponentType} WrappedComponent The component to be wrapped to that uses state.user from the redux store.
* @param {boolean} requireLoggedInUser Whether the wrapped component requires state.user to properly function.
*/
export default function withLoggedInUserSupport(
From 15cd0be9d6b95052ca2aee252aaf7691f7e0dadb Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Thu, 12 Oct 2023 16:14:36 -0400
Subject: [PATCH 12/21] chore(i18n): Add i18n group exceptions.
---
i18n/i18n-exceptions.json | 3 +++
1 file changed, 3 insertions(+)
diff --git a/i18n/i18n-exceptions.json b/i18n/i18n-exceptions.json
index 60f5855cd..d34456a8d 100644
--- a/i18n/i18n-exceptions.json
+++ b/i18n/i18n-exceptions.json
@@ -5,6 +5,9 @@
"sms",
"push"
],
+ "components.ExistingAccountDisplay.fields.*": [
+ "storeTripHistory"
+ ],
"components.OTP2ErrorRenderer.*.body": [
"LOCATION_NOT_FOUND",
"NO_STOPS_IN_RANGE",
From 760539ba01d7455b437932f8c48ec032fe4ffe98 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Mon, 16 Oct 2023 09:51:09 -0400
Subject: [PATCH 13/21] improvement(existing-account-display): Disable inputs
during user data update submission.
---
lib/components/user/existing-account-display.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx
index fe59a83d7..b9784fa2b 100644
--- a/lib/components/user/existing-account-display.tsx
+++ b/lib/components/user/existing-account-display.tsx
@@ -38,7 +38,11 @@ const ExistingAccountDisplay = (parentProps: Props) => {
// Apply changes and submit the form right away to update the user profile.
handleChange(e)
try {
+ // Disable input during submission
+ e.target.disabled = true
await submitForm()
+ // Re-enable input during submission
+ e.target.disabled = false
// Display a toast notification on success.
toastSuccess(
intl.formatMessage({
From fafbe96c3755885d96129cf183e0c636563dda53 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Mon, 16 Oct 2023 17:07:09 -0400
Subject: [PATCH 14/21] improvement(user-account-screen): Consolidate input
change handling.
---
.../user/existing-account-display.tsx | 40 +++--------------
lib/components/user/new-account-wizard.tsx | 11 +++++
.../user/sequential-pane-display.tsx | 7 ++-
lib/components/user/user-account-screen.tsx | 45 +++++++++++++++++--
4 files changed, 62 insertions(+), 41 deletions(-)
diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx
index b9784fa2b..f89ccca55 100644
--- a/lib/components/user/existing-account-display.tsx
+++ b/lib/components/user/existing-account-display.tsx
@@ -1,10 +1,9 @@
import { connect } from 'react-redux'
import { FormattedMessage, useIntl } from 'react-intl'
import { FormikProps } from 'formik'
-import React, { useCallback } from 'react'
+import React from 'react'
import { AppReduxState } from '../../util/state-types'
-import { toastSuccess } from '../util/toasts'
import { TransitModeConfig } from '../../util/config-types'
import PageTitle from '../util/page-title'
@@ -24,44 +23,15 @@ interface Props extends FormikProps {
/**
* This component handles the existing account display.
*/
-const ExistingAccountDisplay = (parentProps: Props) => {
+const ExistingAccountDisplay = (props: Props) => {
// The props include Formik props that provide access to the current user data
// and to its own blur/change/submit event handlers that automate the state.
// We forward the props to each pane so that their individual controls
// can be wired to be managed by Formik.
- const { handleChange, submitForm, wheelchairEnabled } = parentProps
+
+ const { wheelchairEnabled } = props
const intl = useIntl()
- const props = {
- ...parentProps,
- handleChange: useCallback(
- async (e) => {
- // Apply changes and submit the form right away to update the user profile.
- handleChange(e)
- try {
- // Disable input during submission
- e.target.disabled = true
- await submitForm()
- // Re-enable input during submission
- e.target.disabled = false
- // Display a toast notification on success.
- toastSuccess(
- intl.formatMessage({
- // Use a summary text for the field, if defined (e.g. to replace long labels),
- // otherwise, fall back on the first label of the input.
- defaultMessage: e.target.labels[0]?.innerText,
- id: `components.ExistingAccountDisplay.fields.${e.target.name}`
- }),
- intl.formatMessage({
- id: 'components.ExistingAccountDisplay.fieldUpdated'
- })
- )
- } catch {
- alert('Error updating profile')
- }
- },
- [intl, handleChange, submitForm]
- )
- }
+
const panes = [
{
pane: FavoritePlaceList,
diff --git a/lib/components/user/new-account-wizard.tsx b/lib/components/user/new-account-wizard.tsx
index 9cc45c7b9..aa9ac30cf 100644
--- a/lib/components/user/new-account-wizard.tsx
+++ b/lib/components/user/new-account-wizard.tsx
@@ -1,6 +1,7 @@
import { Form, FormikProps } from 'formik'
import { FormattedMessage, useIntl } from 'react-intl'
import React, { useCallback } from 'react'
+import toast from 'react-hot-toast'
import PageTitle from '../util/page-title'
@@ -28,6 +29,7 @@ interface Props extends FormikUserProps {
*/
const NewAccountWizard = ({
activePaneId,
+ onCancel, // provided by UserAccountScreen
onCreate, // provided by UserAccountScreen
...formikProps // provided by Formik
}: Props): JSX.Element => {
@@ -41,6 +43,14 @@ const NewAccountWizard = ({
}
}, [onCreate, userData])
+ const handleFinish = useCallback(() => {
+ // Display a toast to acknowledge saved changes
+ // (although in reality, changes quietly took effect in previous screens).
+ toast.success(intl.formatMessage({ id: 'actions.user.preferencesSaved' }))
+
+ onCancel && onCancel()
+ }, [intl, onCancel])
+
if (activePaneId === 'verify') {
const verifyEmail = intl.formatMessage({
id: 'components.NewAccountWizard.verify'
@@ -97,6 +107,7 @@ const NewAccountWizard = ({
diff --git a/lib/components/user/sequential-pane-display.tsx b/lib/components/user/sequential-pane-display.tsx
index e41a953b7..9f2df97bf 100644
--- a/lib/components/user/sequential-pane-display.tsx
+++ b/lib/components/user/sequential-pane-display.tsx
@@ -21,6 +21,7 @@ export interface PaneProps {
interface OwnProps {
activePaneId: string
+ onFinish?: () => void
panes: PaneProps[]
}
@@ -57,7 +58,7 @@ class SequentialPaneDisplay extends Component> {
}
_handleToNextPane = async (e: MouseEvent,
+ {
+ id
+ }
)
}
From eb85c70317495003f86be29a44bdeef22ca27c92 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Tue, 24 Oct 2023 15:15:13 -0400
Subject: [PATCH 20/21] refactor(user-account-screen): Tweak types
---
lib/components/user/user-account-screen.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/components/user/user-account-screen.tsx b/lib/components/user/user-account-screen.tsx
index 8213d79fe..334068beb 100644
--- a/lib/components/user/user-account-screen.tsx
+++ b/lib/components/user/user-account-screen.tsx
@@ -135,7 +135,7 @@ class UserAccountScreen extends Component {
? toast.loading(
intl.formatMessage({ id: 'components.UserAccountScreen.updating' })
)
- : null
+ : undefined
await submitForm()
// On success, display a toast notification for existing accounts.
From b1880439c297530bac8c011cd058ad9dbe5d722f Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Wed, 25 Oct 2023 11:02:39 -0400
Subject: [PATCH 21/21] improvement(user-account-screen): Add wait cursor to
label of changed input.
---
lib/components/user/user-account-screen.tsx | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/lib/components/user/user-account-screen.tsx b/lib/components/user/user-account-screen.tsx
index 334068beb..f5af2d47c 100644
--- a/lib/components/user/user-account-screen.tsx
+++ b/lib/components/user/user-account-screen.tsx
@@ -125,18 +125,20 @@ class UserAccountScreen extends Component {
submitForm: () => Promise
) => {
const { intl, isCreating } = this.props
+ const firstLabel = t.labels?.[0]
// Disable input (adds a visual effect) during submission
t.disabled = true
const initialStyle = t.style.animation
t.style.animation = 'dive-in 1s linear infinite'
+ const initialCursor = firstLabel ? firstLabel.style.cursor : ''
+ if (firstLabel) firstLabel.style.cursor = 'wait'
+ const loadingToast = !isCreating
+ ? toast.loading(
+ intl.formatMessage({ id: 'components.UserAccountScreen.updating' })
+ )
+ : undefined
try {
- const loadingToast = !isCreating
- ? toast.loading(
- intl.formatMessage({ id: 'components.UserAccountScreen.updating' })
- )
- : undefined
-
await submitForm()
// On success, display a toast notification for existing accounts.
if (!isCreating) {
@@ -144,7 +146,7 @@ class UserAccountScreen extends Component {
intl.formatMessage({
// Use a summary text for the field, if defined (e.g. to replace long labels),
// otherwise, fall back on the first label of the input.
- defaultMessage: t.labels?.[0]?.innerText,
+ defaultMessage: firstLabel?.innerText,
id: `components.UserAccountScreen.fields.${t.name}`
}),
intl.formatMessage({
@@ -154,13 +156,16 @@ class UserAccountScreen extends Component {
)
}
} catch {
+ // Remove any toasts before showing alert.
+ toast.remove()
alert(
intl.formatMessage({
id: 'components.UserAccountScreen.errorUpdatingProfile'
})
)
} finally {
- // Re-enable input and refocus after submission
+ // Re-enable input (remove visuals) and refocus after submission.
+ if (firstLabel) firstLabel.style.cursor = initialCursor
t.disabled = false
t.style.animation = initialStyle
t.focus()