Skip to content

Commit 158ffb3

Browse files
committed
Merge branch 'main' into fixSearchActionFilter
2 parents 3d3e8da + 2abf7ab commit 158ffb3

828 files changed

Lines changed: 30535 additions & 8225 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.

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

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,156 @@ async function submitForm(data: FormData) {
825825

826826
---
827827

828+
### [CLEAN-REACT-PATTERNS-1] Favor composition over configuration
829+
830+
- **Search patterns**: `shouldShow`, `shouldEnable`, `canSelect`, `enable`, `disable`, configuration props patterns
831+
832+
- **Condition**: Flag ONLY when ALL of these are true:
833+
834+
- A **new feature** is being introduced OR an **existing component's API is being expanded with new props**
835+
- The change adds configuration properties (flags, conditional logic)
836+
- These configuration options control feature presence or behavior within the component
837+
- These features could instead be expressed as composable child components
838+
839+
**Features that should be expressed as child components:**
840+
- Optional UI elements that could be composed in
841+
- New behavior that could be introduced as new children
842+
- Features that currently require parent component code changes
843+
844+
**DO NOT flag if:**
845+
- Props are narrow, stable values needed for coordination between composed parts (e.g., `reportID`, `data`, `columns`)
846+
- The component uses composition and child components for features
847+
- Parent components stay stable as features are added
848+
849+
- **Reasoning**: When new features are implemented by adding configuration (props, flags, conditional logic) to existing components, if requirements change, then those components must be repeatedly modified, increasing coupling, surface area, and regression risk. Composition ensures features scale horizontally, limits the scope of changes, and prevents components from becoming configuration-driven "mega components".
850+
851+
Good (composition):
852+
853+
- Features expressed as composable children
854+
- Parent stays stable; add features by adding children
855+
856+
```tsx
857+
<Table data={items} columns={columns}>
858+
<Table.SearchBar />
859+
<Table.Header />
860+
<Table.Body />
861+
</Table>
862+
```
863+
864+
```tsx
865+
<SelectionList data={items}>
866+
<SelectionList.TextInput />
867+
<SelectionList.Body />
868+
</SelectionList>
869+
```
870+
871+
Bad (configuration):
872+
873+
- Features controlled by boolean flags
874+
- Adding a new feature requires modifying the Table component's API
875+
876+
```tsx
877+
<Table
878+
data={items}
879+
columns={columns}
880+
shouldShowSearchBar
881+
shouldShowHeader
882+
shouldEnableSorting
883+
shouldShowPagination
884+
shouldHighlightOnHover
885+
/>
886+
887+
type TableProps = {
888+
data: Item[];
889+
columns: Column[];
890+
shouldShowSearchBar?: boolean; // ❌ Could be <Table.SearchBar />
891+
shouldShowHeader?: boolean; // ❌ Could be <Table.Header />
892+
shouldEnableSorting?: boolean; // ❌ Configuration for header behavior
893+
shouldShowPagination?: boolean; // ❌ Could be <Table.Pagination />
894+
shouldHighlightOnHover?: boolean; // ❌ Configuration for styling behavior
895+
};
896+
```
897+
898+
```tsx
899+
<SelectionList
900+
data={items}
901+
shouldShowTextInput
902+
shouldShowTooltips
903+
shouldScrollToFocusedIndex
904+
shouldDebounceScrolling
905+
shouldUpdateFocusedIndex
906+
canSelectMultiple
907+
disableKeyboardShortcuts
908+
/>
909+
910+
type SelectionListProps = {
911+
shouldShowTextInput?: boolean; // ❌ Could be <SelectionList.TextInput />
912+
shouldShowConfirmButton?: boolean; // ❌ Could be <SelectionList.ConfirmButton />
913+
textInputOptions?: {...}; // ❌ Configuration object for the above
914+
};
915+
```
916+
917+
Good (children manage their own state):
918+
919+
```tsx
920+
// Children are self-contained and manage their own state
921+
// Parent only passes minimal data (IDs)
922+
// Adding new features doesn't require changing the parent
923+
function ReportScreen({ params: { reportID }}) {
924+
return (
925+
<>
926+
<ReportActionsView reportID={reportID} />
927+
// other features
928+
<Composer />
929+
</>
930+
);
931+
}
932+
933+
// Component accesses stores and calculates its own state
934+
// Parent doesn't know the internals
935+
function ReportActionsView({ reportID }) {
936+
const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
937+
const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
938+
// ...
939+
}
940+
```
941+
942+
Bad (parent manages child state):
943+
944+
```tsx
945+
// Parent fetches and manages state for its children
946+
// Parent has to know child implementation details
947+
function ReportScreen({ params: { reportID }}) {
948+
const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {allowStaleData: true, canBeMissing: true});
949+
const reportActions = useMemo(() => getFilteredReportActionsForReportView(unfilteredReportActions), [unfilteredReportActions]);
950+
const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {canBeMissing: true, allowStaleData: true});
951+
const {reportActions: unfilteredReportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
952+
const parentReportAction = useParentReportAction(reportOnyx);
953+
const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs);
954+
const isTransactionThreadView = isReportTransactionThread(report);
955+
// other onyx connections etc
956+
957+
return (
958+
<>
959+
<ReportActionsView
960+
report={report}
961+
reportActions={reportActions}
962+
isLoadingInitialReportActions={reportMetadata?.isLoadingInitialReportActions}
963+
hasNewerActions={hasNewerActions}
964+
hasOlderActions={hasOlderActions}
965+
parentReportAction={parentReportAction}
966+
transactionThreadReportID={transactionThreadReportID}
967+
isReportTransactionThread={isTransactionThreadView}
968+
/>
969+
// other features
970+
<Composer />
971+
</>
972+
);
973+
}
974+
```
975+
976+
---
977+
828978
## Instructions
829979

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

.claude/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@
1919
"mcp__playwright__browser_evaluate",
2020
"mcp__playwright__browser_handle_dialog"
2121
]
22+
},
23+
"enabledPlugins": {
24+
"react-native-best-practices@callstack-agent-skills": true
2225
}
2326
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
name: Exploratory issue template
3+
about: A template to follow when creating a new issue in this repository that was found during Exploratory testing by QA
4+
labels: Monthly, Not a priority
5+
---
6+
7+
If you haven’t already, check out our [contributing guidelines](https://github.com/Expensify/ReactNativeChat/blob/main/contributingGuides/CONTRIBUTING.md) for onboarding and email contributors@expensify.com to request to join our Slack channel!
8+
___
9+
**For all Exploratory issues, start the title of the issue with `[Exploratory]`** (and, do **not** add the `External` label.
10+
11+
**Version Number:**
12+
**Reproducible in staging?:**
13+
**Reproducible in production?:**
14+
**If this was caught during regression testing, add the test name, ID and link from BrowserStack:**
15+
**Email or phone of affected tester (no customers):**
16+
**Logs:** https://stackoverflow.com/c/expensify/questions/4856
17+
**Expensify/Expensify Issue URL:**
18+
**Issue reported by:**
19+
**Slack conversation** (hyperlinked to channel name):
20+
21+
## Action Performed:
22+
Break down in numbered steps
23+
24+
## Expected Result:
25+
Describe what you think should've happened
26+
27+
## Actual Result:
28+
Describe what actually happened
29+
30+
## Workaround:
31+
Can the user still use Expensify without this being fixed? Have you informed them of the workaround?
32+
33+
## Platforms:
34+
Select the officially supported platforms where the issue was reproduced:
35+
- [ ] Android: App
36+
- [ ] Android: mWeb Chrome
37+
- [ ] iOS: App
38+
- [ ] iOS: mWeb Safari
39+
- [ ] iOS: mWeb Chrome
40+
- [ ] Windows: Chrome
41+
- [ ] MacOS: Chrome / Safari
42+
43+
## Screenshots/Videos
44+
45+
46+
[View all open jobs on GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22)

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,18 @@ Key GitHub Actions workflows:
172172

173173
## Development Practices
174174

175+
### React Native Best Practices
176+
Use the `/react-native-best-practices` skill when working on performance-sensitive code, native modules, or release preparation. This ensures code respects established best practices from the start, resulting in more consistent code, fewer review iterations, and better resilience against regressions.
177+
178+
The skill provides guidance on:
179+
- **Performance**: FPS optimization, virtualized lists (FlashList), memoization, atomic state, animations
180+
- **Bundle & App Size**: Barrel imports, tree shaking, bundle analysis, R8 shrinking
181+
- **Startup (TTI)**: Hermes bytecode optimization, native navigation, deferred work
182+
- **Native Modules**: Turbo Module development, threading model, Swift/Kotlin/C++ patterns
183+
- **Memory**: JS and native memory leak detection and patterns
184+
- **Build Compliance**: Android 16KB page alignment (Google Play requirement)
185+
- **Platform Tooling**: Xcode/Android Studio profiling and debugging setup
186+
175187
### Code Quality
176188
- **TypeScript**: Strict mode enabled
177189
- **ESLint**: Linter

Mobile-Expensify

__mocks__/expo-video.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/* eslint-disable no-underscore-dangle */
2+
/* eslint-disable @typescript-eslint/naming-convention */
3+
/* eslint-disable @typescript-eslint/no-unused-vars */
4+
// eslint-disable-next-line no-restricted-imports
5+
import React, {forwardRef, useRef} from 'react';
6+
import type {ForwardedRef} from 'react';
7+
import {View} from 'react-native';
8+
import type {ViewProps} from 'react-native';
9+
10+
type MutedChangeEventPayload = {
11+
isMuted: boolean;
12+
};
13+
14+
type PlayingChangeEventPayload = {
15+
isPlaying: boolean;
16+
};
17+
18+
type StatusChangeEventPayload = {
19+
status: string;
20+
};
21+
22+
type TimeUpdateEventPayload = {
23+
currentTime: number;
24+
};
25+
26+
type VideoPlayer = {
27+
/** lightweight “state” */
28+
isPlaying: boolean;
29+
isMuted: boolean;
30+
currentTime: number;
31+
32+
/** controls */
33+
play: jest.Mock<void, []>;
34+
pause: jest.Mock<void, []>;
35+
replace: jest.Mock<void, [unknown?]>;
36+
seekTo: jest.Mock<void, [number]>;
37+
setIsMuted: jest.Mock<void, [boolean]>;
38+
39+
/** simple event API */
40+
addListener: jest.Mock<{remove: () => void}, [string, (payload: unknown) => void]>;
41+
};
42+
43+
function createMockPlayer(): VideoPlayer {
44+
let _isPlaying = false;
45+
let _isMuted = false;
46+
let _currentTime = 0;
47+
48+
return {
49+
get isPlaying() {
50+
return _isPlaying;
51+
},
52+
get isMuted() {
53+
return _isMuted;
54+
},
55+
get currentTime() {
56+
return _currentTime;
57+
},
58+
59+
play: jest.fn(() => {
60+
_isPlaying = true;
61+
}),
62+
pause: jest.fn(() => {
63+
_isPlaying = false;
64+
}),
65+
replace: jest.fn((_opts?: unknown) => {
66+
// no-op; exist to satisfy code that calls it
67+
}),
68+
seekTo: jest.fn((time: number) => {
69+
_currentTime = time;
70+
}),
71+
setIsMuted: jest.fn((muted: boolean) => {
72+
_isMuted = muted;
73+
}),
74+
75+
addListener: jest.fn((_event: string, _cb: (payload: unknown) => void) => {
76+
// minimal, enough for code that calls .remove()
77+
return {remove: () => {}};
78+
}),
79+
};
80+
}
81+
82+
/**
83+
* Mocked hook – returns a stable mock player instance.
84+
* Signature accepts any args to match real API calls.
85+
*/
86+
function useVideoPlayer(..._args: unknown[]): VideoPlayer {
87+
const ref = useRef<VideoPlayer | null>(null);
88+
if (!ref.current) {
89+
ref.current = createMockPlayer();
90+
}
91+
return ref.current;
92+
}
93+
94+
/**
95+
* Simple stand-in for the native VideoView.
96+
* Accepts a `player` prop to mirror the real component.
97+
*/
98+
type VideoViewProps = ViewProps & {player?: VideoPlayer};
99+
100+
const VideoView = forwardRef((props: VideoViewProps, ref: ForwardedRef<View>) => (
101+
// eslint-disable-next-line react/jsx-props-no-spreading
102+
<View
103+
ref={ref}
104+
accessibilityLabel="MockVideoView"
105+
// eslint-disable-next-line react/jsx-props-no-spreading
106+
{...props}
107+
/>
108+
));
109+
110+
export {useVideoPlayer, VideoView};
111+
export type {MutedChangeEventPayload, PlayingChangeEventPayload, StatusChangeEventPayload, TimeUpdateEventPayload};

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 1009030800
118-
versionName "9.3.8-0"
117+
versionCode 1009031003
118+
versionName "9.3.10-3"
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"
301 KB
Loading
166 KB
Loading

assets/images/home.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)