diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 02fa57f8c8ee..d3b0c8f2311f 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -158,7 +158,7 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) { close(() => setShouldShowOfflineModal(true)); return; } - connect(email); + connect({email, delegatedAccess: account?.delegatedAccess}); }, }); }); @@ -168,7 +168,7 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) { const hideDelegatorMenu = () => { setShouldShowDelegatorMenu(false); - clearDelegatorErrors(); + clearDelegatorErrors({delegatedAccess: account?.delegatedAccess}); }; return ( diff --git a/src/libs/Authentication.ts b/src/libs/Authentication.ts index cf83bb8ac43c..624d1b589b23 100644 --- a/src/libs/Authentication.ts +++ b/src/libs/Authentication.ts @@ -1,7 +1,9 @@ import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Account} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; import {isConnectedAsDelegate, restoreDelegateSession} from './actions/Delegate'; import updateSessionAuthTokens from './actions/Session/updateSessionAuthTokens'; @@ -43,6 +45,16 @@ Onyx.connectWithoutView({ }, }); +let account: OnyxEntry; +// Authentication lib is not connected to any changes on the UI +// So it is okay to use connectWithoutView here. +Onyx.connectWithoutView({ + key: ONYXKEYS.ACCOUNT, + callback: (value) => { + account = value; + }, +}); + function Authenticate(parameters: Parameters): Promise { const commandName = 'Authenticate'; @@ -154,7 +166,7 @@ function reauthenticate(command = ''): Promise { // If we reauthenticate due to an expired delegate token, restore the delegate's original account. // This is because the credentials used to reauthenticate were for the delegate's original account, and not for the account they were connected as. - if (isConnectedAsDelegate()) { + if (isConnectedAsDelegate({delegatedAccess: account?.delegatedAccess})) { Log.info('Reauthenticate while connected as a delegate. Restoring original account.'); restoreDelegateSession(response); return true; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index bed8d535eca9..b4d5963d2c0f 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -303,7 +303,7 @@ function AuthScreens() { // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp() and connect() for delegator from OldDot. if (SessionUtils.didUserLogInDuringSession() || delegatorEmail) { if (delegatorEmail) { - connect(delegatorEmail, true) + connect({email: delegatorEmail, delegatedAccess: account?.delegatedAccess, isFromOldDot: true}) ?.then((success) => { App.setAppLoading(!!success); }) diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index fe38066f02e7..234c49580c71 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -2,7 +2,7 @@ import HybridAppModule from '@expensify/react-native-hybrid-app'; import Onyx from 'react-native-onyx'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; -import type {AddDelegateParams, RemoveDelegateParams, UpdateDelegateRoleParams} from '@libs/API/parameters'; +import type {AddDelegateParams as APIAddDelegateParams, RemoveDelegateParams as APIRemoveDelegateParams, UpdateDelegateRoleParams as APIUpdateDelegateRoleParams} from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; @@ -20,14 +20,6 @@ import {getCurrentUserAccountID} from './Report'; import updateSessionAuthTokens from './Session/updateSessionAuthTokens'; import updateSessionUser from './Session/updateSessionUser'; -let delegatedAccess: DelegatedAccess; -Onyx.connect({ - key: ONYXKEYS.ACCOUNT, - callback: (val) => { - delegatedAccess = val?.delegatedAccess ?? {}; - }, -}); - let credentials: Credentials = {}; Onyx.connect({ key: ONYXKEYS.CREDENTIALS, @@ -79,11 +71,61 @@ const KEYS_TO_PRESERVE_DELEGATE_ACCESS = [ ONYXKEYS.IS_DEBUG_MODE_ENABLED, ]; +type WithDelegatedAccess = { + // Optional keeps call sites clean, but still encourages passing `account?.delegatedAccess`. + delegatedAccess: DelegatedAccess | undefined; +}; + +type WithEmail = { + email: string; +}; + +type WithRole = { + role: DelegateRole; +}; + +type WithValidateCode = { + validateCode: string; +}; + +type WithFieldName = { + // Constrain to known keys to avoid misspells at call sites + fieldName: 'addDelegate' | 'updateDelegateRole'; // but string could work as well +}; + +type WithOldDotFlag = { + isFromOldDot?: boolean; +}; + +// Clear delegator-level errors +type ClearDelegatorErrorsParams = WithDelegatedAccess; + +// Add a delegate (requires code) +type AddDelegateParams = WithEmail & WithRole & WithValidateCode & WithDelegatedAccess; + +// Remove a delegate +type RemoveDelegateParams = WithEmail & WithDelegatedAccess; + +// Clear delegate errors scoped by field +type ClearDelegateErrorsByFieldParams = WithEmail & WithFieldName & WithDelegatedAccess; + +// Update existing delegate role (requires code) +type UpdateDelegateRoleParams = WithEmail & WithRole & WithValidateCode & WithDelegatedAccess; + +// Is connected as delegate? +type IsConnectedAsDelegateParams = WithDelegatedAccess; + +// Connect as delegate +type ConnectParams = WithEmail & WithDelegatedAccess & WithOldDotFlag; + +// Clear pending action for role update +type ClearDelegateRolePendingActionParams = WithEmail & WithDelegatedAccess; + /** * Connects the user as a delegate to another account. * Returns a Promise that resolves to true on success, false on failure, or undefined if not applicable. */ -function connect(email: string, isFromOldDot = false) { +function connect({email, delegatedAccess, isFromOldDot = false}: ConnectParams) { if (!delegatedAccess?.delegators && !isFromOldDot) { return; } @@ -280,7 +322,7 @@ function disconnect() { }); } -function clearDelegatorErrors() { +function clearDelegatorErrors({delegatedAccess}: ClearDelegatorErrorsParams) { if (!delegatedAccess?.delegators) { return; } @@ -291,7 +333,11 @@ function requestValidationCode() { API.write(WRITE_COMMANDS.RESEND_VALIDATE_CODE, null); } -function addDelegate(email: string, role: DelegateRole, validateCode: string) { +function addDelegate({email, role, validateCode, delegatedAccess}: AddDelegateParams) { + if (!delegatedAccess?.delegates) { + return; + } + const existingDelegate = delegatedAccess?.delegates?.find((delegate) => delegate.email === email); const optimisticDelegateData = (): Delegate[] => { @@ -436,13 +482,13 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { }; optimisticData.push(optimisticResetActionCode); - const parameters: AddDelegateParams = {delegateEmail: email, validateCode, role}; + const parameters: APIAddDelegateParams = {delegateEmail: email, validateCode, role}; API.write(WRITE_COMMANDS.ADD_DELEGATE, parameters, {optimisticData, successData, failureData}); } -function removeDelegate(email: string) { - if (!email) { +function removeDelegate({email, delegatedAccess}: RemoveDelegateParams) { + if (!email || !delegatedAccess?.delegates) { return; } @@ -508,12 +554,12 @@ function removeDelegate(email: string) { }, ]; - const parameters: RemoveDelegateParams = {delegateEmail: email}; + const parameters: APIRemoveDelegateParams = {delegateEmail: email}; API.write(WRITE_COMMANDS.REMOVE_DELEGATE, parameters, {optimisticData, successData, failureData}); } -function clearDelegateErrorsByField(email: string, fieldName: string) { +function clearDelegateErrorsByField({email, fieldName, delegatedAccess}: ClearDelegateErrorsByFieldParams) { if (!delegatedAccess?.delegates) { return; } @@ -529,11 +575,11 @@ function clearDelegateErrorsByField(email: string, fieldName: string) { }); } -function isConnectedAsDelegate() { +function isConnectedAsDelegate({delegatedAccess}: IsConnectedAsDelegateParams) { return !!delegatedAccess?.delegate; } -function updateDelegateRole(email: string, role: DelegateRole, validateCode: string) { +function updateDelegateRole({email, role, validateCode, delegatedAccess}: UpdateDelegateRoleParams) { if (!delegatedAccess?.delegates) { return; } @@ -612,46 +658,12 @@ function updateDelegateRole(email: string, role: DelegateRole, validateCode: str }, ]; - const parameters: UpdateDelegateRoleParams = {delegateEmail: email, validateCode, role}; + const parameters: APIUpdateDelegateRoleParams = {delegateEmail: email, validateCode, role}; API.write(WRITE_COMMANDS.UPDATE_DELEGATE_ROLE, parameters, {optimisticData, successData, failureData}); } -function updateDelegateRoleOptimistically(email: string, role: DelegateRole) { - if (!delegatedAccess?.delegates) { - return; - } - - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - delegatedAccess: { - errorFields: { - updateDelegateRole: { - [email]: null, - }, - }, - delegates: delegatedAccess.delegates.map((delegate) => - delegate.email === email - ? { - ...delegate, - role, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: {role: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, - } - : delegate, - ), - }, - }, - }, - ]; - - Onyx.update(optimisticData); -} - -function clearDelegateRolePendingAction(email: string) { +function clearDelegateRolePendingAction({email, delegatedAccess}: ClearDelegateRolePendingActionParams) { if (!delegatedAccess?.delegates) { return; } @@ -705,7 +717,6 @@ export { clearDelegateErrorsByField, restoreDelegateSession, isConnectedAsDelegate, - updateDelegateRoleOptimistically, clearDelegateRolePendingAction, updateDelegateRole, removeDelegate, diff --git a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx index b59400326db9..ff70276674cd 100644 --- a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx +++ b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx @@ -42,7 +42,7 @@ function DelegateMagicCodeModal({login, role, onClose, isValidateCodeActionModal if (isEmptyObject(validateLoginError) && isEmptyObject(validateCodeAction?.errorFields)) { return; } - clearDelegateErrorsByField(currentDelegate?.email ?? '', 'addDelegate'); + clearDelegateErrorsByField({email: currentDelegate?.email ?? '', fieldName: 'addDelegate', delegatedAccess: account?.delegatedAccess}); }; return ( @@ -56,7 +56,7 @@ function DelegateMagicCodeModal({login, role, onClose, isValidateCodeActionModal isVisible={isValidateCodeActionModalVisible} title={translate('delegate.makeSureItIsYou')} sendValidateCode={() => requestValidateCodeAction()} - handleSubmitForm={(validateCode) => addDelegate(login, role, validateCode)} + handleSubmitForm={(validateCode) => addDelegate({email: login, role, validateCode, delegatedAccess: account?.delegatedAccess})} descriptionPrimary={translate('delegate.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} /> ); diff --git a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodeModal.tsx b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodeModal.tsx index e2342dd652bc..a8c19d74e271 100644 --- a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodeModal.tsx +++ b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodeModal.tsx @@ -41,7 +41,7 @@ function UpdateDelegateMagicCodeModal({login, role, isValidateCodeActionModalVis if (isEmptyObject(updateDelegateErrors) && isEmptyObject(validateCodeAction?.errorFields)) { return; } - clearDelegateErrorsByField(currentDelegate?.email ?? '', 'updateDelegateRole'); + clearDelegateErrorsByField({email: currentDelegate?.email ?? '', fieldName: 'updateDelegateRole', delegatedAccess: account?.delegatedAccess}); }; return ( @@ -54,7 +54,7 @@ function UpdateDelegateMagicCodeModal({login, role, isValidateCodeActionModalVis isVisible={isValidateCodeActionModalVisible} title={translate('delegate.makeSureItIsYou')} sendValidateCode={() => requestValidateCodeAction()} - handleSubmitForm={(validateCode) => updateDelegateRole(login, role, validateCode)} + handleSubmitForm={(validateCode) => updateDelegateRole({email: login, role, validateCode, delegatedAccess: account?.delegatedAccess})} descriptionPrimary={translate('delegate.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} /> ); diff --git a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx index b527f45c6b7e..106a810f5183 100644 --- a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx +++ b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx @@ -11,13 +11,11 @@ import TextLink from '@components/TextLink'; import useBeforeRemove from '@hooks/useBeforeRemove'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearDelegateRolePendingAction, updateDelegateRoleOptimistically} from '@libs/actions/Delegate'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; -import type {DelegateRole} from '@src/types/onyx/Account'; import UpdateDelegateMagicCodeModal from './UpdateDelegateMagicCodeModal'; type UpdateDelegateRolePageProps = PlatformStackScreenProps; @@ -46,12 +44,6 @@ function UpdateDelegateRolePage({route}: UpdateDelegateRolePageProps) { })); useBeforeRemove(() => setIsValidateCodeActionModalVisible(false)); - useEffect(() => { - updateDelegateRoleOptimistically(login ?? '', currentRole as DelegateRole); - return () => clearDelegateRolePendingAction(login); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [login]); - return ( const [formError, setFormError] = useState({}); const [validateCode, setValidateCode] = useState(''); const inputValidateCodeRef = useRef(null); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const login = account?.primaryLogin; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case const focusTimeoutRef = useRef(null); const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === delegate); const errorFields = account?.delegatedAccess?.errorFields ?? {}; - const validateLoginError = ErrorUtils.getLatestError(errorFields.updateDelegateRole?.[currentDelegate?.email ?? '']); + const validateLoginError = getLatestError(errorFields.updateDelegateRole?.[currentDelegate?.email ?? '']); const shouldDisableResendValidateCode = !!isOffline || currentDelegate?.isLoading; @@ -115,7 +115,7 @@ function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () => if (!login) { return; } - Delegate.requestValidationCode(); + requestValidationCode(); inputValidateCodeRef.current?.clear(); }; @@ -128,10 +128,10 @@ function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () => setValidateCode(text); setFormError({}); if (validateLoginError) { - Delegate.clearDelegateErrorsByField(currentDelegate?.email ?? '', 'updateDelegateRole'); + clearDelegateErrorsByField({email: currentDelegate?.email ?? '', fieldName: 'updateDelegateRole', delegatedAccess: account?.delegatedAccess}); } }, - [currentDelegate?.email, validateLoginError], + [currentDelegate?.email, validateLoginError, account?.delegatedAccess], ); /** @@ -143,15 +143,15 @@ function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () => return; } - if (!ValidationUtils.isValidValidateCode(validateCode)) { + if (!isValidValidateCode(validateCode)) { setFormError({validateCode: 'validateCodeForm.error.incorrectMagicCode'}); return; } setFormError({}); - Delegate.updateDelegateRole(delegate, role, validateCode); - }, [delegate, role, validateCode]); + updateDelegateRole({email: delegate, role, validateCode, delegatedAccess: account?.delegatedAccess}); + }, [delegate, role, validateCode, account?.delegatedAccess]); return ( @@ -187,7 +187,7 @@ function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () =>