diff --git a/src/CONST.ts b/src/CONST.ts index 496b7d8b28ec..cd222656acf8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4551,6 +4551,12 @@ const CONST = { ALL: 'all', SUBMITTER: 'submitter', }, + DELEGATE: { + DENIED_ACCESS_VARIANTS: { + DELEGATE: 'delegate', + SUBMITTER: 'submitter', + }, + }, DELEGATE_ROLE_HELPDOT_ARTICLE_LINK: 'https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/', STRIPE_GBP_AUTH_STATUSES: { SUCCEEDED: 'succeeded', diff --git a/src/components/DelegateNoAccessModal.tsx b/src/components/DelegateNoAccessModal.tsx index 442c3ec9c4e2..e1e5917ef757 100644 --- a/src/components/DelegateNoAccessModal.tsx +++ b/src/components/DelegateNoAccessModal.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import CONST from '@src/CONST'; import ConfirmModal from './ConfirmModal'; @@ -8,14 +9,13 @@ import TextLink from './TextLink'; type DelegateNoAccessModalProps = { isNoDelegateAccessMenuVisible: boolean; onClose: () => void; - delegatorEmail: string; }; -export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onClose, delegatorEmail = ''}: DelegateNoAccessModalProps) { +export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onClose}: DelegateNoAccessModalProps) { const {translate} = useLocalize(); - const noDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail}); + const {delegatorEmail} = useDelegateUserDetails(); + const noDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail ?? ''}); const noDelegateAccessHyperLinked = translate('delegate.notAllowedMessageHyperLinked'); - const delegateNoAccessPrompt = ( {noDelegateAccessPromptStart} diff --git a/src/components/DelegateNoAccessWrapper.tsx b/src/components/DelegateNoAccessWrapper.tsx new file mode 100644 index 000000000000..c49d15890112 --- /dev/null +++ b/src/components/DelegateNoAccessWrapper.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import AccountUtils from '@libs/AccountUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Account} from '@src/types/onyx'; +import callOrReturn from '@src/types/utils/callOrReturn'; +import FullPageNotFoundView from './BlockingViews/FullPageNotFoundView'; + +const DENIED_ACCESS_VARIANTS = { + // To Restrict All Delegates From Accessing The Page. + [CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]: (account: OnyxEntry) => isDelegate(account), + // To Restrict Only Limited Access Delegates From Accessing The Page. + [CONST.DELEGATE.DENIED_ACCESS_VARIANTS.SUBMITTER]: (account: OnyxEntry) => isSubmitter(account), +} as const satisfies Record) => boolean>; + +type AccessDeniedVariants = keyof typeof DENIED_ACCESS_VARIANTS; + +type DelegateNoAccessWrapperProps = { + accessDeniedVariants?: AccessDeniedVariants[]; + shouldForceFullScreen?: boolean; + children?: (() => React.ReactNode) | React.ReactNode; +}; + +function isDelegate(account: OnyxEntry) { + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + return isActingAsDelegate; +} + +function isSubmitter(account: OnyxEntry) { + const isDelegateOnlySubmitter = AccountUtils.isDelegateOnlySubmitter(account); + return isDelegateOnlySubmitter; +} + +function DelegateNoAccessWrapper({accessDeniedVariants = [], shouldForceFullScreen, ...props}: DelegateNoAccessWrapperProps) { + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isPageAccessDenied = accessDeniedVariants.reduce((acc, variant) => { + const accessDeniedFunction = DENIED_ACCESS_VARIANTS[variant]; + return acc || accessDeniedFunction(account); + }, false); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + if (isPageAccessDenied) { + return ( + { + if (shouldUseNarrowLayout) { + Navigation.dismissModal(); + return; + } + Navigation.goBack(); + }} + titleKey="delegate.notAllowed" + subtitleKey="delegate.noAccessMessage" + shouldShowLink={false} + /> + ); + } + return callOrReturn(props.children); +} + +export default DelegateNoAccessWrapper; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d01b69ed5649..a438bae497f7 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -174,7 +174,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID); const displayedAmount = isAnyTransactionOnHold && canAllowSettlement && hasValidNonHeldAmount ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); - const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); + const {isDelegateAccessRestricted} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; @@ -494,7 +494,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setIsNoDelegateAccessMenuVisible(false)} - delegatorEmail={delegatorEmail ?? ''} /> setIsPaidAnimationRunning(false), []); @@ -592,7 +592,6 @@ function ReportPreview({ setIsNoDelegateAccessMenuVisible(false)} - delegatorEmail={delegatorEmail ?? ''} /> {isHoldMenuVisible && !!iouReport && requestType !== undefined && ( diff --git a/src/languages/en.ts b/src/languages/en.ts index 3e1ea7f0d7cc..3b4c3cae6ef5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5307,6 +5307,7 @@ const translations = { enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to add a copilot. It should arrive within a minute or two.`, enterMagicCodeUpdate: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to update your copilot.`, notAllowed: 'Not so fast...', + noAccessMessage: "As a copilot, you don't have access to \nthis page. Sorry!", notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `You don't have permission to take this action for ${accountOwnerEmail} as a`, notAllowedMessageHyperLinked: ' copilot', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index d28a19fbb1be..e2fd20ef0172 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5826,6 +5826,7 @@ const translations = { enterMagicCodeUpdate: ({contactMethod}: EnterMagicCodeParams) => `Por favor, introduce el código mágico enviado a ${contactMethod} para actualizar el nivel de acceso de tu copiloto.`, notAllowed: 'No tan rápido...', + noAccessMessage: 'Como copiloto, no tienes acceso a esta página. ¡Lo sentimos!', notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `No tienes permiso para realizar esta acción para ${accountOwnerEmail}`, notAllowedMessageHyperLinked: ' copiloto', }, diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index 7b0a76e59922..d99db40d07ee 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -3,6 +3,7 @@ import {useOnyx} from 'react-native-onyx'; import AddPlaidBankAccount from '@components/AddPlaidBankAccount'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmationPage from '@components/ConfirmationPage'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -13,6 +14,7 @@ import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirec import Navigation from '@libs/Navigation/Navigation'; import * as BankAccounts from '@userActions/BankAccounts'; import * as PaymentMethods from '@userActions/PaymentMethods'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -77,41 +79,43 @@ function AddPersonalBankAccountPage() { testID={AddPersonalBankAccountPage.displayName} > - - {shouldShowSuccess ? ( - exitFlow(true)} + + - ) : ( - 0} - submitButtonText={translate('common.saveAndContinue')} - scrollContextEnabled - onSubmit={submitBankAccountForm} - validate={BankAccounts.validatePlaidSelection} - style={[styles.mh5, styles.flex1]} - > - exitFlow(true)} /> - - )} + ) : ( + 0} + submitButtonText={translate('common.saveAndContinue')} + scrollContextEnabled + onSubmit={submitBankAccountForm} + validate={BankAccounts.validatePlaidSelection} + style={[styles.mh5, styles.flex1]} + > + + + )} + ); diff --git a/src/pages/AddressPage.tsx b/src/pages/AddressPage.tsx index 88e52409751b..31cffbe9b511 100644 --- a/src/pages/AddressPage.tsx +++ b/src/pages/AddressPage.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import AddressForm from '@components/AddressForm'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -10,6 +11,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {BackToParams} from '@libs/Navigation/types'; import type {FormOnyxValues} from '@src/components/Form/types'; import type {Country} from '@src/CONST'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/HomeAddressForm'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; @@ -81,27 +83,29 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true, backTo includeSafeAreaPaddingBottom={false} testID={AddressPage.displayName} > - Navigation.goBack(backTo)} - /> - {isLoadingApp ? ( - - ) : ( - + Navigation.goBack(backTo)} /> - )} + {isLoadingApp ? ( + + ) : ( + + )} + ); } diff --git a/src/pages/EnablePayments/EnablePayments.tsx b/src/pages/EnablePayments/EnablePayments.tsx index 357a2be9b1e0..b55141fec299 100644 --- a/src/pages/EnablePayments/EnablePayments.tsx +++ b/src/pages/EnablePayments/EnablePayments.tsx @@ -1,5 +1,6 @@ import React, {useEffect} from 'react'; import {useOnyx} from 'react-native-onyx'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -22,6 +23,7 @@ function EnablePaymentsPage() { const {isOffline} = useNetwork(); const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => !!account?.delegatedAccess?.delegate}); useEffect(() => { if (isOffline) { @@ -33,6 +35,18 @@ function EnablePaymentsPage() { } }, [isOffline, userWallet]); + if (isActingAsDelegate) { + return ( + + + + ); + } + if (isEmptyObject(userWallet)) { return ; } @@ -54,7 +68,6 @@ function EnablePaymentsPage() { } const currentStep = isEmptyObject(bankAccountList) ? CONST.WALLET.STEP.ADD_BANK_ACCOUNT : userWallet?.currentStep || CONST.WALLET.STEP.ADDITIONAL_DETAILS; - switch (currentStep) { case CONST.WALLET.STEP.ADD_BANK_ACCOUNT: return ; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 8bf3da6e33e0..5934afc09cd9 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -256,7 +256,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta const [moneyRequestReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`); const isMoneyRequestExported = ReportUtils.isExported(moneyRequestReportActions); - const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); + const {isDelegateAccessRestricted} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); const unapproveExpenseReportOrShowModal = useCallback(() => { @@ -942,7 +942,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta setIsNoDelegateAccessMenuVisible(false)} - delegatorEmail={delegatorEmail ?? ''} /> setIsNoDelegateAccessMenuVisible(false)} - delegatorEmail={delegatorEmail ?? ''} /> ); diff --git a/src/pages/settings/ExitSurvey/ExitSurveyBookCall.tsx b/src/pages/settings/ExitSurvey/ExitSurveyBookCall.tsx index 93191b809ee8..10085dd7a569 100644 --- a/src/pages/settings/ExitSurvey/ExitSurveyBookCall.tsx +++ b/src/pages/settings/ExitSurvey/ExitSurveyBookCall.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -22,58 +23,60 @@ function ExitSurveyBookCallPage() { return ( - Navigation.goBack()} - /> - - {isOffline && } - {!isOffline && ( - <> - {translate('exitSurvey.bookACallTitle')} - {translate('exitSurvey.bookACallTextTop')} - - {Object.values(CONST.EXIT_SURVEY.BENEFIT).map((value) => { - return ( - - - {translate(`exitSurvey.benefits.${value}`)} - - ); - })} - - {translate('exitSurvey.bookACallTextBottom')} - - )} - - -