Skip to content
Merged
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 @@ -6,6 +6,7 @@ import MoneyRequestReceiptView from '@components/ReportActionItem/MoneyRequestRe
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView';

import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import useMarkOpenReportEndOnSkeleton from '@hooks/useMarkOpenReportEndOnSkeleton';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
Expand Down Expand Up @@ -122,7 +123,7 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport
const {isSmallScreenWidth} = useResponsiveLayout();

const reportID = report?.reportID;
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const {reportPendingAction, reportErrors: allReportErrors} = getReportOfflinePendingActionAndErrors(report);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.chatReportID)}`);

Expand Down Expand Up @@ -233,7 +234,7 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport
return;
}

if (isLoadingApp) {
if (isAppLoadPending) {
return (
<View style={styles.flex1}>
<ReportHeaderSkeletonView reasonAttributes={loadingAppReasonAttributes} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {ListItem, TextInputOptions} from '@components/SelectionList/types';

import {advancedSearchPoliciesSelector, useAdvancedSearchFiltersWorkspaces} from '@hooks/useAdvancedSearchFilters';
import useDebouncedState from '@hooks/useDebouncedState';
import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import useInitialValue from '@hooks/useInitialValue';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -40,7 +41,7 @@ function WorkspaceSelector({value = [], selectionListTextInputStyle, selectionLi
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const [policies = getEmptyObject<NonNullable<OnyxCollection<Policy>>>(), policiesResult] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: advancedSearchPoliciesSelector});
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
// Fetch the full (unfiltered) workspace list and apply the search filter locally, so pinning is decided from the
Expand Down Expand Up @@ -98,7 +99,7 @@ function WorkspaceSelector({value = [], selectionListTextInputStyle, selectionLi
itemCount={listData.length}
isSearchable={shouldShowWorkspaceSearchInput}
>
{isLoadingApp && !isOffline ? (
{isAppLoadPending && !isOffline ? (
<View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsCenter]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.SMALL}
Expand Down
35 changes: 33 additions & 2 deletions src/hooks/useInFlightRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {AnyRequest} from '@src/types/onyx';

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

import {useEffect} from 'react';

import useNetwork from './useNetwork';
import useOnyx from './useOnyx';

Expand Down Expand Up @@ -99,9 +101,38 @@ function useIsPendingInternal(group: PendingRequestGroup, scopeKey?: string | nu
return !!hasPendingPersistedRequest || !!hasPendingOngoingRequest;
}

/** Whether an OpenApp request is currently in the queue (the initial app load, not background reconnects). */
// Process-session memory: an OpenApp was seen in the queue this session and its deferred updates (whose
// finallyData clears IS_LOADING_APP) have not flushed yet. The sequential queue drops the request from
// PERSISTED_(ONGOING_)REQUESTS before it flushes those held updates, so `hasPendingOpenApp` alone goes
// false too early and the migrated screens would render cleared/stale data during that window. This latch
// keeps the gate pending across it. It is NOT a stored flag: a stranded IS_LOADING_APP read from disk on
// a fresh reload never sets it, because that reload runs ReconnectApp, not OpenApp. Keying on an observed
// OpenApp rather than HAS_LOADED_APP is what also covers an account switch, where HAS_LOADED_APP is
// already true but a real OpenApp still fires (see Delegate's atomic reset).
//
// This is deliberately module scoped, not a useRef: the observing consumer can unmount while the flush is
// still in progress (an account switch remounts screens), and a different consumer that mounts during the
// window must still see the latch. Reading a mutable module value during render is safe here because the
// only value the render combines it with is the reactive isLoadingApp, and the latch only changes inside
// the effect below, whose deps are exactly [hasPendingOpenApp, isLoadingApp]: any latch change is therefore
// accompanied by a dep change that re-renders every consumer, so no consumer can strand a stale read.
let hasObservedOpenAppFlushPending = false;

/** Whether an OpenApp request or its deferred Onyx updates are pending. */
function useIsAppLoadPending(): boolean {
return useIsPendingInternal('appLoad');
const hasPendingOpenApp = useIsPendingInternal('appLoad');
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);

useEffect(() => {
if (hasPendingOpenApp) {
hasObservedOpenAppFlushPending = true;
} else if (isLoadingApp !== true) {
// The flag cleared, so the deferred OpenApp updates flushed: stop covering the window.
hasObservedOpenAppFlushPending = false;
}
}, [hasPendingOpenApp, isLoadingApp]);

return hasPendingOpenApp || (hasObservedOpenAppFlushPending && isLoadingApp === true);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/useReportActionsListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type SCREENS from '@src/SCREENS';

import {useRoute} from '@react-navigation/native';

import {useIsAppLoadPending} from './useInFlightRequests';
import useLoadReportActions from './useLoadReportActions';
import useNetworkWithOfflineStatus from './useNetworkWithOfflineStatus';
import useOnyx from './useOnyx';
Expand Down Expand Up @@ -61,7 +62,7 @@ function useReportActionsListModel(reportID: string) {
const isReportArchived = useReportIsArchived(reportID);
const canPerformWriteAction = !!canUserPerformWriteAction(report, isReportArchived);

const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();

const [reportPaginationState] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE}${reportID}`);

Expand Down Expand Up @@ -117,7 +118,7 @@ function useReportActionsListModel(reportID: string) {
shouldBeAlignedToTop,
isLoadingInitialReportActions,
hasOnceLoadedReportActions,
isLoadingApp,
isLoadingApp: isAppLoadPending,
reportActionsLength: reportActions.length,
oldestUnreadReportAction,
isSingleExpenseReport,
Expand Down
7 changes: 4 additions & 3 deletions src/pages/DynamicNewReportWorkspaceSelectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useCreateNewReport from '@hooks/useCreateNewReport';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebouncedState from '@hooks/useDebouncedState';
import useDynamicBackPath from '@hooks/useDynamicBackPath';
import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -92,8 +93,8 @@ function DynamicNewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelec
const transactionIDs = selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs;
const [transactions] = useTransactionsByID(transactionIDs);

const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const shouldShowLoadingIndicator = isLoadingApp && !isOffline;
const isAppLoadPending = useIsAppLoadPending();
const shouldShowLoadingIndicator = isAppLoadPending && !isOffline;
const [pendingPolicySelection, setPendingPolicySelection] = useState<{policy: WorkspaceListItem; shouldShowEmptyReportConfirmation: boolean} | null>(null);

const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS);
Expand Down Expand Up @@ -276,7 +277,7 @@ function DynamicNewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelec
<View style={[styles.flex1, styles.fullScreenLoading]}>
<ActivityIndicator
size="large"
reasonAttributes={{context: 'DynamicNewReportWorkspaceSelectionPage', isLoadingApp: !!isLoadingApp}}
reasonAttributes={{context: 'DynamicNewReportWorkspaceSelectionPage', isLoadingApp: isAppLoadPending}}
/>
</View>
) : (
Expand Down
7 changes: 4 additions & 3 deletions src/pages/DynamicReportChangeWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import UserListItem from '@components/SelectionList/ListItem/UserListItem';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebouncedState from '@hooks/useDebouncedState';
import useDynamicBackPath from '@hooks/useDynamicBackPath';
import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
Expand Down Expand Up @@ -73,13 +74,13 @@ function DynamicReportChangeWorkspacePage({report}: DynamicReportChangeWorkspace
const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [reportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`);
const [isChangePolicyTrainingModalDismissed = false] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {selector: changePolicyTrainingModalDismissedSelector});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const isReportLastVisibleArchived = useReportIsArchived(report?.parentReportID);
const [submitterLogin] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsLoginSelector(report?.ownerAccountID)}, [report?.ownerAccountID]);
const [doesSubmitterPersonalDetailExist] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: doesPersonalDetailExistSelector(report?.ownerAccountID)}, [report?.ownerAccountID]);
const [managerLogin] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsLoginSelector(report?.managerID)}, [report?.managerID]);
const shouldShowLoadingIndicator = isLoadingApp && !isOffline;
const shouldShowLoadingIndicator = isAppLoadPending && !isOffline;
const {isBetaEnabled} = usePermissions();
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const session = useSession();
Expand Down Expand Up @@ -213,7 +214,7 @@ function DynamicReportChangeWorkspacePage({report}: DynamicReportChangeWorkspace
<View style={[styles.flex1, styles.fullScreenLoading]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
reasonAttributes={{context: 'DynamicReportChangeWorkspacePage', isLoadingApp: !!isLoadingApp}}
reasonAttributes={{context: 'DynamicReportChangeWorkspacePage', isLoadingApp: isAppLoadPending}}
/>
</View>
) : (
Expand Down
7 changes: 4 additions & 3 deletions src/pages/SetDefaultWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {WorkspaceListItemType} from '@components/SelectionList/ListItem/typ
import UserListItem from '@components/SelectionList/ListItem/UserListItem';

import useDebouncedState from '@hooks/useDebouncedState';
import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
Expand Down Expand Up @@ -39,10 +40,10 @@ function SetDefaultWorkspacePage({route}: SetDefaultWorkspacePageProps) {
const {translate, localeCompare} = useLocalize();

const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);

const shouldShowLoadingIndicator = isLoadingApp && !isOffline;
const shouldShowLoadingIndicator = isAppLoadPending && !isOffline;
const session = useSession();

const selectPolicy = (selectedPolicyID?: string) => {
Expand Down Expand Up @@ -103,7 +104,7 @@ function SetDefaultWorkspacePage({route}: SetDefaultWorkspacePageProps) {
<View style={[styles.flex1, styles.fullScreenLoading]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
reasonAttributes={{context: 'SetDefaultWorkspacePage', isLoadingApp: !!isLoadingApp}}
reasonAttributes={{context: 'SetDefaultWorkspacePage', isLoadingApp: isAppLoadPending}}
/>
</View>
) : (
Expand Down
5 changes: 3 additions & 2 deletions src/pages/domain/DomainsListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WorkspaceListLayout from '@components/WorkspaceListLayout';

import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDocumentTitle from '@hooks/useDocumentTitle';
import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -36,7 +37,7 @@ function DomainsListPage() {

useDocumentTitle(translate('common.domains'));

const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const [allDomains] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN);
const [allDomainErrors] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN_ERRORS);

Expand All @@ -49,7 +50,7 @@ function DomainsListPage() {
};

const domainRows: DomainRowData[] = [];
const shouldShowLoadingIndicator = !!isLoadingApp && !isOffline;
const shouldShowLoadingIndicator = isAppLoadPending && !isOffline;

if (!isEmptyObject(allDomains)) {
for (const domain of Object.values(allDomains)) {
Expand Down
5 changes: 3 additions & 2 deletions src/pages/inbox/ReportActions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import MoneyRequestReportActionsList from '@components/MoneyRequestReportView/MoneyRequestReportActionsList';

import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import useMarkOpenReportEndOnSkeleton from '@hooks/useMarkOpenReportEndOnSkeleton';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
Expand Down Expand Up @@ -46,7 +47,7 @@ function ReportActions() {
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`);
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
const [reportLoadingState = defaultReportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportIDFromRoute}`);
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const {reportActions} = usePaginatedReportActions(reportIDFromRoute);

const allReportTransactions = useReportTransactionsCollection(reportIDFromRoute);
Expand All @@ -65,7 +66,7 @@ function ReportActions() {
// Concierge is excluded so the body still mounts under the app-load skeleton, seeding sessionStartTime
// before content appeared.
const isConciergeMainDM = isConciergeChatReport(report, conciergeReportID);
const shouldShowAppLoadSkeleton = !!isLoadingApp && !isOffline && !!report && !shouldWaitForTransactions && !shouldDisplayMoneyRequestActionsList && !isConciergeMainDM;
const shouldShowAppLoadSkeleton = isAppLoadPending && !isOffline && !!report && !shouldWaitForTransactions && !shouldDisplayMoneyRequestActionsList && !isConciergeMainDM;

useMarkOpenReportEndOnSkeleton(report, shouldShowAppLoadSkeleton);

Expand Down
7 changes: 4 additions & 3 deletions src/pages/settings/Profile/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Section from '@components/Section';

import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDocumentTitle from '@hooks/useDocumentTitle';
import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import {useMemoizedLazyAsset, useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
Expand Down Expand Up @@ -69,7 +70,7 @@ function ProfilePage() {
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const route = useRoute<PlatformStackRouteProp<SettingsSplitNavigatorParamList, typeof SCREENS.SETTINGS.PROFILE.ROOT>>();
useDocumentTitle(translate('common.profile'));
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const getPronouns = (): string => {
const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? '';
return pronounsKey ? translate(`pronouns.${pronounsKey}` as TranslationPaths) : translate('profilePage.selectYourPronouns');
Expand Down Expand Up @@ -182,7 +183,7 @@ function ProfilePage() {

const privateSectionReasonAttributes: SkeletonSpanReasonAttributes = {
context: 'ProfilePage.privateSection',
isLoadingApp: !!isLoadingApp,
isLoadingApp: isAppLoadPending,
};

return (
Expand Down Expand Up @@ -292,7 +293,7 @@ function ProfilePage() {
childrenStyles={styles.pt3}
titleStyles={styles.accountSettingsSectionTitle}
>
{isLoadingApp ? (
{isAppLoadPending ? (
<View style={[styles.flex1, styles.pRelative, StyleUtils.getBackgroundColorStyle(theme.cardBG)]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
Expand Down
7 changes: 4 additions & 3 deletions src/pages/settings/Wallet/WalletPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDelegateAccountID from '@hooks/useDelegateAccountID';
import useDocumentTitle from '@hooks/useDocumentTitle';
import {useIsAppLoadPending} from '@hooks/useInFlightRequests';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -89,7 +90,7 @@ function WalletPage() {
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY);
const [walletTerms = getEmptyObject<OnyxTypes.WalletTerms>()] = useOnyx(ONYXKEYS.WALLET_TERMS);
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const isAppLoadPending = useIsAppLoadPending();
const [userAccount] = useOnyx(ONYXKEYS.ACCOUNT);
const [lastUsedPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD);
const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID);
Expand Down Expand Up @@ -661,8 +662,8 @@ function WalletPage() {
];
}, [bottomMountItem, confirmDeleteCard, icons.MoneySearch, icons.Table, icons.Trashcan, paymentMethod.methodID, selectedCard?.bank, shouldUseNarrowLayout, translate]);

if (isLoadingApp) {
const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'WalletPage', isLoadingApp: !!isLoadingApp};
if (isAppLoadPending) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep skeletons gated until OpenApp updates are flushed

When another write follows OpenApp in the sequential queue, this becomes false before the app data is available: SequentialQueue.process() removes the completed request at src/libs/Network/SequentialQueue.ts:199-203, while write response/finally updates are deliberately held until the entire queue is empty and flushed at lines 424-427. This occurs, for example, when clearOnyxAndResetApp() enqueues OpenApp and then restores previously queued writes; a slow or retrying restored write can therefore make Wallet and the other migrated screens render cleared/stale Onyx data for an extended period, whereas IS_LOADING_APP remained true until that flush applied its finallyData.

Useful? React with 👍 / 👎.

const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'WalletPage', isLoadingApp: isAppLoadPending};
return (
<ScreenWrapper
testID="WalletPage"
Expand Down
Loading
Loading