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
52 changes: 35 additions & 17 deletions src/hooks/useDefaultExpensePolicy.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
import type {OnyxCollection} from 'react-native-onyx';
import {isPaidGroupPolicy, isPolicyAccessible} from '@libs/PolicyUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useOnyx from './useOnyx';
import usePreferredPolicy from './usePreferredPolicy';

/**
* Selector that finds the single qualifying group policy ID from the collection.
* Returns only an ID (stable) — prevents re-renders when unrelated policies change.
*/
function getSingleGroupPolicyID(policies: OnyxCollection<Policy>, login: string): string | undefined {
if (!policies) {
return undefined;
}

let singlePolicyID: string | undefined;
for (const policy of Object.values(policies)) {
if (!policy || !isPaidGroupPolicy(policy) || !isPolicyAccessible(policy, login)) {
continue;
}
if (!singlePolicyID) {
singlePolicyID = policy.id;
} else {
return undefined; // More than one — no single default
}
}

return singlePolicyID;
}

export default function useDefaultExpensePolicy() {
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const {isRestrictedToPreferredPolicy, preferredPolicyID} = usePreferredPolicy();
const {login = ''} = useCurrentUserPersonalDetails();
const [preferredPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${preferredPolicyID}`);

// Selector returns only the qualifying policy ID — stable value, prevents re-renders
const [singleGroupPolicyID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
selector: (policies) => getSingleGroupPolicyID(policies, login),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we return the policy here instead of the ID, similar to what we did with useReportAttributesByID?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Policy objects are large, so returning the full object from a collection selector would require Onyx to deep compare it on every collection change, it might defeat the performance a bit
useReportAttributesByID works differently because it selects from a derived value where each entry is a small object, making deep comparison cheap. Here we're selecting from the full COLLECTION.POLICY, so returning only the stable ID and doing a per-key lookup is more efficient
what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep comparison is cheap (single small object)
selects from a derived value where each entry is a small object

Ah, you're right. I've been thinking how it can be cheap if it's a report object. I forgot that it's a report attributes 🤦. All good!

});

// Per-key lookup for the single group policy (only fires when that specific policy changes)
const [singleGroupPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${singleGroupPolicyID}`);

if (isRestrictedToPreferredPolicy && isPaidGroupPolicy(preferredPolicy) && isPolicyAccessible(preferredPolicy, login)) {
return preferredPolicy;
}
Expand All @@ -20,20 +53,5 @@ export default function useDefaultExpensePolicy() {
return activePolicy;
}

// If there is exactly one group policy, use that as the default expense policy
let singlePolicy;
for (const policy of Object.values(allPolicies ?? {})) {
if (!policy || !isPaidGroupPolicy(policy) || !isPolicyAccessible(policy, login)) {
continue;
}

if (!singlePolicy) {
singlePolicy = policy;
} else {
singlePolicy = undefined;
break;
}
}

return singlePolicy;
return singleGroupPolicy;
}
83 changes: 60 additions & 23 deletions src/hooks/usePolicyForMovingExpenses.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {activePolicySelector} from '@selectors/Policy';
import type {OnyxEntry} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {useSession} from '@components/OnyxListItemProvider';
import {canSubmitPerDiemExpenseFromWorkspace, isPaidGroupPolicy, isPolicyMemberWithoutPendingDelete, isTimeTrackingEnabled} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -31,48 +31,85 @@ function isPolicyValidForMovingExpenses(policy: OnyxEntry<Policy>, login: string
);
}

function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string) {
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {
selector: activePolicySelector,
});
type PolicyQualificationResult = {
singlePolicyID: string | undefined;
isMemberOfMoreThanOnePolicy: boolean;
validExpensePolicyID: string | undefined;
};

const session = useSession();
const login = session?.email ?? '';
/**
* Selector that computes which policies qualify for moving expenses.
* Returns only IDs and flags — stable output that prevents re-renders when unrelated policies change.
*/
function getPolicyQualificationResult(
policies: OnyxCollection<Policy>,
login: string,
isPerDiemRequest?: boolean,
isTimeRequest?: boolean,
expensePolicyID?: string,
): PolicyQualificationResult {
if (!policies) {
return {singlePolicyID: undefined, isMemberOfMoreThanOnePolicy: false, validExpensePolicyID: undefined};
}

// Early exit optimization: only need to check if we have 0, 1, or >1 policies
let singleUserPolicy;
let singlePolicyID: string | undefined;
let isMemberOfMoreThanOnePolicy = false;
for (const policy of Object.values(allPolicies ?? {})) {
for (const policy of Object.values(policies)) {
if (!isPolicyValidForMovingExpenses(policy, login, isPerDiemRequest, isTimeRequest)) {
continue;
}

if (!singleUserPolicy) {
singleUserPolicy = policy;
if (!singlePolicyID) {
singlePolicyID = policy?.id;
} else {
isMemberOfMoreThanOnePolicy = true;
break; // Found 2, no need to continue
break;
}
}

// If an expense policy ID is provided and valid, prefer it over the active policy
// This ensures that when viewing/editing an expense from workspace B, we show workspace B
// even if the user's default workspace is A
let validExpensePolicyID: string | undefined;
if (expensePolicyID) {
const expensePolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${expensePolicyID}`];
const expensePolicy = policies[`${ONYXKEYS.COLLECTION.POLICY}${expensePolicyID}`];
if (expensePolicy && isPolicyValidForMovingExpenses(expensePolicy, login, isPerDiemRequest, isTimeRequest)) {
return {policyForMovingExpensesID: expensePolicyID, policyForMovingExpenses: expensePolicy, shouldSelectPolicy: false};
validExpensePolicyID = expensePolicyID;
}
}

return {singlePolicyID, isMemberOfMoreThanOnePolicy, validExpensePolicyID};
}

function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string) {
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {
selector: activePolicySelector,
});

const session = useSession();
const login = session?.email ?? '';

// Contextual selector — captures login/flags from closure.
// Returns only IDs + flags (stable output) to prevent re-renders when unrelated policies change.
const policyQualificationSelector = (policies: OnyxCollection<Policy>) => getPolicyQualificationResult(policies, login, isPerDiemRequest, isTimeRequest, expensePolicyID);
const [qualificationResult] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
selector: policyQualificationSelector,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

});

const {singlePolicyID, isMemberOfMoreThanOnePolicy, validExpensePolicyID} = qualificationResult ?? {};

// Per-key lookup for the resolved policy (only fires when that specific policy changes)
const resolvedPolicyID = validExpensePolicyID ?? singlePolicyID;
const [resolvedPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${resolvedPolicyID}`);

// If an expense policy ID is provided and valid, prefer it over the active policy
if (validExpensePolicyID) {
return {policyForMovingExpensesID: validExpensePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false};
}

if (activePolicy && (!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(activePolicy)) && (!isTimeRequest || isTimeTrackingEnabled(activePolicy))) {
return {policyForMovingExpensesID: activePolicyID, policyForMovingExpenses: activePolicy, shouldSelectPolicy: false};
}

if (singleUserPolicy && !isMemberOfMoreThanOnePolicy) {
return {policyForMovingExpensesID: singleUserPolicy.id, policyForMovingExpenses: singleUserPolicy, shouldSelectPolicy: false};
if (singlePolicyID && !isMemberOfMoreThanOnePolicy) {
return {policyForMovingExpensesID: singlePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false};
}

if (isMemberOfMoreThanOnePolicy) {
Expand Down
14 changes: 14 additions & 0 deletions src/hooks/useReportAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,18 @@ function useReportAttributes() {
return reportAttributes?.reports;
}

/**
* Returns a single report's attributes using a selector.
* Deep comparison is cheap (single small object), so re-renders only occur
* when that specific report's attributes change — not on every global report change.
*/
function useReportAttributesByID(reportID: string | undefined) {
const reportAttributesByIDSelector = (value: {reports?: Record<string, unknown>} | undefined) => (reportID ? value?.reports?.[reportID] : undefined);
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {
selector: reportAttributesByIDSelector,
});
return reportAttributes;
}

export default useReportAttributes;
export {useReportAttributesByID};
9 changes: 6 additions & 3 deletions src/hooks/useReportIsArchived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import {isArchivedReport} from '@libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import useOnyx from './useOnyx';

const isArchivedSelector = isArchivedReport;

function useReportIsArchived(reportID?: string): boolean {
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`);
const isReportArchived = isArchivedReport(reportNameValuePairs);
return isReportArchived;
const [isArchived] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {
selector: isArchivedSelector,
});
return !!isArchived;
}

export default useReportIsArchived;
5 changes: 5 additions & 0 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@
};

let allPersonalDetails: OnyxTypes.PersonalDetailsList = {};
Onyx.connect({

Check warning on line 562 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand Down Expand Up @@ -678,7 +678,7 @@
};

let allTransactions: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 681 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -692,7 +692,7 @@
});

let allTransactionDrafts: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 695 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -701,7 +701,7 @@
});

let allTransactionViolations: NonNullable<OnyxCollection<OnyxTypes.TransactionViolations>> = {};
Onyx.connect({

Check warning on line 704 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -715,7 +715,7 @@
});

let allPolicyTags: OnyxCollection<OnyxTypes.PolicyTagLists> = {};
Onyx.connect({

Check warning on line 718 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY_TAGS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -728,7 +728,7 @@
});

let allReports: OnyxCollection<OnyxTypes.Report>;
Onyx.connect({

Check warning on line 731 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -737,7 +737,7 @@
});

let allReportNameValuePairs: OnyxCollection<OnyxTypes.ReportNameValuePairs>;
Onyx.connect({

Check warning on line 740 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -747,7 +747,7 @@

let deprecatedUserAccountID = -1;
let deprecatedCurrentUserEmail = '';
Onyx.connect({

Check warning on line 750 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
deprecatedCurrentUserEmail = value?.email ?? '';
Expand All @@ -756,7 +756,7 @@
});

let deprecatedCurrentUserPersonalDetails: OnyxEntry<OnyxTypes.PersonalDetails>;
Onyx.connect({

Check warning on line 759 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
deprecatedCurrentUserPersonalDetails = value?.[deprecatedUserAccountID] ?? undefined;
Expand All @@ -764,7 +764,7 @@
});

let allReportActions: OnyxCollection<OnyxTypes.ReportActions>;
Onyx.connect({

Check warning on line 767 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand Down Expand Up @@ -820,6 +820,10 @@
return deprecatedUserAccountID;
}

function getAllReportNameValuePairs(): OnyxCollection<OnyxTypes.ReportNameValuePairs> {
return allReportNameValuePairs;
}

/**
* This function uses Onyx.connect and should be replaced with useOnyx for reactive data access.
* TODO: remove `getPolicyTagsData` from this file (https://github.com/Expensify/App/issues/72721)
Expand Down Expand Up @@ -10214,6 +10218,7 @@
getAllReports,
getAllReportActionsFromIOU,
getAllTransactionDrafts,
getAllReportNameValuePairs,
getCurrentUserEmail,
getUserAccountID,
getReceiptError,
Expand Down
Loading