Skip to content

Commit

Permalink
Merge pull request Expensify#52103 from ChavdaSachin/fix-50796/Handle…
Browse files Browse the repository at this point in the history
…-blocked-copilot-and-Expensify-card-flows-gracefully

Handle blocked copilot and Expensify card flows gracefully
  • Loading branch information
Julesssss authored Dec 6, 2024
2 parents a9bf599 + edfbbcd commit b9107bf
Show file tree
Hide file tree
Showing 48 changed files with 1,101 additions and 688 deletions.
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4565,6 +4565,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',
Expand Down
12 changes: 7 additions & 5 deletions src/components/DelegateNoAccessModal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -8,18 +9,19 @@ 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');
const noDelegateAccessHyperLinked = translate('delegate.notAllowedMessageHyperLinked');

const noDelegateAccessPromptEnd = translate('delegate.notAllowedMessageEnd', {accountOwnerEmail: delegatorEmail ?? ''});
const delegateNoAccessPrompt = (
<Text>
{noDelegateAccessPromptStart}
<TextLink href={CONST.DELEGATE_ROLE_HELPDOT_ARTICLE_LINK}>{noDelegateAccessHyperLinked}</TextLink>.
<TextLink href={CONST.DELEGATE_ROLE_HELPDOT_ARTICLE_LINK}>{noDelegateAccessHyperLinked}</TextLink>
{noDelegateAccessPromptEnd}
</Text>
);

Expand Down
67 changes: 67 additions & 0 deletions src/components/DelegateNoAccessWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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<Account>) => isDelegate(account),
// To Restrict Only Limited Access Delegates From Accessing The Page.
[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.SUBMITTER]: (account: OnyxEntry<Account>) => isSubmitter(account),
} as const satisfies Record<string, (account: OnyxEntry<Account>) => boolean>;

type AccessDeniedVariants = keyof typeof DENIED_ACCESS_VARIANTS;

type DelegateNoAccessWrapperProps = {
accessDeniedVariants?: AccessDeniedVariants[];
shouldForceFullScreen?: boolean;
children?: (() => React.ReactNode) | React.ReactNode;
};

function isDelegate(account: OnyxEntry<Account>) {
const isActingAsDelegate = !!account?.delegatedAccess?.delegate;
return isActingAsDelegate;
}

function isSubmitter(account: OnyxEntry<Account>) {
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 (
<FullPageNotFoundView
shouldShow
shouldForceFullScreen={shouldForceFullScreen}
onBackButtonPress={() => {
if (shouldUseNarrowLayout) {
Navigation.dismissModal();
return;
}
Navigation.goBack();
}}
titleKey="delegate.notAllowed"
subtitleKey="delegate.noAccessMessage"
shouldShowLink={false}
/>
);
}
return callOrReturn(props.children);
}

export default DelegateNoAccessWrapper;
3 changes: 1 addition & 2 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -494,7 +494,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onClose={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>

<ConfirmModal
Expand Down
3 changes: 1 addition & 2 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function ReportPreview({
[chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled],
);

const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
const {isDelegateAccessRestricted} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);

const stopAnimation = useCallback(() => setIsPaidAnimationRunning(false), []);
Expand Down Expand Up @@ -590,7 +590,6 @@ function ReportPreview({
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onClose={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>

{isHoldMenuVisible && !!iouReport && requestType !== undefined && (
Expand Down
4 changes: 3 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5341,8 +5341,10 @@ 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...',
notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `You don't have permission to take this action for ${accountOwnerEmail} as a`,
noAccessMessage: "As a copilot, you don't have access to \nthis page. Sorry!",
notAllowedMessageStart: `As a`,
notAllowedMessageHyperLinked: ' copilot',
notAllowedMessageEnd: ({accountOwnerEmail}: AccountOwnerParams) => ` for ${accountOwnerEmail}, you don't have permission to take this action. Sorry!`,
},
debug: {
debug: 'Debug',
Expand Down
4 changes: 3 additions & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5861,8 +5861,10 @@ 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...',
notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `No tienes permiso para realizar esta acción para ${accountOwnerEmail}`,
noAccessMessage: 'Como copiloto, no tienes acceso a esta página. ¡Lo sentimos!',
notAllowedMessageStart: `Como`,
notAllowedMessageHyperLinked: ' copiloto',
notAllowedMessageEnd: ({accountOwnerEmail}: AccountOwnerParams) => ` de ${accountOwnerEmail}, no tienes permiso para realizar esta acción. ¡Lo siento!`,
},
debug: {
debug: 'Depuración',
Expand Down
70 changes: 37 additions & 33 deletions src/pages/AddPersonalBankAccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -77,41 +79,43 @@ function AddPersonalBankAccountPage() {
testID={AddPersonalBankAccountPage.displayName}
>
<FullPageNotFoundView shouldShow={!isUserValidated}>
<HeaderWithBackButton
title={translate('bankAccount.addBankAccount')}
onBackButtonPress={exitFlow}
/>
{shouldShowSuccess ? (
<ConfirmationPage
heading={translate('addPersonalBankAccountPage.successTitle')}
description={translate('addPersonalBankAccountPage.successMessage')}
shouldShowButton
buttonText={translate('common.continue')}
onButtonPress={() => exitFlow(true)}
<DelegateNoAccessWrapper accessDeniedVariants={[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]}>
<HeaderWithBackButton
title={translate('bankAccount.addBankAccount')}
onBackButtonPress={exitFlow}
/>
) : (
<FormProvider
formID={ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM}
isSubmitButtonVisible={(plaidData?.bankAccounts ?? []).length > 0}
submitButtonText={translate('common.saveAndContinue')}
scrollContextEnabled
onSubmit={submitBankAccountForm}
validate={BankAccounts.validatePlaidSelection}
style={[styles.mh5, styles.flex1]}
>
<InputWrapper
inputID={INPUT_IDS.BANK_INFO_STEP.SELECTED_PLAID_ACCOUNT_ID}
InputComponent={AddPlaidBankAccount}
onSelect={setSelectedPlaidAccountId}
text={translate('walletPage.chooseAccountBody')}
plaidData={plaidData}
isDisplayedInWalletFlow
onExitPlaid={goBack}
receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()}
selectedPlaidAccountID={selectedPlaidAccountId}
{shouldShowSuccess ? (
<ConfirmationPage
heading={translate('addPersonalBankAccountPage.successTitle')}
description={translate('addPersonalBankAccountPage.successMessage')}
shouldShowButton
buttonText={translate('common.continue')}
onButtonPress={() => exitFlow(true)}
/>
</FormProvider>
)}
) : (
<FormProvider
formID={ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM}
isSubmitButtonVisible={(plaidData?.bankAccounts ?? []).length > 0}
submitButtonText={translate('common.saveAndContinue')}
scrollContextEnabled
onSubmit={submitBankAccountForm}
validate={BankAccounts.validatePlaidSelection}
style={[styles.mh5, styles.flex1]}
>
<InputWrapper
inputID={INPUT_IDS.BANK_INFO_STEP.SELECTED_PLAID_ACCOUNT_ID}
InputComponent={AddPlaidBankAccount}
onSelect={setSelectedPlaidAccountId}
text={translate('walletPage.chooseAccountBody')}
plaidData={plaidData}
isDisplayedInWalletFlow
onExitPlaid={goBack}
receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()}
selectedPlaidAccountID={selectedPlaidAccountId}
/>
</FormProvider>
)}
</DelegateNoAccessWrapper>
</FullPageNotFoundView>
</ScreenWrapper>
);
Expand Down
44 changes: 24 additions & 20 deletions src/pages/AddressPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -81,27 +83,29 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true, backTo
includeSafeAreaPaddingBottom
testID={AddressPage.displayName}
>
<HeaderWithBackButton
title={title}
shouldShowBackButton
onBackButtonPress={() => Navigation.goBack(backTo)}
/>
{isLoadingApp ? (
<FullscreenLoadingIndicator style={[styles.flex1, styles.pRelative]} />
) : (
<AddressForm
formID={ONYXKEYS.FORMS.HOME_ADDRESS_FORM}
onSubmit={updateAddress}
submitButtonText={translate('common.save')}
city={city}
country={currentCountry}
onAddressChanged={handleAddressChange}
state={state}
street1={street1}
street2={street2}
zip={zipcode}
<DelegateNoAccessWrapper accessDeniedVariants={[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]}>
<HeaderWithBackButton
title={title}
shouldShowBackButton
onBackButtonPress={() => Navigation.goBack(backTo)}
/>
)}
{isLoadingApp ? (
<FullscreenLoadingIndicator style={[styles.flex1, styles.pRelative]} />
) : (
<AddressForm
formID={ONYXKEYS.FORMS.HOME_ADDRESS_FORM}
onSubmit={updateAddress}
submitButtonText={translate('common.save')}
city={city}
country={currentCountry}
onAddressChanged={handleAddressChange}
state={state}
street1={street1}
street2={street2}
zip={zipcode}
/>
)}
</DelegateNoAccessWrapper>
</ScreenWrapper>
);
}
Expand Down
15 changes: 14 additions & 1 deletion src/pages/EnablePayments/EnablePayments.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand All @@ -33,6 +35,18 @@ function EnablePaymentsPage() {
}
}, [isOffline, userWallet]);

if (isActingAsDelegate) {
return (
<ScreenWrapper
testID={EnablePaymentsPage.displayName}
includeSafeAreaPaddingBottom={false}
shouldEnablePickerAvoiding={false}
>
<DelegateNoAccessWrapper accessDeniedVariants={[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]} />
</ScreenWrapper>
);
}

if (isEmptyObject(userWallet)) {
return <FullScreenLoadingIndicator />;
}
Expand All @@ -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 <AddBankAccount />;
Expand Down
3 changes: 1 addition & 2 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -986,7 +986,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onClose={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>
<ConfirmModal
title={translate('iou.unapproveReport')}
Expand Down
Loading

0 comments on commit b9107bf

Please sign in to comment.