Skip to content

Commit 2f993b7

Browse files
committed
Merge branch 'main' into save-gradle-profile-report-from-adhoc-builds
2 parents f10f8b8 + 705bc68 commit 2f993b7

File tree

283 files changed

+9251
-3631
lines changed

Some content is hidden

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

283 files changed

+9251
-3631
lines changed

.claude/agents/code-inline-reviewer.md

Lines changed: 257 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ function UserProfile({ userId }) {
410410

411411
---
412412

413-
### [PERFORMANCE-12] Prevent memory leaks in components and plugins
413+
### [PERF-12] Prevent memory leaks in components and plugins
414414

415415
- **Search patterns**: `setInterval`, `setTimeout`, `addEventListener`, `subscribe`, `useEffect` with missing cleanup
416416

@@ -770,7 +770,7 @@ useEffect(() => {
770770

771771
---
772772

773-
### [CONSISTENCY-5] Ensure proper error handling
773+
### [CONSISTENCY-6] Ensure proper error handling
774774

775775
- **Search patterns**: `try`, `catch`, `async`, `await`, `Promise`, `.then(`, `.catch(`
776776

@@ -1189,6 +1189,261 @@ function SaveButton({ getSiblingFormData }: { getSiblingFormData: () => FormData
11891189

11901190
---
11911191

1192+
### [CLEAN-REACT-PATTERNS-4] Avoid side-effect spaghetti
1193+
1194+
- **Search patterns**: Multiple `useEffect` in single component, large component bodies mixing data access/navigation/UI state/lifecycle, hooks or utilities handling several unrelated responsibilities
1195+
1196+
- **Condition**: Flag when a component, hook, or utility aggregates multiple unrelated responsibilities in a single unit, making it difficult to modify one concern without touching others.
1197+
1198+
**Signs of violation:**
1199+
- Component has several `useEffect` hooks handling unrelated concerns (e.g., telemetry, deep linking, audio, session management all in one component)
1200+
- A single `useEffect` or hook handles multiple distinct responsibilities
1201+
- Unrelated state variables are interdependent or updated together
1202+
- Logic mixes data fetching, navigation, UI state, and lifecycle behavior in one place
1203+
- Removing one piece of functionality requires careful untangling from others
1204+
1205+
**What counts as "unrelated":**
1206+
- Group by responsibility (what the code does), NOT by timing (when it runs)
1207+
- Data fetching and analytics are NOT related — they serve different purposes even if both run on mount
1208+
- Session management and audio configuration are NOT related — different domains entirely
1209+
1210+
**DO NOT flag if:**
1211+
- Component is a thin orchestration layer that ONLY composes child components (no business logic, no effects beyond rendering)
1212+
- Effects are extracted into focused custom hooks with single responsibilities (e.g., `useDebugShortcut`, `usePriorityMode`) — inline `useEffect` calls are a code smell and should be named hooks
1213+
1214+
- **Reasoning**: When multiple unrelated responsibilities are grouped into a single component, hook, or utility, if any one concern changes, then unrelated logic must be touched as well, increasing coupling, regression risk, and cognitive load. This is the single responsibility principle for React: extract small units that do very little, very well. A component with several unrelated effects is a code smell - even a single effect can benefit from extraction to something with a good name, proper description, and isolated tests.
1215+
1216+
**Bucketing questions for refactoring:**
1217+
1. Does this logic need the React render loop? YES → Extract to a focused custom hook. NO → Extract out of React entirely (e.g., Onyx migration, global initialization).
1218+
2. Does this logic need to be in this component? YES → Keep it, but use a focused hook. NO → Extract to a separate component that owns this concern.
1219+
1220+
**Hook granularity guidance:**
1221+
- Group effects that serve the same purpose into one hook (e.g., all telemetry setup in `useTelemetry`)
1222+
- Group effects that can be reused together across components
1223+
- Don't create 15 separate single-effect hooks if 5 well-named grouped hooks make more sense
1224+
1225+
Good (separated concerns):
1226+
1227+
- Each piece of logic is extracted to a focused hook or component
1228+
- Parent component only orchestrates what to render
1229+
- State subscriptions in smaller components don't cause re-renders in parent
1230+
- Component-scoped hooks can be co-located in the same directory for maintainability
1231+
1232+
```tsx
1233+
function DebugMenu() {
1234+
useDebugShortcut();
1235+
1236+
return (
1237+
// Debug menu UI
1238+
);
1239+
}
1240+
1241+
function ParentComponent({ reportID }: { reportID: string }) {
1242+
return (
1243+
<View>
1244+
{/* Each child owns its own concerns */}
1245+
<ReportView reportID={reportID} />
1246+
<DebugMenu />
1247+
</View>
1248+
);
1249+
}
1250+
```
1251+
1252+
```tsx
1253+
// Focused hook that does one thing well
1254+
function useDebugShortcut() {
1255+
useEffect(() => {
1256+
const debugShortcutConfig = CONST.KEYBOARD_SHORTCUTS.DEBUG;
1257+
const unsubscribeDebugShortcut = KeyboardShortcut.subscribe(
1258+
debugShortcutConfig.shortcutKey,
1259+
() => toggleTestToolsModal(),
1260+
debugShortcutConfig.descriptionKey,
1261+
debugShortcutConfig.modifiers,
1262+
true,
1263+
);
1264+
1265+
return () => {
1266+
unsubscribeDebugShortcut();
1267+
};
1268+
}, []);
1269+
}
1270+
```
1271+
1272+
Bad (side-effect spaghetti):
1273+
1274+
- Component mixes session management, deep linking, telemetry, navigation, splash screen, audio, and other startup logic
1275+
- Several unrelated `useOnyx` calls and `useEffect` hooks in a single component
1276+
- Changing one concern risks breaking others
1277+
1278+
```tsx
1279+
function Expensify() {
1280+
// Session & auth
1281+
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
1282+
const [session] = useOnyx(ONYXKEYS.SESSION);
1283+
1284+
// Navigation & routing
1285+
const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
1286+
const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH);
1287+
const [isNavigationReady, setIsNavigationReady] = useState(false);
1288+
1289+
// App state
1290+
const [updateAvailable] = useOnyx(ONYXKEYS.UPDATE_AVAILABLE);
1291+
const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED);
1292+
const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED);
1293+
1294+
// Splash screen
1295+
const {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext);
1296+
1297+
// ... 10+ more useOnyx calls for unrelated concerns ...
1298+
1299+
// Telemetry effect
1300+
useEffect(() => {
1301+
bootsplashSpan.current = startSpan(CONST.TELEMETRY.SPAN_BOOTSPLASH.ROOT, {...});
1302+
// ...
1303+
}, []);
1304+
1305+
// Public room checking effect
1306+
useEffect(() => {
1307+
if (isCheckingPublicRoom) return;
1308+
endSpan(CONST.TELEMETRY.SPAN_BOOTSPLASH.ONYX);
1309+
// ...
1310+
}, [isCheckingPublicRoom]);
1311+
1312+
// Splash screen effect
1313+
useEffect(() => {
1314+
if (!shouldHideSplash) return;
1315+
startSpan(CONST.TELEMETRY.SPAN_BOOTSPLASH.SPLASH_HIDER, {...});
1316+
}, [shouldHideSplash]);
1317+
1318+
// Deep linking effect
1319+
useEffect(() => {
1320+
Linking.getInitialURL().then((url) => {
1321+
if (url) {
1322+
openReportFromDeepLink(url, ...);
1323+
}
1324+
});
1325+
// ...
1326+
}, []);
1327+
1328+
// Audio mode effect
1329+
useEffect(() => {
1330+
Audio.setAudioModeAsync({playsInSilentModeIOS: true});
1331+
}, []);
1332+
1333+
// ... 10+ more useEffects mixing concerns ...
1334+
}
1335+
```
1336+
1337+
In this example:
1338+
- The component handles telemetry, deep linking, audio, session, navigation, splash screen, and more
1339+
- Each concern is interleaved with others, making it hard to modify one without risking regression in another
1340+
- Effects could be extracted to focused hooks: `useTelemetrySpans`, `useDeepLinking`, `useAudioMode`, etc.
1341+
- Entry points don't get special treatment — extracting effects into named hooks improves clarity and makes it possible to understand what each effect does and how to safely modify it
1342+
1343+
---
1344+
1345+
### [CLEAN-REACT-PATTERNS-5] Keep state and subscriptions narrow
1346+
1347+
- **Search patterns**: Contexts/hooks/stores exposing large bundled objects, providers with many unrelated `useOnyx` calls, state structures mixing unrelated concerns
1348+
1349+
- **Condition**: Flag when a state structure (context, hook, store, or subscription) bundles unrelated concerns together, causing consumers to re-render when data they don't use changes.
1350+
1351+
**Signs of violation:**
1352+
- State provider (context, hook, or store) that bundles unrelated data (e.g., navigation state + list data + cache utilities in one structure)
1353+
- State object where properties serve different purposes and change independently
1354+
- Multiple unrelated subscriptions (`useOnyx`, `useContext`, store selectors) aggregated into a single exposed value
1355+
- Consumers of a state source that only use a subset of the provided values
1356+
1357+
**DO NOT flag if:**
1358+
- State values are cohesive — they change together and serve the same purpose (e.g., `keyboardHeight` + `isKeyboardShown` both relate to keyboard state)
1359+
- The state structure is intentionally designed as an aggregation point and consumers use most/all values
1360+
- Individual `useOnyx` calls without selectors — this is covered by [PERF-11]
1361+
1362+
- **Reasoning**: When unrelated pieces of data are grouped into a single state structure, if an unused part changes, then all consumers re-render unnecessarily. This silently expands render scope, increases coupling, and makes performance regressions hard to detect. Structuring state around cohesive concerns ensures render scope stays predictable and changes remain local.
1363+
1364+
**Distinction from PERF-11**: PERF-11 addresses individual `useOnyx` selector usage. This rule addresses state structure — how multiple values are grouped and exposed to consumers via contexts, hooks, or stores.
1365+
1366+
**Distinction from CLEAN-REACT-PATTERNS-2**: PATTERNS-2 addresses data flow direction — parent shouldn't fetch data just to pass to children. This rule addresses how state is structured and grouped within any state provider.
1367+
1368+
Good (cohesive state — all values serve one purpose):
1369+
1370+
- All state relates to one concern (keyboard)
1371+
- Values change together — no wasted re-renders
1372+
- Derived state computed inline, not stored separately
1373+
1374+
```tsx
1375+
type KeyboardStateContextValue = {
1376+
isKeyboardShown: boolean;
1377+
isKeyboardActive: boolean;
1378+
keyboardHeight: number;
1379+
};
1380+
1381+
function KeyboardStateProvider({children}: ChildrenProps) {
1382+
const [keyboardHeight, setKeyboardHeight] = useState(0);
1383+
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
1384+
1385+
useEffect(() => {
1386+
const showListener = KeyboardEvents.addListener('keyboardDidShow', (e) => {
1387+
setKeyboardHeight(e.height);
1388+
setIsKeyboardActive(true);
1389+
});
1390+
const hideListener = KeyboardEvents.addListener('keyboardDidHide', () => {
1391+
setKeyboardHeight(0);
1392+
setIsKeyboardActive(false);
1393+
});
1394+
return () => {
1395+
showListener.remove();
1396+
hideListener.remove();
1397+
};
1398+
}, []);
1399+
1400+
const contextValue = useMemo(() => ({
1401+
keyboardHeight,
1402+
isKeyboardShown: keyboardHeight !== 0, // Derived, not separate state
1403+
isKeyboardActive,
1404+
}), [keyboardHeight, isKeyboardActive]);
1405+
1406+
return <KeyboardStateContext.Provider value={contextValue}>{children}</KeyboardStateContext.Provider>;
1407+
}
1408+
```
1409+
1410+
Bad (grab-bag state — bundles unrelated concerns):
1411+
1412+
- State provider subscribes to many unrelated Onyx collections
1413+
- Exposed value mixes navigation state, list data, membership data, and cache utilities
1414+
- Any consumer re-renders when ANY subscribed value changes
1415+
1416+
```tsx
1417+
function SidebarOrderedReportsContextProvider({children}) {
1418+
// ❌ Many unrelated Onyx subscriptions bundled together
1419+
const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
1420+
const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
1421+
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
1422+
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
1423+
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
1424+
const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
1425+
const [reportsDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
1426+
const [betas] = useOnyx(ONYXKEYS.BETAS);
1427+
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES);
1428+
1429+
// ❌ Context value mixes unrelated concerns
1430+
const contextValue = {
1431+
orderedReports, // List data
1432+
orderedReportIDs, // List data
1433+
currentReportID, // Navigation state
1434+
policyMemberAccountIDs, // Policy membership
1435+
clearLHNCache, // Cache management utility
1436+
};
1437+
1438+
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
1439+
}
1440+
1441+
// A component needing only currentReportID re-renders when orderedReports changes
1442+
// A component needing only policyMemberAccountIDs re-renders when navigation changes
1443+
```
1444+
1445+
---
1446+
11921447
## Instructions
11931448

11941449
1. **First, get the list of changed files and their diffs:**

.claude/agents/deploy-blocker-investigator.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ Technical explanation of what went wrong in the code.
207207

208208
**DO NOT:**
209209
- Remove `DeployBlockerCash` if there's an App PR that caused or contributed to the issue
210+
- Remove `DeployBlockerCash` if the issue is not reproducible on production
211+
- Remove any of the blocker labels if it has been added by an internal employee
210212
- Remove both blocker labels simultaneously
211213
- Make assumptions about code you haven't read
212214
- Recommend DEMOTE for bugs affecting core functionality (auth, payments, data loss)

.github/workflows/testBuild.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ jobs:
301301

302302
- name: Rock Remote Build - Android
303303
id: rock-remote-build-android
304-
uses: callstackincubator/android@0bbc1b7c2e1a8be1ecb4d6c744c211869823fd65
304+
uses: callstackincubator/android@2177ac62cabe662aa49a2bc6f3154070705dd8f5
305305
env:
306306
GITHUB_TOKEN: ${{ github.token }}
307307
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -320,6 +320,7 @@ jobs:
320320
keystore-path: '../tools/buildtools/upload-key.keystore'
321321
comment-bot: false
322322
rock-build-extra-params: '--extra-params "-PreactNativeArchitectures=arm64-v8a,x86_64 --profile"'
323+
custom-ref: ${{ needs.prep.outputs.APP_REF }}
323324

324325
- name: Upload Gradle profile report
325326
if: always()
@@ -422,7 +423,7 @@ jobs:
422423

423424
- name: Rock Remote Build - iOS
424425
id: rock-remote-build-ios
425-
uses: callstackincubator/ios@8dcef6cc275e0cf3299f5a97cde5ebd635c887d7
426+
uses: callstackincubator/ios@d638bd25c764655baeeced3232c692f1698cf72b
426427
env:
427428
GITHUB_TOKEN: ${{ github.token }}
428429
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -450,6 +451,7 @@ jobs:
450451
}
451452
]
452453
comment-bot: false
454+
custom-ref: ${{ needs.prep.outputs.APP_REF }}
453455

454456
- name: Set artifact URL output
455457
id: set-artifact-url

.github/workflows/testBuildOnPush.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ on:
55
branches: [main]
66
paths-ignore: ['docs/**', 'contributingGuides/**', 'help/**', '.github/**', 'scripts/**', 'tests/**', 'jest/**', '.claude/**']
77

8+
concurrency:
9+
group: 'testBuildOnPush'
10+
cancel-in-progress: false
11+
812
jobs:
913
prep:
1014
runs-on: ubuntu-latest
@@ -432,7 +436,6 @@ jobs:
432436
summary.addRaw(`\n**Mobile-Expensify Submodule SHA:** [${submoduleSHA}](${submoduleUrl})`);
433437
}
434438
435-
436439
const prUrl = '${{ needs.prep.outputs.PR_URL }}';
437440
438441
if (prUrl) {
@@ -441,5 +444,4 @@ jobs:
441444
summary.addRaw(`\n**PR Link:** No PR associated with this commit.`);
442445
}
443446
444-
445447
summary.write();

Mobile-Expensify

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ android {
114114
minSdkVersion rootProject.ext.minSdkVersion
115115
targetSdkVersion rootProject.ext.targetSdkVersion
116116
multiDexEnabled rootProject.ext.multiDexEnabled
117-
versionCode 1009031153
118-
versionName "9.3.11-53"
117+
versionCode 1009031502
118+
versionName "9.3.15-2"
119119
// Supported language variants must be declared here to avoid from being removed during the compilation.
120120
// This also helps us to not include unnecessary language variants in the APK.
121121
resConfigs "en", "es"

assets/images/gallery-plus.svg

Lines changed: 1 addition & 0 deletions
Loading
-350 Bytes
Loading

assets/images/odometer-end.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)