Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import getCreateReportRoute, {getReportsRootRoute, navigateToCreateReportWorkspa
import Navigation from '@libs/Navigation/Navigation';
import {openTravelDotLink} from '@libs/openTravelDotLink';
// eslint-disable-next-line no-restricted-imports -- TravelDot booking requires a paid workspace, matching the existing FAB behavior.
import {canSendInvoice, getDefaultChatEnabledPolicy, getGroupPoliciesWhereReportCanBeCreated, hasAcceptedTravelTerms, isPaidGroupPolicy, shouldShowPolicy} from '@libs/PolicyUtils';
import {canSendInvoice, getGroupPoliciesWhereReportCanBeCreated, hasAcceptedTravelTerms, isPaidGroupPolicy, shouldShowPolicy} from '@libs/PolicyUtils';
import {generateReportID} from '@libs/ReportUtils';

import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage';
Expand All @@ -37,8 +37,11 @@ import {primaryLoginSelector} from '@src/selectors/Account';
import {isTrackIntentUserSelector} from '@src/selectors/Onboarding';
import {emailSelector} from '@src/selectors/Session';
import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft';
import type {Policy} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';

import type {OnyxEntry} from 'react-native-onyx';

import {Str} from 'expensify-common';
import {useState} from 'react';

Expand Down Expand Up @@ -113,7 +116,6 @@ function useCreateNavigationSuggestions(query = ''): NavigationSuggestionSourceI
const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);

const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy([...groupPoliciesWithChatEnabled], activePolicy);
const isInvoiceVisible = canSendInvoice(allPolicies ?? null, sessionEmail);
const isTravelVisible = !!activePolicy?.isTravelEnabled;
const isBlockedFromSpotnanaTravel = isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL);
Expand Down Expand Up @@ -141,8 +143,8 @@ function useCreateNavigationSuggestions(query = ''): NavigationSuggestionSourceI
);

const {createReport, isVisible: isCreateReportVisible} = useCreateReport({
onCreateReport: (shouldDismissEmptyReportsConfirmation?: boolean) => {
if (!defaultChatEnabledPolicy?.id) {
onCreateReport: (policy: OnyxEntry<Policy>, shouldDismissEmptyReportsConfirmation?: boolean) => {
if (!policy?.id) {
return;
}

Expand All @@ -156,7 +158,7 @@ function useCreateNavigationSuggestions(query = ''): NavigationSuggestionSourceI
currentUserPersonalDetails,
false,
isBetaEnabled(CONST.BETAS.ASAP_SUBMIT),
defaultChatEnabledPolicy,
policy,
isTrackIntentUser,
getCurrencyDecimals,
rules,
Expand Down
19 changes: 13 additions & 6 deletions src/hooks/useCreateReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import {useCallback} from 'react';
import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useOnyx from './useOnyx';
import usePreferredPolicy from './usePreferredPolicy';
import useShouldShowEmptyReportConfirmation from './useShouldShowEmptyReportConfirmation';

type UseCreateReportParams = {
/** Callback that creates the report and navigates after creation */
onCreateReport: (shouldDismissEmptyReportsConfirmation?: boolean) => void;
/** Callback that creates the report on the resolved workspace and navigates after creation */
onCreateReport: (policy: OnyxEntry<OnyxTypes.Policy>, shouldDismissEmptyReportsConfirmation?: boolean) => void;
/** Group paid policies with expense chat enabled */
groupPoliciesWithChatEnabled: readonly never[] | Array<OnyxEntry<OnyxTypes.Policy>>;
/** Optional custom navigation to the workspace selector */
Expand Down Expand Up @@ -64,6 +65,7 @@ export default function useCreateReport({
const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
const {accountID} = useCurrentUserPersonalDetails();
const {isRestrictedToPreferredPolicy, preferredPolicyID} = usePreferredPolicy();

// Gate visibility and routing on policy hydration. Without this, during Onyx cold-start
// groupPoliciesWithChatEnabled.length === 0 would be true even for users who actually have
Expand All @@ -72,15 +74,18 @@ export default function useCreateReport({
const isVisible = arePoliciesLoaded;
const shouldNavigateToUpgradePath = groupPoliciesWithChatEnabled.length === 0;

const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<OnyxTypes.Policy>>, activePolicy);
// A domain security group can lock the user to a preferred workspace. It then takes precedence over the active policy and the selector is skipped.
const lockedPreferredPolicy = isRestrictedToPreferredPolicy ? groupPoliciesWithChatEnabled.find((policy) => policy?.id === preferredPolicyID) : undefined;
const isLockedToPreferredPolicy = !!lockedPreferredPolicy;
const defaultChatEnabledPolicy = lockedPreferredPolicy ?? getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<OnyxTypes.Policy>>, activePolicy) ?? undefined;
const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id;

const shouldShowEmptyReportConfirmation = useShouldShowEmptyReportConfirmation(defaultChatEnabledPolicyID, shouldSkipEmptyReportConfirmation);

const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({
policyID: defaultChatEnabledPolicyID,
policyName: defaultChatEnabledPolicy?.name ?? '',
onConfirm: onCreateReport,
onConfirm: (shouldDismissEmptyReportsConfirmation: boolean) => onCreateReport(defaultChatEnabledPolicy, shouldDismissEmptyReportsConfirmation),
shouldHandleNavigationBack,
});

Expand Down Expand Up @@ -118,8 +123,9 @@ export default function useCreateReport({
const hasMultipleNonPersonalWorkspaces = groupPoliciesWithChatEnabled.length > 1;
const isDefaultBillingRestricted =
!!workspaceIDForReportCreation && shouldRestrictUserBillableActions(defaultChatEnabledPolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID);
const shouldOfferAlternatives = !isLockedToPreferredPolicy && hasMultipleNonPersonalWorkspaces && (isDefaultPersonal || isDefaultBillingRestricted);

if (!workspaceIDForReportCreation || (isDefaultPersonal && hasMultipleNonPersonalWorkspaces) || (isDefaultBillingRestricted && hasMultipleNonPersonalWorkspaces)) {
if (!workspaceIDForReportCreation || shouldOfferAlternatives) {
if (onNavigateToWorkspaceSelection) {
onNavigateToWorkspaceSelection();
} else {
Expand All @@ -133,7 +139,7 @@ export default function useCreateReport({
if (shouldShowEmptyReportConfirmation) {
openCreateReportConfirmation();
} else {
onCreateReport(false);
onCreateReport(defaultChatEnabledPolicy, false);
}
return;
}
Expand All @@ -150,6 +156,7 @@ export default function useCreateReport({
userBillingGracePeriodEnds,
amountOwed,
accountID,
isLockedToPreferredPolicy,
groupPoliciesWithChatEnabled.length,
onNavigateToWorkspaceSelection,
shouldShowEmptyReportConfirmation,
Expand Down
10 changes: 4 additions & 6 deletions src/pages/Search/EmptySearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {startTestDrive} from '@libs/actions/Tour';
import DateUtils from '@libs/DateUtils';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
import {canSendInvoice, getDefaultChatEnabledPolicy, getGroupPoliciesWhereReportCanBeCreated} from '@libs/PolicyUtils';
import {canSendInvoice, getGroupPoliciesWhereReportCanBeCreated} from '@libs/PolicyUtils';
import {generateReportID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {getAllPolicyValues, getFilterFromQuery, isDefaultExpenseReportsQuery, isDefaultExpensesQuery, isSearchBeforeViolationsSnapshotStarted} from '@libs/SearchQueryUtils';
import {TODO_SEARCH_KEYS} from '@libs/SearchUIUtils';
Expand Down Expand Up @@ -157,8 +157,6 @@ function EmptySearchViewContent({
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);

const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<Policy>>, activePolicy);

const filteredPolicyID = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID);
let isFilteredWorkspaceAccessible = true;
if (filteredPolicyID.value) {
Expand All @@ -167,16 +165,16 @@ function EmptySearchViewContent({
isFilteredWorkspaceAccessible = !!filteredPolicy;
}

const handleCreateWorkspaceReport = (shouldDismissEmptyReportsConfirmation?: boolean) => {
if (!defaultChatEnabledPolicy?.id) {
const handleCreateWorkspaceReport = (policy: OnyxEntry<Policy>, shouldDismissEmptyReportsConfirmation?: boolean) => {
if (!policy?.id) {
return;
}

const {reportID: createdReportID} = createNewReport(
currentUserPersonalDetails,
hasViolations,
isASAPSubmitBetaEnabled,
defaultChatEnabledPolicy,
policy,
isTrackIntentUser,
getCurrencyDecimals,
rules,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import {createNewReport} from '@libs/actions/Report';
import getCreateReportRoute, {getReportsRootRoute, navigateToCreateReportWorkspaceSelection} from '@libs/Navigation/helpers/getCreateReportRoute';
import Navigation from '@libs/Navigation/Navigation';
import {getDefaultChatEnabledPolicy, getGroupPoliciesWhereReportCanBeCreated} from '@libs/PolicyUtils';
import {getGroupPoliciesWhereReportCanBeCreated} from '@libs/PolicyUtils';
import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';

import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage';
Expand All @@ -31,16 +31,13 @@ import React from 'react';

const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.CREATE_REPORT;

// Returns up to 2 matching policies
const chatEnabledPaidGroupPoliciesSelector = (policies: OnyxCollection<OnyxTypes.Policy>, currentUserLogin: string | undefined) =>
getGroupPoliciesWhereReportCanBeCreated(policies, currentUserLogin).slice(0, 2);
getGroupPoliciesWhereReportCanBeCreated(policies, currentUserLogin);

function CreateReportMenuItem() {
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const icons = useMemoizedLazyExpensifyIcons(['Document']);
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionEmailAndAccountIDSelector});
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
Expand All @@ -54,12 +51,10 @@ function CreateReportMenuItem() {
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);

const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<OnyxTypes.Policy>>, activePolicy);

const isReportInSearch = isOnSearchMoneyRequestReportPage();

const handleCreateWorkspaceReport = (shouldDismissEmptyReportsConfirmation?: boolean) => {
if (!defaultChatEnabledPolicy?.id) {
const handleCreateWorkspaceReport = (policy: OnyxEntry<OnyxTypes.Policy>, shouldDismissEmptyReportsConfirmation?: boolean) => {
if (!policy?.id) {
return;
}

Expand All @@ -71,7 +66,7 @@ function CreateReportMenuItem() {
currentUserPersonalDetails,
hasViolations,
isASAPSubmitBetaEnabled,
defaultChatEnabledPolicy,
policy,
isTrackIntentUser,
getCurrencyDecimals,
rules,
Expand Down
32 changes: 18 additions & 14 deletions tests/unit/CreateReportMenuItemTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ jest.mock('@libs/PolicyUtils', () => {
const CONSTANTS = jest.requireActual<{default: typeof CONST}>('@src/CONST').default;

return {
getDefaultChatEnabledPolicy: jest.fn((policies: Policy[]) => policies.at(0)),
getGroupPoliciesWhereReportCanBeCreated: jest.fn((policies: Record<string, Policy> | undefined) =>
Object.values(policies ?? {}).filter(
(policy): policy is Policy =>
Expand Down Expand Up @@ -101,21 +100,21 @@ function makePolicy(id: string, type: Policy['type']): Policy {
} as Policy;
}

function setupUseOnyx() {
function setupUseOnyx(activePolicyID = 'personal-1') {
const personalPolicy = makePolicy('personal-1', CONST.POLICY.TYPE.PERSONAL);
const groupPolicy = makePolicy('team-1', CONST.POLICY.TYPE.TEAM);
const submitPolicy = makePolicy('submit-1', CONST.POLICY.TYPE.SUBMIT);
const corporatePolicy = makePolicy('corporate-1', CONST.POLICY.TYPE.CORPORATE);
const policies = {
[`${ONYXKEYS.COLLECTION.POLICY}${personalPolicy.id}`]: personalPolicy,
[`${ONYXKEYS.COLLECTION.POLICY}${groupPolicy.id}`]: groupPolicy,
[`${ONYXKEYS.COLLECTION.POLICY}${submitPolicy.id}`]: submitPolicy,
[`${ONYXKEYS.COLLECTION.POLICY}${corporatePolicy.id}`]: corporatePolicy,
};
const values = new Map<string, unknown>([
[ONYXKEYS.NVP_ACTIVE_POLICY_ID, personalPolicy.id],
[`${ONYXKEYS.COLLECTION.POLICY}${personalPolicy.id}`, personalPolicy],
[
ONYXKEYS.COLLECTION.POLICY,
{
[`${ONYXKEYS.COLLECTION.POLICY}${personalPolicy.id}`]: personalPolicy,
[`${ONYXKEYS.COLLECTION.POLICY}${groupPolicy.id}`]: groupPolicy,
[`${ONYXKEYS.COLLECTION.POLICY}${submitPolicy.id}`]: submitPolicy,
},
],
[ONYXKEYS.NVP_ACTIVE_POLICY_ID, activePolicyID],
[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, policies[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]],
[ONYXKEYS.COLLECTION.POLICY, policies],
[ONYXKEYS.SESSION, {accountID: 1, email: 'user@test.com'}],
[ONYXKEYS.BETAS, []],
[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {}],
Expand All @@ -134,14 +133,19 @@ describe('CreateReportMenuItem', () => {
setupUseOnyx();
});

it('passes only report-creation workspaces to useCreateReport', () => {
it.each([
['the personal workspace is active', 'personal-1'],
['a workspace beyond the first two eligible ones is active', 'corporate-1'],
])('passes every report-creation workspace to useCreateReport when %s', (_description, activePolicyID) => {
setupUseOnyx(activePolicyID);

render(<CreateReportMenuItem />);

const params = mockUseCreateReport.mock.calls.at(0)?.at(0);
expect(params?.groupPoliciesWithChatEnabled).toHaveLength(2);
expect(params?.groupPoliciesWithChatEnabled).toEqual([
expect.objectContaining({id: 'team-1', type: CONST.POLICY.TYPE.TEAM}),
expect.objectContaining({id: 'submit-1', type: CONST.POLICY.TYPE.SUBMIT}),
expect.objectContaining({id: 'corporate-1', type: CONST.POLICY.TYPE.CORPORATE}),
]);
});
});
10 changes: 4 additions & 6 deletions tests/unit/useCreateNavigationSuggestionsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

type MockUseCreateReportParams = {
onCreateReport: (shouldDismissEmptyReportsConfirmation?: boolean) => void;
onCreateReport: (policy: unknown, shouldDismissEmptyReportsConfirmation?: boolean) => void;
groupPoliciesWithChatEnabled: unknown[] | readonly never[];
onNavigateToWorkspaceSelection: () => void;
shouldHandleNavigationBack: boolean;
Expand All @@ -37,7 +37,6 @@ const mockUseOnyx = jest.fn<unknown[], [key: string, options?: MockOnyxOptions]>
const isBetaEnabledByDefault = (beta: string) => beta !== CONST.BETAS.PREVENT_SPOTNANA_TRAVEL;
const mockIsBetaEnabled = jest.fn(isBetaEnabledByDefault);
const mockCanSendInvoice = jest.fn<boolean, unknown[]>(() => false);
const mockGetDefaultChatEnabledPolicy = jest.fn((policies: unknown[]) => (policies.length === 1 ? policies.at(0) : undefined));
const mockGetGroupPoliciesWhereReportCanBeCreated = jest.fn<unknown[], [policies: unknown, currentUserLogin?: string]>();
const mockShouldShowPolicy = jest.fn<boolean, unknown[]>(() => true);
const mockHasAcceptedTravelTerms = jest.fn(() => false);
Expand Down Expand Up @@ -165,7 +164,6 @@ jest.mock('@libs/openTravelDotLink', () => ({

jest.mock('@libs/PolicyUtils', () => ({
canSendInvoice: (...args: unknown[]) => mockCanSendInvoice(...args),
getDefaultChatEnabledPolicy: (policies: unknown[]) => mockGetDefaultChatEnabledPolicy(policies),
getGroupPoliciesWhereReportCanBeCreated: (policies: unknown, currentUserLogin?: string) => mockGetGroupPoliciesWhereReportCanBeCreated(policies, currentUserLogin),
hasAcceptedTravelTerms: () => mockHasAcceptedTravelTerms(),
isPaidGroupPolicy: () => mockIsPaidGroupPolicy(),
Expand Down Expand Up @@ -282,7 +280,7 @@ describe('useCreateNavigationSuggestions', () => {
renderHook(() => useCreateNavigationSuggestions());

const onCreateReport = mockUseCreateReport.mock.calls.at(0)?.at(0)?.onCreateReport;
act(() => onCreateReport?.());
act(() => onCreateReport?.(undefined));

expect(createNewReport).not.toHaveBeenCalled();
expect(Navigation.navigate).not.toHaveBeenCalled();
Expand Down Expand Up @@ -421,7 +419,7 @@ describe('useCreateNavigationSuggestions', () => {
renderHook(() => useCreateNavigationSuggestions());

const onCreateReport = mockUseCreateReport.mock.calls.at(0)?.at(0)?.onCreateReport;
act(() => onCreateReport?.(true));
act(() => onCreateReport?.(submitPolicy, true));

expect(createNewReport).toHaveBeenCalledWith(expect.anything(), false, true, submitPolicy, false, mockGetCurrencyDecimals, undefined, false, true);
expect(clearLastSearchParams).not.toHaveBeenCalled();
Expand All @@ -437,7 +435,7 @@ describe('useCreateNavigationSuggestions', () => {
mockIsOnSearchMoneyRequestReportPage.mockReturnValue(true);

const createReportParams = mockUseCreateReport.mock.calls.at(0)?.at(0);
act(() => createReportParams?.onCreateReport());
act(() => createReportParams?.onCreateReport(submitPolicy));
act(() => createReportParams?.onNavigateToWorkspaceSelection());

expect(clearLastSearchParams).toHaveBeenCalledTimes(1);
Expand Down
Loading
Loading