Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle blocked copilot and Expensify card flows gracefully #52103

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a047d8a
show DelegateNoAccessModal for restricted delegate actions
ChavdaSachin Nov 6, 2024
b0465b9
Merge remote-tracking branch 'upstream/main' into fix-50796/Handle-bl…
ChavdaSachin Nov 7, 2024
f33b43f
LegalNamePage
ChavdaSachin Nov 13, 2024
cd5a453
DOB
ChavdaSachin Nov 13, 2024
61b69c1
PhoneNumber
ChavdaSachin Nov 13, 2024
feff84b
Address
ChavdaSachin Nov 13, 2024
7cadc0f
Wallet
ChavdaSachin Nov 13, 2024
285b061
cleanup
ChavdaSachin Nov 13, 2024
617282c
Switch To OD
ChavdaSachin Nov 13, 2024
7adfac1
Payment method menuItems
ChavdaSachin Nov 13, 2024
c1b09af
Payment method menuitems
ChavdaSachin Nov 13, 2024
6b74659
Delegate menu items
ChavdaSachin Nov 13, 2024
c095972
Dependent cleanup
ChavdaSachin Nov 13, 2024
cb314b3
SubscriptionPage
ChavdaSachin Nov 13, 2024
85da30a
lint
ChavdaSachin Nov 13, 2024
c6a2529
lint
ChavdaSachin Nov 13, 2024
3906089
Merge remote-tracking branch 'upstream/main' into fix-50796/Handle-bl…
ChavdaSachin Nov 13, 2024
ead9296
lint
ChavdaSachin Nov 13, 2024
853ccde
cleanup
ChavdaSachin Nov 13, 2024
887356c
DelegateNoAccessWrapper initial implementation
ChavdaSachin Nov 19, 2024
d3eb43f
Merge main
ChavdaSachin Nov 19, 2024
5df885f
Add DelegateNoAccessWrapper
ChavdaSachin Nov 20, 2024
08e7be3
Wrapper Use Guidelines
ChavdaSachin Nov 20, 2024
96d6216
Merge Main
ChavdaSachin Nov 20, 2024
4d1b7e1
Cleanup and lint
ChavdaSachin Nov 20, 2024
5fa58ff
Merge main and some minor changes
ChavdaSachin Nov 23, 2024
e68f388
Cleanup
ChavdaSachin Nov 23, 2024
a7d8cb3
Revert accidental chages to ios/Podfile.lock
ChavdaSachin Nov 23, 2024
43c3e6c
Show Not So Fast.. modal on clicking private profile credentials
ChavdaSachin Nov 25, 2024
79792d5
lint and some cleanup
ChavdaSachin Nov 25, 2024
bfe6d6e
prettier
ChavdaSachin Nov 25, 2024
97c9677
disable single execution for special case
ChavdaSachin Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 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,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 = (
<Text>
{noDelegateAccessPromptStart}
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 @@ -193,7 +193,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 @@ -592,7 +592,6 @@ function ReportPreview({
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onClose={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>

{isHoldMenuVisible && !!iouReport && requestType !== undefined && (
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
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={false}
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 @@ -942,7 +942,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onClose={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>
<ConfirmModal
title={translate('iou.unapproveReport')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function AttachmentPickerWithMenuItems({
const {translate} = useLocalize();
const {windowHeight, windowWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
const {isDelegateAccessRestricted} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`);
const {canUseCombinedTrackSubmit} = usePermissions();
Expand Down Expand Up @@ -370,7 +370,6 @@ function AttachmentPickerWithMenuItems({
<DelegateNoAccessModal
isNoDelegateAccessMenuVisible={isNoDelegateAccessMenuVisible}
onClose={() => setIsNoDelegateAccessMenuVisible(false)}
delegatorEmail={delegatorEmail ?? ''}
/>
</>
);
Expand Down
Loading