Skip to content

Commit 53dd2ff

Browse files
committed
Merge branch 'main' of https://github.com/Expensify/App into ikevin127-ppMelvinUpdate2
2 parents ec1fe24 + e48c614 commit 53dd2ff

274 files changed

Lines changed: 6787 additions & 2527 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/skills/coding-standards/rules/clean-react-1-composition-over-config.md

Lines changed: 395 additions & 89 deletions
Large diffs are not rendered by default.

.claude/skills/coding-standards/rules/clean-react-4-no-side-effect-spaghetti.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,41 @@ In this example:
9191
- Effects could be extracted to focused hooks: `useTelemetrySpans`, `useDeepLinking`, `useAudioMode`, etc.
9292
- 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
9393

94+
#### Incorrect (internal render helper functions)
95+
96+
- Internal `render*` functions signal the component owns multiple rendering responsibilities that should be separate components
97+
- They close over the entire component scope — the React Compiler cannot independently memoize them
98+
- They hide the component's render tree — the return statement doesn't show what actually renders
99+
- The fix is to extract each helper into its own component with explicit props
100+
101+
```tsx
102+
function ForYouSection() {
103+
const theme = useTheme();
104+
const {translate} = useLocalize();
105+
const {submitCount, approveCount} = useTodos();
106+
107+
// ❌ Internal render helper — closes over everything, hides structure
108+
const renderTodoItems = () => (
109+
<View style={styles.todoContainer}>
110+
{todoItems.map(({key, icon, ...rest}) => (
111+
<BaseWidgetItem key={key} icon={icon} {...rest} />
112+
))}
113+
</View>
114+
);
115+
116+
// ❌ Another render helper — branching logic buried in a function
117+
const renderContent = () => {
118+
if (isLoadingApp) {
119+
return <ActivityIndicator size="large" />;
120+
}
121+
return hasAnyTodos ? renderTodoItems() : <EmptyState />;
122+
};
123+
124+
// The return statement hides the actual render tree
125+
return <WidgetContainer>{renderContent()}</WidgetContainer>;
126+
}
127+
```
128+
94129
### Correct
95130

96131
#### Correct (separated concerns)
@@ -154,6 +189,9 @@ Flag when a component, hook, or utility aggregates multiple unrelated responsibi
154189
- Unrelated state variables are interdependent or updated together
155190
- Logic mixes data fetching, navigation, UI state, and lifecycle behavior in one place
156191
- Removing one piece of functionality requires careful untangling from others
192+
- Component defines internal `render*` functions or arrow functions that return JSX and calls them in its return statement (e.g., `const renderContent = () => ...`, `{renderContent()}`)
193+
- These functions close over the component's entire scope, preventing the React Compiler from independently memoizing them
194+
- The component's return statement calls these helpers instead of showing the render tree directly
157195

158196
**What counts as "unrelated":**
159197
- Group by responsibility (what the code does), NOT by timing (when it runs)
@@ -163,7 +201,11 @@ Flag when a component, hook, or utility aggregates multiple unrelated responsibi
163201
**DO NOT flag if:**
164202
- Component is a thin orchestration layer that ONLY composes child components (no business logic, no effects beyond rendering)
165203
- 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
204+
- The internal function is a **callback or event handler** (e.g., `handlePress`, `onSubmit`), not a render helper — only functions that return JSX qualify
205+
- The internal function is a **single early return** for a guard clause (e.g., `if (!data) return <EmptyState />;` at the top of the component) — simple guards in the component body are not render helpers
166206

167207
**Search Patterns** (hints for reviewers):
168208
- `useEffect`
169209
- `useOnyx`
210+
- `const render\w+\s*=` or `function render\w+` inside a component body (internal render helpers)
211+
- `{render\w+\(\)}` in JSX return statements (helper invocations)

.github/actions/javascript/authorChecklist/index.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15889,7 +15889,7 @@ class GithubUtils {
1588915889
/**
1589015890
* Generate the issue body and assignees for a StagingDeployCash.
1589115891
*/
15892-
static generateStagingDeployCashBodyAndAssignees(tag, PRList, PRListMobileExpensify, verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false) {
15892+
static generateStagingDeployCashBodyAndAssignees({ tag, PRList, PRListMobileExpensify = [], verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false, chronologicalSection = '', }) {
1589315893
return this.fetchAllPullRequests(PRList.map((pr) => this.getPullRequestNumberFromURL(pr)))
1589415894
.then((data) => {
1589515895
const internalQAPRs = Array.isArray(data) ? data.filter((pr) => !(0, isEmptyObject_1.isEmptyObject)(pr.labels.find((item) => item.name === CONST_1.default.LABELS.INTERNAL_QA))) : [];
@@ -15960,6 +15960,10 @@ class GithubUtils {
1596015960
}
1596115961
issueBody += '\r\n\r\n';
1596215962
}
15963+
if (chronologicalSection) {
15964+
issueBody += chronologicalSection;
15965+
issueBody += '\r\n\r\n';
15966+
}
1596315967
issueBody += '**Deployer verifications:**';
1596415968
// eslint-disable-next-line max-len
1596515969
issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-mobile-app/crashlytics/app/ios:com.expensify.expensifylite/issues?state=open&time=last-seven-days&types=crash&tag=all&sort=eventCount) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`;
@@ -16075,6 +16079,26 @@ class GithubUtils {
1607516079
...(options.status && { status: options.status }),
1607616080
});
1607716081
}
16082+
/**
16083+
* Get the workflow run URL for a specific commit SHA and workflow file.
16084+
* Returns the HTML URL of the matching run, or undefined if not found.
16085+
*/
16086+
static async getWorkflowRunURLForCommit(commitSha, workflowFile) {
16087+
try {
16088+
const response = await this.octokit.actions.listWorkflowRuns({
16089+
owner: CONST_1.default.GITHUB_OWNER,
16090+
repo: CONST_1.default.APP_REPO,
16091+
workflow_id: workflowFile,
16092+
head_sha: commitSha,
16093+
per_page: 1,
16094+
});
16095+
return response.data.workflow_runs.at(0)?.html_url;
16096+
}
16097+
catch (error) {
16098+
console.warn(`Failed to find workflow run for commit ${commitSha}:`, error);
16099+
return undefined;
16100+
}
16101+
}
1607816102
/**
1607916103
* Generate the URL of an New Expensify pull request given the PR number.
1608016104
*/
@@ -16232,6 +16256,7 @@ class GithubUtils {
1623216256
commit: commit.sha,
1623316257
subject: commit.commit.message,
1623416258
authorName: commit.commit.author?.name ?? 'Unknown',
16259+
date: commit.commit.committer?.date ?? '',
1623516260
}));
1623616261
}
1623716262
catch (error) {

.github/actions/javascript/awaitStagingDeploys/index.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12667,7 +12667,7 @@ class GithubUtils {
1266712667
/**
1266812668
* Generate the issue body and assignees for a StagingDeployCash.
1266912669
*/
12670-
static generateStagingDeployCashBodyAndAssignees(tag, PRList, PRListMobileExpensify, verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false) {
12670+
static generateStagingDeployCashBodyAndAssignees({ tag, PRList, PRListMobileExpensify = [], verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false, chronologicalSection = '', }) {
1267112671
return this.fetchAllPullRequests(PRList.map((pr) => this.getPullRequestNumberFromURL(pr)))
1267212672
.then((data) => {
1267312673
const internalQAPRs = Array.isArray(data) ? data.filter((pr) => !(0, isEmptyObject_1.isEmptyObject)(pr.labels.find((item) => item.name === CONST_1.default.LABELS.INTERNAL_QA))) : [];
@@ -12738,6 +12738,10 @@ class GithubUtils {
1273812738
}
1273912739
issueBody += '\r\n\r\n';
1274012740
}
12741+
if (chronologicalSection) {
12742+
issueBody += chronologicalSection;
12743+
issueBody += '\r\n\r\n';
12744+
}
1274112745
issueBody += '**Deployer verifications:**';
1274212746
// eslint-disable-next-line max-len
1274312747
issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-mobile-app/crashlytics/app/ios:com.expensify.expensifylite/issues?state=open&time=last-seven-days&types=crash&tag=all&sort=eventCount) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`;
@@ -12853,6 +12857,26 @@ class GithubUtils {
1285312857
...(options.status && { status: options.status }),
1285412858
});
1285512859
}
12860+
/**
12861+
* Get the workflow run URL for a specific commit SHA and workflow file.
12862+
* Returns the HTML URL of the matching run, or undefined if not found.
12863+
*/
12864+
static async getWorkflowRunURLForCommit(commitSha, workflowFile) {
12865+
try {
12866+
const response = await this.octokit.actions.listWorkflowRuns({
12867+
owner: CONST_1.default.GITHUB_OWNER,
12868+
repo: CONST_1.default.APP_REPO,
12869+
workflow_id: workflowFile,
12870+
head_sha: commitSha,
12871+
per_page: 1,
12872+
});
12873+
return response.data.workflow_runs.at(0)?.html_url;
12874+
}
12875+
catch (error) {
12876+
console.warn(`Failed to find workflow run for commit ${commitSha}:`, error);
12877+
return undefined;
12878+
}
12879+
}
1285612880
/**
1285712881
* Generate the URL of an New Expensify pull request given the PR number.
1285812882
*/
@@ -13010,6 +13034,7 @@ class GithubUtils {
1301013034
commit: commit.sha,
1301113035
subject: commit.commit.message,
1301213036
authorName: commit.commit.author?.name ?? 'Unknown',
13037+
date: commit.commit.committer?.date ?? '',
1301313038
}));
1301413039
}
1301513040
catch (error) {

.github/actions/javascript/checkAndroidStatus/index.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737410,7 +737410,7 @@ class GithubUtils {
737410737410
/**
737411737411
* Generate the issue body and assignees for a StagingDeployCash.
737412737412
*/
737413-
static generateStagingDeployCashBodyAndAssignees(tag, PRList, PRListMobileExpensify, verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false) {
737413+
static generateStagingDeployCashBodyAndAssignees({ tag, PRList, PRListMobileExpensify = [], verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false, chronologicalSection = '', }) {
737414737414
return this.fetchAllPullRequests(PRList.map((pr) => this.getPullRequestNumberFromURL(pr)))
737415737415
.then((data) => {
737416737416
const internalQAPRs = Array.isArray(data) ? data.filter((pr) => !(0, isEmptyObject_1.isEmptyObject)(pr.labels.find((item) => item.name === CONST_1.default.LABELS.INTERNAL_QA))) : [];
@@ -737481,6 +737481,10 @@ class GithubUtils {
737481737481
}
737482737482
issueBody += '\r\n\r\n';
737483737483
}
737484+
if (chronologicalSection) {
737485+
issueBody += chronologicalSection;
737486+
issueBody += '\r\n\r\n';
737487+
}
737484737488
issueBody += '**Deployer verifications:**';
737485737489
// eslint-disable-next-line max-len
737486737490
issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-mobile-app/crashlytics/app/ios:com.expensify.expensifylite/issues?state=open&time=last-seven-days&types=crash&tag=all&sort=eventCount) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`;
@@ -737596,6 +737600,26 @@ class GithubUtils {
737596737600
...(options.status && { status: options.status }),
737597737601
});
737598737602
}
737603+
/**
737604+
* Get the workflow run URL for a specific commit SHA and workflow file.
737605+
* Returns the HTML URL of the matching run, or undefined if not found.
737606+
*/
737607+
static async getWorkflowRunURLForCommit(commitSha, workflowFile) {
737608+
try {
737609+
const response = await this.octokit.actions.listWorkflowRuns({
737610+
owner: CONST_1.default.GITHUB_OWNER,
737611+
repo: CONST_1.default.APP_REPO,
737612+
workflow_id: workflowFile,
737613+
head_sha: commitSha,
737614+
per_page: 1,
737615+
});
737616+
return response.data.workflow_runs.at(0)?.html_url;
737617+
}
737618+
catch (error) {
737619+
console.warn(`Failed to find workflow run for commit ${commitSha}:`, error);
737620+
return undefined;
737621+
}
737622+
}
737599737623
/**
737600737624
* Generate the URL of an New Expensify pull request given the PR number.
737601737625
*/
@@ -737753,6 +737777,7 @@ class GithubUtils {
737753737777
commit: commit.sha,
737754737778
subject: commit.commit.message,
737755737779
authorName: commit.commit.author?.name ?? 'Unknown',
737780+
date: commit.commit.committer?.date ?? '',
737756737781
}));
737757737782
}
737758737783
catch (error) {

.github/actions/javascript/checkDeployBlockers/index.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11934,7 +11934,7 @@ class GithubUtils {
1193411934
/**
1193511935
* Generate the issue body and assignees for a StagingDeployCash.
1193611936
*/
11937-
static generateStagingDeployCashBodyAndAssignees(tag, PRList, PRListMobileExpensify, verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false) {
11937+
static generateStagingDeployCashBodyAndAssignees({ tag, PRList, PRListMobileExpensify = [], verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false, chronologicalSection = '', }) {
1193811938
return this.fetchAllPullRequests(PRList.map((pr) => this.getPullRequestNumberFromURL(pr)))
1193911939
.then((data) => {
1194011940
const internalQAPRs = Array.isArray(data) ? data.filter((pr) => !(0, isEmptyObject_1.isEmptyObject)(pr.labels.find((item) => item.name === CONST_1.default.LABELS.INTERNAL_QA))) : [];
@@ -12005,6 +12005,10 @@ class GithubUtils {
1200512005
}
1200612006
issueBody += '\r\n\r\n';
1200712007
}
12008+
if (chronologicalSection) {
12009+
issueBody += chronologicalSection;
12010+
issueBody += '\r\n\r\n';
12011+
}
1200812012
issueBody += '**Deployer verifications:**';
1200912013
// eslint-disable-next-line max-len
1201012014
issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-mobile-app/crashlytics/app/ios:com.expensify.expensifylite/issues?state=open&time=last-seven-days&types=crash&tag=all&sort=eventCount) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`;
@@ -12120,6 +12124,26 @@ class GithubUtils {
1212012124
...(options.status && { status: options.status }),
1212112125
});
1212212126
}
12127+
/**
12128+
* Get the workflow run URL for a specific commit SHA and workflow file.
12129+
* Returns the HTML URL of the matching run, or undefined if not found.
12130+
*/
12131+
static async getWorkflowRunURLForCommit(commitSha, workflowFile) {
12132+
try {
12133+
const response = await this.octokit.actions.listWorkflowRuns({
12134+
owner: CONST_1.default.GITHUB_OWNER,
12135+
repo: CONST_1.default.APP_REPO,
12136+
workflow_id: workflowFile,
12137+
head_sha: commitSha,
12138+
per_page: 1,
12139+
});
12140+
return response.data.workflow_runs.at(0)?.html_url;
12141+
}
12142+
catch (error) {
12143+
console.warn(`Failed to find workflow run for commit ${commitSha}:`, error);
12144+
return undefined;
12145+
}
12146+
}
1212312147
/**
1212412148
* Generate the URL of an New Expensify pull request given the PR number.
1212512149
*/
@@ -12277,6 +12301,7 @@ class GithubUtils {
1227712301
commit: commit.sha,
1227812302
subject: commit.commit.message,
1227912303
authorName: commit.commit.author?.name ?? 'Unknown',
12304+
date: commit.commit.committer?.date ?? '',
1228012305
}));
1228112306
}
1228212307
catch (error) {

.github/actions/javascript/checkSVGCompression/index.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20459,7 +20459,7 @@ class GithubUtils {
2045920459
/**
2046020460
* Generate the issue body and assignees for a StagingDeployCash.
2046120461
*/
20462-
static generateStagingDeployCashBodyAndAssignees(tag, PRList, PRListMobileExpensify, verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false) {
20462+
static generateStagingDeployCashBodyAndAssignees({ tag, PRList, PRListMobileExpensify = [], verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false, chronologicalSection = '', }) {
2046320463
return this.fetchAllPullRequests(PRList.map((pr) => this.getPullRequestNumberFromURL(pr)))
2046420464
.then((data) => {
2046520465
const internalQAPRs = Array.isArray(data) ? data.filter((pr) => !(0, isEmptyObject_1.isEmptyObject)(pr.labels.find((item) => item.name === CONST_1.default.LABELS.INTERNAL_QA))) : [];
@@ -20530,6 +20530,10 @@ class GithubUtils {
2053020530
}
2053120531
issueBody += '\r\n\r\n';
2053220532
}
20533+
if (chronologicalSection) {
20534+
issueBody += chronologicalSection;
20535+
issueBody += '\r\n\r\n';
20536+
}
2053320537
issueBody += '**Deployer verifications:**';
2053420538
// eslint-disable-next-line max-len
2053520539
issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-mobile-app/crashlytics/app/ios:com.expensify.expensifylite/issues?state=open&time=last-seven-days&types=crash&tag=all&sort=eventCount) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`;
@@ -20645,6 +20649,26 @@ class GithubUtils {
2064520649
...(options.status && { status: options.status }),
2064620650
});
2064720651
}
20652+
/**
20653+
* Get the workflow run URL for a specific commit SHA and workflow file.
20654+
* Returns the HTML URL of the matching run, or undefined if not found.
20655+
*/
20656+
static async getWorkflowRunURLForCommit(commitSha, workflowFile) {
20657+
try {
20658+
const response = await this.octokit.actions.listWorkflowRuns({
20659+
owner: CONST_1.default.GITHUB_OWNER,
20660+
repo: CONST_1.default.APP_REPO,
20661+
workflow_id: workflowFile,
20662+
head_sha: commitSha,
20663+
per_page: 1,
20664+
});
20665+
return response.data.workflow_runs.at(0)?.html_url;
20666+
}
20667+
catch (error) {
20668+
console.warn(`Failed to find workflow run for commit ${commitSha}:`, error);
20669+
return undefined;
20670+
}
20671+
}
2064820672
/**
2064920673
* Generate the URL of an New Expensify pull request given the PR number.
2065020674
*/
@@ -20802,6 +20826,7 @@ class GithubUtils {
2080220826
commit: commit.sha,
2080320827
subject: commit.commit.message,
2080420828
authorName: commit.commit.author?.name ?? 'Unknown',
20829+
date: commit.commit.committer?.date ?? '',
2080520830
}));
2080620831
}
2080720832
catch (error) {

0 commit comments

Comments
 (0)