diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index ad58294c0cc8..d723e5ad2912 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -87,7 +87,7 @@ function AccountSwitcher() { } const delegatePersonalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); - const error = ErrorUtils.getLatestErrorField(account?.delegatedAccess, 'connect'); + const error = ErrorUtils.getLatestError(account?.delegatedAccess?.errorFields?.disconnect); return [ createBaseMenuItem(delegatePersonalDetails, error, { @@ -105,8 +105,9 @@ function AccountSwitcher() { const delegatorMenuItems: PopoverMenuItem[] = delegators .filter(({email}) => email !== currentUserPersonalDetails.login) - .map(({email, role, errorFields}) => { - const error = ErrorUtils.getLatestErrorField({errorFields}, 'connect'); + .map(({email, role}) => { + const errorFields = account?.delegatedAccess?.errorFields ?? {}; + const error = ErrorUtils.getLatestError(errorFields?.connect?.[email]); const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email); return createBaseMenuItem(personalDetails, error, { badgeText: translate('delegate.role', {role}), diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index ba2a33a367d4..b880239b8abf 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -134,6 +134,15 @@ function getLatestErrorFieldForAnyField Object.assign(acc, error), {}); } +function getLatestError(errors?: Errors): Errors { + if (!errors || Object.keys(errors).length === 0) { + return {}; + } + + const key = Object.keys(errors).sort().reverse().at(0) ?? ''; + return {[key]: getErrorMessageWithTranslationData(errors[key])}; +} + /** * Method used to attach already translated message * @param errors - An object containing current errors in the form @@ -198,6 +207,7 @@ export { getLatestErrorFieldForAnyField, getLatestErrorMessage, getLatestErrorMessageField, + getLatestError, getMicroSecondOnyxErrorWithTranslationKey, getMicroSecondOnyxErrorWithMessage, getMicroSecondOnyxErrorObject, diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index 28f2019bb231..e294a57e6c5f 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -47,7 +47,11 @@ function connect(email: string) { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { - delegators: delegatedAccess.delegators.map((delegator) => (delegator.email === email ? {...delegator, errorFields: {connect: null}} : delegator)), + errorFields: { + connect: { + [email]: null, + }, + }, }, }, }, @@ -59,7 +63,11 @@ function connect(email: string) { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { - delegators: delegatedAccess.delegators.map((delegator) => (delegator.email === email ? {...delegator, errorFields: undefined} : delegator)), + errorFields: { + connect: { + [email]: null, + }, + }, }, }, }, @@ -71,9 +79,11 @@ function connect(email: string) { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { - delegators: delegatedAccess.delegators.map((delegator) => - delegator.email === email ? {...delegator, errorFields: {connect: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('delegate.genericError')}} : delegator, - ), + errorFields: { + connect: { + [email]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('delegate.genericError'), + }, + }, }, }, }, @@ -112,7 +122,7 @@ function disconnect() { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { - errorFields: {connect: null}, + errorFields: {disconnect: null}, }, }, }, @@ -136,7 +146,7 @@ function disconnect() { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { - errorFields: {connect: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('delegate.genericError')}, + errorFields: {disconnect: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('delegate.genericError')}, }, }, }, @@ -190,7 +200,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { : { ...delegate, isLoading: true, - errorFields: {addDelegate: null}, pendingFields: {email: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, @@ -204,7 +213,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { email, role, isLoading: true, - errorFields: {addDelegate: null}, pendingFields: {email: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, @@ -218,6 +226,11 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { value: { delegatedAccess: { delegates: optimisticDelegateData(), + errorFields: { + addDelegate: { + [email]: null, + }, + }, }, isLoading: true, }, @@ -233,7 +246,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { : { ...delegate, isLoading: false, - errorFields: {addDelegate: null}, pendingAction: null, pendingFields: {email: null, role: null}, optimisticAccountID: undefined, @@ -247,7 +259,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { { email, role, - errorFields: {addDelegate: null}, isLoading: false, pendingAction: null, pendingFields: {email: null, role: null}, @@ -263,6 +274,11 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { value: { delegatedAccess: { delegates: successDelegateData(), + errorFields: { + addDelegate: { + [email]: null, + }, + }, }, isLoading: false, }, @@ -278,7 +294,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { : { ...delegate, isLoading: false, - errorFields: {addDelegate: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('contacts.genericFailureMessages.validateSecondaryLogin')}, }, ) ?? [] ); @@ -289,9 +304,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { { email, role, - errorFields: { - addDelegate: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('contacts.genericFailureMessages.validateSecondaryLogin'), - }, isLoading: false, pendingFields: {email: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, @@ -328,11 +340,15 @@ function removeDelegate(email: string) { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { + errorFields: { + removeDelegate: { + [email]: null, + }, + }, delegates: delegatedAccess.delegates?.map((delegate) => delegate.email === email ? { ...delegate, - errorFields: {removeDelegate: null}, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, pendingFields: {email: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, role: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, } @@ -361,13 +377,15 @@ function removeDelegate(email: string) { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { + errorFields: { + removeDelegate: { + [email]: null, + }, + }, delegates: delegatedAccess.delegates?.map((delegate) => delegate.email === email ? { ...delegate, - errorFields: { - removeDelegate: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('delegate.genericError'), - }, pendingAction: null, pendingFields: undefined, } @@ -383,14 +401,18 @@ function removeDelegate(email: string) { API.write(WRITE_COMMANDS.REMOVE_DELEGATE, parameters, {optimisticData, successData, failureData}); } -function clearAddDelegateErrors(email: string, fieldName: string) { +function clearDelegateErrorsByField(email: string, fieldName: string) { if (!delegatedAccess?.delegates) { return; } Onyx.merge(ONYXKEYS.ACCOUNT, { delegatedAccess: { - delegates: delegatedAccess.delegates.map((delegate) => (delegate.email !== email ? delegate : {...delegate, errorFields: {...delegate.errorFields, [fieldName]: null}})), + errorFields: { + [fieldName]: { + [email]: null, + }, + }, }, }); } @@ -422,12 +444,16 @@ function updateDelegateRole(email: string, role: DelegateRole, validateCode: str key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { + errorFields: { + updateDelegateRole: { + [email]: null, + }, + }, delegates: delegatedAccess.delegates.map((delegate) => delegate.email === email ? { ...delegate, role, - errorFields: {updateDelegateRole: null}, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: {role: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, isLoading: true, @@ -445,12 +471,16 @@ function updateDelegateRole(email: string, role: DelegateRole, validateCode: str key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { + errorFields: { + updateDelegateRole: { + [email]: null, + }, + }, delegates: delegatedAccess.delegates.map((delegate) => delegate.email === email ? { ...delegate, role, - errorFields: {updateDelegateRole: null}, pendingAction: null, pendingFields: {role: null}, isLoading: false, @@ -472,9 +502,6 @@ function updateDelegateRole(email: string, role: DelegateRole, validateCode: str delegate.email === email ? { ...delegate, - errorFields: { - updateDelegateRole: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('delegate.genericError'), - }, isLoading: false, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: {role: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, @@ -502,12 +529,16 @@ function updateDelegateRoleOptimistically(email: string, role: DelegateRole) { key: ONYXKEYS.ACCOUNT, value: { delegatedAccess: { + errorFields: { + updateDelegateRole: { + [email]: null, + }, + }, delegates: delegatedAccess.delegates.map((delegate) => delegate.email === email ? { ...delegate, role, - errorFields: {updateDelegateRole: null}, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: {role: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, } @@ -568,7 +599,7 @@ export { clearDelegatorErrors, addDelegate, requestValidationCode, - clearAddDelegateErrors, + clearDelegateErrorsByField, removePendingDelegate, restoreDelegateSession, isConnectedAsDelegate, diff --git a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx index 4f6770bd98ff..e466b862ae9a 100644 --- a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx +++ b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx @@ -23,16 +23,17 @@ function DelegateMagicCodeModal({login, role, onClose, isValidateCodeActionModal const [account] = useOnyx(ONYXKEYS.ACCOUNT); const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === login); - const validateLoginError = ErrorUtils.getLatestErrorField(currentDelegate, 'addDelegate'); + const addDelegateErrors = account?.delegatedAccess?.errorFields?.addDelegate?.[login]; + const validateLoginError = ErrorUtils.getLatestError(addDelegateErrors); useEffect(() => { - if (!currentDelegate || !!currentDelegate.pendingFields?.email || !!currentDelegate.errorFields?.addDelegate) { + if (!currentDelegate || !!currentDelegate.pendingFields?.email || !!addDelegateErrors) { return; } // Dismiss modal on successful magic code verification Navigation.navigate(ROUTES.SETTINGS_SECURITY); - }, [login, currentDelegate, role]); + }, [login, currentDelegate, role, addDelegateErrors]); const onBackButtonPress = () => { onClose?.(); @@ -42,7 +43,7 @@ function DelegateMagicCodeModal({login, role, onClose, isValidateCodeActionModal if (!validateLoginError) { return; } - Delegate.clearAddDelegateErrors(currentDelegate?.email ?? '', 'addDelegate'); + Delegate.clearDelegateErrorsByField(currentDelegate?.email ?? '', 'addDelegate'); }; return ( diff --git a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodePage.tsx b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodePage.tsx index 2c1bc55e0e92..3bc82e8d7e65 100644 --- a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodePage.tsx +++ b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodePage.tsx @@ -28,15 +28,16 @@ function UpdateDelegateMagicCodePage({route}: UpdateDelegateMagicCodePageProps) const validateCodeFormRef = useRef(null); const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === login); + const updateDelegateErrors = account?.delegatedAccess?.errorFields?.addDelegate?.[login]; useEffect(() => { - if (!currentDelegate || !!currentDelegate.pendingFields?.role || !!currentDelegate.errorFields?.updateDelegateRole) { + if (!currentDelegate || !!currentDelegate.pendingFields?.role || !!updateDelegateErrors) { return; } // Dismiss modal on successful magic code verification Navigation.dismissModal(); - }, [login, currentDelegate, role]); + }, [login, currentDelegate, role, updateDelegateErrors]); const onBackButtonPress = () => { Navigation.goBack(ROUTES.SETTINGS_UPDATE_DELEGATE_ROLE.getRoute(login, role)); diff --git a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/ValidateCodeForm/BaseValidateCodeForm.tsx index 4c07803ef0e3..7c35d1478eb2 100644 --- a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -66,7 +66,8 @@ function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () => const focusTimeoutRef = useRef(null); const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === delegate); - const validateLoginError = ErrorUtils.getLatestErrorField(currentDelegate, 'updateDelegateRole'); + const errorFields = account?.delegatedAccess?.errorFields ?? {}; + const validateLoginError = ErrorUtils.getLatestError(errorFields.updateDelegateRole?.[currentDelegate?.email ?? '']); const shouldDisableResendValidateCode = !!isOffline || currentDelegate?.isLoading; @@ -127,7 +128,7 @@ function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () => setValidateCode(text); setFormError({}); if (validateLoginError) { - Delegate.clearAddDelegateErrors(currentDelegate?.email ?? '', 'updateDelegateRole'); + Delegate.clearDelegateErrorsByField(currentDelegate?.email ?? '', 'updateDelegateRole'); } }, [currentDelegate?.email, validateLoginError], diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index ec82000d06c8..8429cf7b96c4 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -26,7 +26,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {clearAddDelegateErrors, removeDelegate} from '@libs/actions/Delegate'; +import {clearDelegateErrorsByField, removeDelegate} from '@libs/actions/Delegate'; import * as ErrorUtils from '@libs/ErrorUtils'; import getClickedTargetLocation from '@libs/getClickedTargetLocation'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; @@ -56,6 +56,7 @@ function SecuritySettingsPage() { const [shouldShowDelegatePopoverMenu, setShouldShowDelegatePopoverMenu] = useState(false); const [shouldShowRemoveDelegateModal, setShouldShowRemoveDelegateModal] = useState(false); const [selectedDelegate, setSelectedDelegate] = useState(); + const errorFields = account?.delegatedAccess?.errorFields ?? {}; const [anchorPosition, setAnchorPosition] = useState({ anchorPositionHorizontal: 0, @@ -136,9 +137,10 @@ function SecuritySettingsPage() { () => delegates .filter((d) => !d.optimisticAccountID) - .map(({email, role, pendingAction, errorFields, pendingFields}) => { + .map(({email, role, pendingAction, pendingFields}) => { const personalDetail = getPersonalDetailByEmail(email); - const error = ErrorUtils.getLatestErrorField({errorFields}, 'addDelegate'); + const addDelegateErrors = errorFields?.addDelegate?.[email]; + const error = ErrorUtils.getLatestError(addDelegateErrors); const onPress = (e: GestureResponderEvent | KeyboardEvent) => { if (isEmptyObject(pendingAction)) { @@ -171,14 +173,14 @@ function SecuritySettingsPage() { shouldShowRightIcon: true, pendingAction, shouldForceOpacity: !!pendingAction, - onPendingActionDismiss: () => clearAddDelegateErrors(email, 'addDelegate'), + onPendingActionDismiss: () => clearDelegateErrorsByField(email, 'addDelegate'), error, onPress, }; }), // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - [delegates, translate, styles, personalDetails], + [delegates, translate, styles, personalDetails, errorFields], ); const delegatorMenuItems: MenuItemProps[] = useMemo( diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts index b856ed9010dd..3902d67882c4 100644 --- a/src/types/onyx/Account.ts +++ b/src/types/onyx/Account.ts @@ -20,9 +20,6 @@ type Delegate = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Whether the user validation code was sent */ validateCodeSent?: boolean; - /** Field-specific server side errors keyed by microtime */ - errorFields?: OnyxCommon.ErrorFields; - /** Whether the user is loading */ isLoading?: boolean; @@ -30,6 +27,24 @@ type Delegate = OnyxCommon.OnyxValueWithOfflineFeedback<{ optimisticAccountID?: number; }>; +/** Delegate errors */ +type DelegateErrors = { + /** Errors while adding a delegate keyed by email */ + addDelegate?: Record; + + /** Errors while updating a delegate's role keyed by email */ + updateDelegateRole?: Record; + + /** Errors while removing a delegate keyed by email */ + removeDelegate?: Record; + + /** Errors while connecting as a delegate keyed by email */ + connect?: Record; + + /** Errors while disconnecting as a delegate. No email needed here. */ + disconnect?: OnyxCommon.Errors; +}; + /** Model of delegated access data */ type DelegatedAccess = { /** The users that can access your account as a delegate */ @@ -41,8 +56,8 @@ type DelegatedAccess = { /** The email of original user when they are acting as a delegate for another account */ delegate?: string; - /** Authentication failure errors when disconnecting as a copilot */ - errorFields?: OnyxCommon.ErrorFields; + /** Field-specific server side errors keyed by microtime */ + errorFields?: DelegateErrors; }; /** Model of user account */