Skip to content

Commit be97d97

Browse files
committed
merge main and resolve conflicts.
Signed-off-by: krishna2323 <belivethatkg@gmail.com>
2 parents 701e807 + f7dc9fd commit be97d97

466 files changed

Lines changed: 4360 additions & 2987 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Mobile-Expensify

config/eslint/eslint.seatbelt.tsv

Lines changed: 2 additions & 65 deletions
Large diffs are not rendered by default.

contributingGuides/LOADING_STATE.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ const shouldShowLoadingIndicator = isAppLoadPending && !isOffline;
5757

5858
// ...
5959
{shouldShowLoadingIndicator ? (
60-
<ActivityIndicator
61-
reasonAttributes={{context: 'WorkspacesListPage', isOffline} satisfies SkeletonSpanReasonAttributes}
62-
/>
60+
<ActivityIndicator />
6361
) : (
6462
<WorkspaceListTable workspaces={workspaceRows} />
6563
)}
@@ -172,5 +170,3 @@ Keep `HAS_LOADED_APP` and the cold-restart fallback. The queue hook is the prima
172170
Report skeleton consumers use `useIsReportLoadPending(reportID)` wherever pending `OpenReport` work is part of the loading decision. They keep existing readiness checks, including `hasOnceLoadedReportActions`, report data completeness, and offline behavior. A stranded `isLoadingInitialReportActions` value without a matching queue request or in-memory latch must not show a skeleton.
173171

174172
Do not remove the legacy fields as part of this migration. `IS_LOADING_APP` and report loading state still support recovery, report positioning, navigation guards, and the deferred-flush bridge. Skeleton consumers should use the public hooks. Full flag deletion is outside this plan.
175-
176-
**Keep telemetry for a terminal request that still shows a skeleton.** `useSkeletonSpan` in `src/libs/telemetry/useSkeletonSpan.ts` and the `reasonAttributes` prop on `ActivityIndicator` record a span while a skeleton is mounted. They flag skeletons that remain past `CONST.TELEMETRY.CONFIG.SKELETON_MIN_DURATION`. Pass `SkeletonSpanReasonAttributes` with a `context` for the screen and any state that explains the render, such as `isOffline`. This makes the case queryable under the `skeleton.` namespace.

contributingGuides/OBSERVABILITY.md

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,6 @@ Minimum set of parameters required to create a span:
3737

3838
**Span configuration**: Set of parameters passed to `Sentry.startInteractiveSpan`. See the [Sentry docs](https://docs.sentry.io/platforms/react-native/tracing/instrumentation/custom-instrumentation/#starting-inactive-spans-startinactivespan) for more details.
3939

40-
Additional parameters can be added as a config object (third parameter):
41-
42-
**Minimum Duration**: what's the minimum duration of a span. Spans shorter than this duration are discarded.
43-
44-
```typescript
45-
startSpan(CONST.TELEMETRY.SPAN_SKELETON, {
46-
name: CONST.TELEMETRY.SPAN_SKELETON,
47-
op: CONST.TELEMETRY.SPAN_SKELETON,
48-
}, {minDuration: CONST.TELEMETRY.CONFIG.SKELETON_MIN_DURATION});
49-
```
50-
5140
#### Finishing a Span
5241

5342
There are two ways to finish a span:
@@ -72,7 +61,7 @@ Defined in `src/CONST/index.ts` under `CONST.TELEMETRY`:
7261
- Span names: `SPAN_OPEN_REPORT`, `SPAN_SEND_MESSAGE`
7362
- Tag names: `TAGS.ACTIVE_POLICY`, `TAGS.AUTHENTICATION_ERROR_TYPE`
7463
- Attribute names: `ATTRIBUTE_REPORT_ID`, `ATTRIBUTE_MESSAGE_LENGTH`
75-
- Configuration: `CONFIG.SKELETON_MIN_DURATION`
64+
- Configuration: `CONFIG.MEMORY_TRACKING_INTERVAL`
7665

7766
#### Naming Conventions
7867

@@ -86,7 +75,7 @@ Defined in `src/CONST/index.ts` under `CONST.TELEMETRY`:
8675
### Middleware
8776

8877
Process events before sending to Sentry:
89-
- **minDurationFilter** - Discards spans shorter than a specified duration
78+
- **maxDurationFilter** - Discards spans longer than a specified duration
9079
- **scopeTagsEnricher** - Adds cohort and policy tags
9180
- **emailDomainFilter** - Removes accounts we don't want to send telemetry for
9281

@@ -115,7 +104,6 @@ Error conditions tracked for trend analysis:
115104

116105
- **ANRs**: number of "Application Not Responding" errors
117106
- **404 pages**: number of user actions other than deep links that result in 404
118-
- **Infinite skeletons**: Skeleton visible 10+ seconds
119107
- **Authentication failures**: number of authentication errors other than wrong credentials
120108

121109
### Feature Health

contributingGuides/OBSERVABILITY_METRICS.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,6 @@ This document lists all implemented telemetry metrics in the Expensify App.
172172
**End**: Immediately after start (tracking occurrence, not duration) ([`src/libs/telemetry/useAbsentPageSpan.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/libs/telemetry/useAbsentPageSpan.ts#L39))
173173
**Attributes**: `url`, `navigationSource: 'deeplink' | 'button'`
174174

175-
### Infinite Skeletons
176-
177-
**Constant**: `CONST.TELEMETRY.SPAN_SKELETON`
178-
**Sentry Name**: `ManualSkeleton`
179-
**Threshold**: 10s minimum duration (only sent if visible 10+ seconds)
180-
**What's Measured**: Number of skeleton components visible longer than expected
181-
**Start**: Skeleton component mounted ([`src/libs/telemetry/useSkeletonSpan.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/libs/telemetry/useSkeletonSpan.ts#L13))
182-
**End**: Component unmounts ([`src/libs/telemetry/useSkeletonSpan.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/libs/telemetry/useSkeletonSpan.ts#L24))
183-
**Span ID**: `${CONST.TELEMETRY.SPAN_SKELETON}_${component}_${reactId}`
184-
**Minimum Duration**: `CONST.TELEMETRY.CONFIG.SKELETON_MIN_DURATION` (10s)
185-
186175
### Authentication Failures
187176

188177
**Constants**: `CONST.TELEMETRY.TAGS.AUTHENTICATION_FUNCTION`, `CONST.TELEMETRY.TAGS.AUTHENTICATION_ERROR_TYPE`, `CONST.TELEMETRY.TAGS.AUTHENTICATION_JSON_CODE` ([`src/CONST/index.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/CONST/index.ts#L1700-L1702))

patches/react-native/details.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,10 @@
333333
- Upstream PR/issue: 🛑
334334
- E/App issue: https://github.com/Expensify/App/issues/57556
335335
- PR introducing patch: https://github.com/Expensify/App/pull/94332
336+
337+
### [react-native+0.85.3+040+fix-find-shadow-node-uaf-APP-HNA.patch](react-native+0.85.3+040+fix-find-shadow-node-uaf-APP-HNA.patch)
338+
339+
- Reason: Fixes a fatal Android HybridApp crash (APP-HNA) — a SIGSEGV in `findShadowNodeByTagRecursively` reached via `FabricUIManagerBinding::findNextFocusableElement` during focus navigation (D-pad / hardware-keyboard Tab / accessibility focus) inside a scroll view. `UIManager::findShadowNodeByTag_DEPRECATED` has two paths gated on RN's `fixFindShadowNodeByTagRaceCondition` feature flag: the safe path holds the root node alive via a `shared_ptr` for the entire traversal, while the flag-off path grabs a raw root pointer via `tryCommit` (immediately cancelled) that keeps nothing alive. The flag defaults to `false`, so a concurrent commit/unmount on the background thread can free the shadow subtree mid-traversal, leaving a dangling `shared_ptr` that segfaults at `ShadowNode::getTag()`. This patch removes the flag gate and unconditionally uses the safe `shared_ptr`-holding path. Upstream removed the flag (making the safe path the default) in RN 0.87.0, so this patch can be dropped once we upgrade to RN >= 0.87.0.
340+
- Upstream PR/issue: https://github.com/facebook/react-native/pull/55751 (introduced the fix behind the `fixFindShadowNodeByTagRaceCondition` flag) and https://github.com/facebook/react-native/pull/56850 (removed the flag in RN 0.87.0)
341+
- E/App issue: https://github.com/Expensify/App/issues/97471
342+
- PR introducing patch: https://github.com/Expensify/App/pull/97496
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
diff --git a/node_modules/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/node_modules/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp
2+
--- a/node_modules/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp
3+
+++ b/node_modules/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp
4+
@@ -522,30 +522,16 @@ std::shared_ptr<const ShadowNode> UIManager::findShadowNodeByTag_DEPRECATED(
5+
auto shadowNode = std::shared_ptr<const ShadowNode>{};
6+
7+
shadowTreeRegistry_.enumerate([&](const ShadowTree& shadowTree, bool& stop) {
8+
- // Obtain a pointer to the root node. The flag-gated path uses
9+
- // getCurrentRevision() which keeps the root alive via shared_ptr for
10+
- // the entire traversal, fixing a use-after-free race condition.
11+
- RootShadowNode::Shared rootShadowNodeHolder;
12+
- const RootShadowNode* rootShadowNode = nullptr;
13+
- if (ReactNativeFeatureFlags::fixFindShadowNodeByTagRaceCondition()) {
14+
- rootShadowNodeHolder = shadowTree.getCurrentRevision().rootShadowNode;
15+
- rootShadowNode = rootShadowNodeHolder.get();
16+
- } else {
17+
- // TODO(T257154369): Remove after flag rollout.
18+
- // The public interface of `ShadowTree` discourages accessing a stored
19+
- // pointer to a root node because of the possible data race.
20+
- // To work around this, we ask for a commit and immediately cancel it
21+
- // returning `nullptr` instead of a new shadow tree.
22+
- // We don't want to add a way to access a stored pointer to a root
23+
- // node because this `findShadowNodeByTag` is deprecated. It is only
24+
- // added to make migration to the new architecture easier.
25+
- shadowTree.tryCommit(
26+
- [&](const RootShadowNode& oldRootShadowNode) {
27+
- rootShadowNode = &oldRootShadowNode;
28+
- return nullptr;
29+
- },
30+
- {/* default commit options */});
31+
- }
32+
+ // APP-HNA: Always keep the root node alive via a shared_ptr for the entire
33+
+ // traversal to avoid a use-after-free race (SIGSEGV crash in
34+
+ // findShadowNodeByTagRecursively when the shadow subtree is torn down on a
35+
+ // background thread mid-traversal). This unconditionally enables the safe
36+
+ // behavior that upstream React Native gates behind the
37+
+ // `fixFindShadowNodeByTagRaceCondition` feature flag. Remove this patch once
38+
+ // that flag is enabled by default upstream.
39+
+ RootShadowNode::Shared rootShadowNodeHolder =
40+
+ shadowTree.getCurrentRevision().rootShadowNode;
41+
+ const RootShadowNode* rootShadowNode = rootShadowNodeHolder.get();
42+
43+
if (rootShadowNode != nullptr) {
44+
const auto& children = rootShadowNode->getChildren();

src/CONST/index.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2211,7 +2211,6 @@ const CONST = {
22112211
SPAN_GEOLOCATION_WAIT: 'ManualGeolocationWait',
22122212
SPAN_SEND_MESSAGE: 'ManualSendMessage',
22132213
SPAN_NOT_FOUND_PAGE: 'ManualNotFoundPage',
2214-
SPAN_SKELETON: 'ManualSkeleton',
22152214
SPAN_ODOMETER_TO_CONFIRMATION: 'ManualOdometerToConfirmation',
22162215
SPAN_ODOMETER_IMAGE_STITCH: 'ManualOdometerImageStitch',
22172216
SPAN_ODOMETER_IMAGE_CAPTURE: 'ManualOdometerImageCapture',
@@ -2255,7 +2254,6 @@ const CONST = {
22552254
ATTRIBUTE_CANCELED_BY_SKELETON: 'canceled_by_skeleton',
22562255
ATTRIBUTE_ROUTE_FROM: 'route_from',
22572256
ATTRIBUTE_ROUTE_TO: 'route_to',
2258-
ATTRIBUTE_MIN_DURATION: 'min_duration',
22592257
ATTRIBUTE_FINISHED_MANUALLY: 'finished_manually',
22602258
ATTRIBUTE_IS_WARM: 'is_warm',
22612259
ATTRIBUTE_LAZY_TAB_FALLBACK_SHOWN: 'lazy_tab_fallback_shown',
@@ -2266,7 +2264,6 @@ const CONST = {
22662264
// report list, so durations that include the openApp wait can be excluded from render measurements.
22672265
ATTRIBUTE_SKELETON_SHOWN: 'skeleton_shown',
22682266
ATTRIBUTE_WAS_LIST_EMPTY: 'was_list_empty',
2269-
ATTRIBUTE_SKELETON_PREFIX: 'skeleton.',
22702267
ATTRIBUTE_SCENARIO: 'scenario',
22712268
// Start type stamped on the navigate-to-reports spans: cold, warm_first, or warm_subsequent.
22722269
ATTRIBUTE_START_TYPE: 'start_type',
@@ -2381,10 +2378,7 @@ const CONST = {
23812378
WARM_SUBSEQUENT: 'warm_subsequent',
23822379
UNKNOWN: 'unknown',
23832380
},
2384-
// Event names
2385-
EVENT_SKELETON_ATTRIBUTES_UPDATE: 'skeleton_attributes_updated',
23862381
CONFIG: {
2387-
SKELETON_MIN_DURATION: 10_000,
23882382
MEMORY_TRACKING_INTERVAL: 2 * 60 * 1000,
23892383

23902384
// Web Memory Thresholds (% of jsHeapSizeLimit)
@@ -2828,6 +2822,7 @@ const CONST = {
28282822
FILE_TOO_LARGE: 'fileTooLarge',
28292823
FILE_TOO_SMALL: 'fileTooSmall',
28302824
FILE_CORRUPTED: 'fileCorrupted',
2825+
HEIC_CONVERSION_FAILED: 'heicConversionFailed',
28312826
PROTECTED_FILE: 'protectedFile',
28322827
HEIC_OR_HEIF_IMAGE: 'heicOrHeifImage',
28332828
IMAGE_DIMENSIONS_TOO_LARGE: 'imageDimensionsTooLarge',
@@ -6989,6 +6984,11 @@ const CONST = {
69896984
reportView: true,
69906985
},
69916986
TAG: {column: this.TABLE_COLUMNS.TAG, search: true, reportView: true},
6987+
VIOLATIONS: {
6988+
column: this.TABLE_COLUMNS.VIOLATIONS,
6989+
search: false,
6990+
reportView: false,
6991+
},
69926992
TAG_GL_CODE: {
69936993
column: this.TABLE_COLUMNS.TAG_GL_CODE,
69946994
search: true,
@@ -7304,6 +7304,7 @@ const CONST = {
73047304
TO: 'to',
73057305
CATEGORY: 'category',
73067306
TAG: 'tag',
7307+
VIOLATIONS: 'violations',
73077308
ORIGINAL_AMOUNT: 'originalamount',
73087309
REIMBURSABLE: 'reimbursable',
73097310
BILLABLE: 'billable',
@@ -7537,6 +7538,7 @@ const CONST = {
75377538
[this.TABLE_COLUMNS.TO]: 'to',
75387539
[this.TABLE_COLUMNS.CATEGORY]: 'category',
75397540
[this.TABLE_COLUMNS.TAG]: 'tag',
7541+
[this.TABLE_COLUMNS.VIOLATIONS]: 'violations',
75407542
[this.TABLE_COLUMNS.ORIGINAL_AMOUNT]: 'purchase-amount',
75417543
[this.TABLE_COLUMNS.REIMBURSABLE]: 'reimbursable',
75427544
[this.TABLE_COLUMNS.BILLABLE]: 'billable',
@@ -7637,6 +7639,7 @@ const CONST = {
76377639
TOP_CATEGORIES: 'topCategories',
76387640
TOP_MERCHANTS: 'topMerchants',
76397641
SPEND_OVER_TIME: 'spendOverTime',
7642+
VIOLATIONS_BY_SUBMITTER: 'violationsBySubmitter',
76407643
},
76417644
SAVED_SEARCH_PREFIX: 'savedSearch_',
76427645
GROUP_PREFIX: 'group_',

src/Expensify.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,16 @@ function Expensify() {
269269
return;
270270
}
271271
updateLastRoute('');
272-
Navigation.navigate(lastRoute as Route);
272+
273+
// On iOS, changing the Contacts permission in Settings forces the app to reload (see `goToSettings`).
274+
// Restoring a deep RHP route (e.g. the money-request participant selector) directly on the boot frame
275+
// kicks the react-navigation card's native-driver entering animation while the tree is still hydrating,
276+
// which can crash with "Unable to find node on an unmounted component" when the card unmounts mid-transition.
277+
// Defer the restore by one frame so the card is not mounted-then-unmounted mid-animation.
278+
const restoreAnimationFrame = requestAnimationFrame(() => {
279+
Navigation.navigate(lastRoute as Route);
280+
});
281+
return () => cancelAnimationFrame(restoreAnimationFrame);
273282
// Disabling this rule because we only want it to run on the first render.
274283
// eslint-disable-next-line react-hooks/exhaustive-deps
275284
}, [isNavigationReady]);

src/components/AccountSwitcher.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
44
import useLocalize from '@hooks/useLocalize';
55
import useNetwork from '@hooks/useNetwork';
66
import useOnyx from '@hooks/useOnyx';
7+
import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin';
78
import usePopoverPosition from '@hooks/usePopoverPosition';
89
import useResponsiveLayout from '@hooks/useResponsiveLayout';
910
import useThemeStyles from '@hooks/useThemeStyles';
@@ -14,7 +15,6 @@ import {close} from '@libs/actions/Modal';
1415
import {getLatestError} from '@libs/ErrorUtils';
1516
import {getGpsPoints, stopGpsTrip} from '@libs/GPSDraftDetailsUtils';
1617
import {sortAlphabetically} from '@libs/OptionsListUtils';
17-
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
1818

1919
import TextWithEmojiFragment from '@pages/inbox/report/comment/TextWithEmojiFragment';
2020

@@ -66,16 +66,19 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) {
6666
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
6767
const [gpsDraftDetails] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS);
6868

69+
const delegate = account?.delegatedAccess?.delegate;
70+
const delegators = account?.delegatedAccess?.delegators ?? [];
71+
const personalDetailsByLogin = usePersonalDetailsByLogins([delegate, ...delegators.map((delegator) => delegator.email)]);
72+
6973
const buttonRef = useRef<View>(null);
7074
const {windowHeight, windowWidth} = useWindowDimensions();
7175
const {calculatePopoverPosition} = usePopoverPosition();
7276

7377
const [shouldShowDelegatorMenu, setShouldShowDelegatorMenu] = useState(false);
7478
// Measured from the Switch button so the menu opens directly below it, rather than a fixed position.
7579
const [popoverPosition, setPopoverPosition] = useState<AnchorPosition>();
76-
const delegators = account?.delegatedAccess?.delegators ?? [];
7780

78-
const isActingAsDelegate = !!account?.delegatedAccess?.delegate;
81+
const isActingAsDelegate = !!delegate;
7982
const canSwitchAccounts = delegators.length > 0 || isActingAsDelegate;
8083
const displayName = currentUserPersonalDetails?.displayName ?? '';
8184
const doesDisplayNameContainEmojis = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g')).test(displayName);
@@ -196,18 +199,15 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) {
196199
const currentUserMenuItem = createBaseMenuItem(currentUserPersonalDetails, undefined, {isSelected: true});
197200

198201
if (isActingAsDelegate) {
199-
const delegateEmail = account?.delegatedAccess?.delegate ?? '';
200-
201202
// Avoid duplicating the current user in the list when switching accounts
202-
if (delegateEmail === currentUserPersonalDetails.login) {
203+
if (delegate === currentUserPersonalDetails.login) {
203204
return [currentUserMenuItem];
204205
}
205206

206-
const delegatePersonalDetails = getPersonalDetailByEmail(delegateEmail);
207207
const error = getLatestError(account?.delegatedAccess?.errorFields?.disconnect);
208208

209209
return [
210-
createBaseMenuItem(delegatePersonalDetails, error, {
210+
createBaseMenuItem(personalDetailsByLogin[delegate], error, {
211211
onSelected: () => {
212212
if (isOffline) {
213213
close(showOfflineModal);
@@ -232,7 +232,7 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) {
232232
.map(({email, role}) => {
233233
const errorFields = account?.delegatedAccess?.errorFields ?? {};
234234
const error = getLatestError(errorFields?.connect?.[email]);
235-
const personalDetails = getPersonalDetailByEmail(email);
235+
const personalDetails = personalDetailsByLogin[email];
236236
return createBaseMenuItem(personalDetails, error, {
237237
badgeText: translate('delegate.role', role),
238238
onSelected: () => {

0 commit comments

Comments
 (0)