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 ( - <> +

{verifyEmail}

- + ) } @@ -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