Skip to content

Commit 3b4ab9a

Browse files
committed
Merge branch 'main' into issue-82411
2 parents 89dfad9 + 04446e8 commit 3b4ab9a

1,443 files changed

Lines changed: 24979 additions & 16403 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Each rule file contains:
6161
12. **DO NOT describe what you are doing, create comments with a summary, explanations, extra content, comments on rules that are NOT violated or ANYTHING ELSE.**
6262
Only inline comments regarding rules violations are allowed. If no violations are found, add a reaction instead of creating any comment.
6363
EXCEPTION: If you believe something MIGHT be a Rule violation but are uncertain, err on the side of creating an inline comment with your concern rather than skipping it.
64+
13. **Reality check before posting**: Before creating each inline comment, re-read the specific code one more time and confirm the violation is real. If upon re-reading you realize the code is actually correct, **do NOT post the comment** — silently skip it and move on. Never post a comment that flags a violation and then concludes it is not actually a problem.
6465

6566
## Tool Usage Example
6667

.claude/skills/onyx/SKILL.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
name: onyx
3+
description: Onyx state management patterns — useOnyx hook, action files, optimistic updates, collections, and offline-first architecture. Use when working with Onyx connections, writing action files, debugging state, or implementing API calls with optimistic data.
4+
---
5+
6+
## Core Concepts
7+
8+
Onyx is a **persistent storage solution wrapped in a Pub/Sub library** that enables reactive, offline-first data management — key-value storage with automatic AsyncStorage persistence, reactive subscriptions, and collection management.
9+
10+
For the full API reference (initialization, storage providers, cache eviction, benchmarks, Redux DevTools), see https://github.com/Expensify/react-native-onyx/blob/main/README.md.
11+
12+
## Common Patterns
13+
14+
### Action File Pattern
15+
16+
**IMPORTANT:** Onyx state must only be modified from action files (`src/libs/actions/`). Never call `Onyx.merge`, `Onyx.set`, `Onyx.clear`, or `API.write` directly from a component.
17+
18+
```typescript
19+
import Onyx from 'react-native-onyx';
20+
import ONYXKEYS from '@src/ONYXKEYS';
21+
22+
function setIsOffline(isNetworkOffline: boolean, reason = '') {
23+
if (reason) {
24+
Log.info(`[Network] Client is ${isNetworkOffline ? 'offline' : 'online'} because: ${reason}`);
25+
}
26+
Onyx.merge(ONYXKEYS.NETWORK, {isOffline: isNetworkOffline});
27+
}
28+
29+
export {setIsOffline};
30+
```
31+
32+
### Optimistic Updates Pattern
33+
34+
Optimistic updates allow users to see changes immediately while the API request is queued. This is fundamental to Expensify's offline-first architecture.
35+
36+
For **which pattern to use** (A / B / C / D) and UX behavior for each, see https://github.com/Expensify/App/blob/main/contributingGuides/philosophies/OFFLINE.md.
37+
38+
#### Understanding the Three Data Sets
39+
40+
**CRITICAL:** Backend response data is automatically applied via Pusher updates or HTTPS responses. You do NOT manually set backend data in `successData`/`failureData` — only UI state cleanup goes there.
41+
42+
1. **optimisticData** (Applied immediately, before the API call)
43+
- Mirrors what the backend would return on success
44+
- Gives the user instant feedback without waiting for the server
45+
- Often includes `pendingAction` to flag the change as in-flight (e.g. greying out a comment while offline)
46+
- `pendingAction` is cleared once `successData` or `failureData` is applied
47+
48+
2. **successData** (Applied when API succeeds)
49+
- Used for UI state cleanup: clearing `pendingAction`, setting `isLoading: false`
50+
- For `add` actions: often not needed (optimisticData already set the right state)
51+
- For `update`/`delete` actions: include to clear pending state
52+
53+
3. **failureData** (Applied when API fails)
54+
- Reverts optimisticData changes
55+
- Clears `pendingAction`.
56+
- Adds `errors` field for the user to see
57+
- Always include this to handle unexpected failures
58+
59+
For code examples of each pattern (A/B, loading state, `finallyData`), see [offline-patterns.md](offline-patterns.md).
60+
61+
## Performance Optimization
62+
63+
### 1. Subscribe to Specific Collection Members
64+
65+
```typescript
66+
// BAD: re-renders on any report change
67+
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
68+
const myReport = allReports[`report_${reportID}`];
69+
70+
// GOOD: re-renders only when this report changes
71+
const [myReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
72+
```
73+
74+
### 2. Use Selectors to Narrow Re-renders
75+
76+
```typescript
77+
const accountIDSelector = (account: Account) => account?.accountID;
78+
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountIDSelector});
79+
```
80+
81+
`useOnyx` caches by selector reference — a new function reference on every render bypasses the cache and causes unnecessary re-renders. Prefer pure selectors defined in `src/selectors/` over inline functions. If a selector must be defined inside a component, ensure referential stability: React Compiler handles this automatically, but in components that are not compiled, wrap the selector in `useMemo`.
82+
83+
```typescript
84+
// BAD: new function reference on every render defeats caching
85+
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.accountID});
86+
87+
// GOOD: stable reference defined outside the component
88+
// src/selectors/accountSelectors.ts
89+
const selectAccountID = (account: Account) => account?.accountID;
90+
91+
// GOOD: stable reference via useMemo (for non-React-Compiler components)
92+
const selector = useMemo(() => (account: Account) => account?.accountID, []);
93+
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector});
94+
```
95+
96+
For `skipCacheCheck` (large objects) and batch collection update patterns, see https://github.com/Expensify/react-native-onyx/blob/main/README.md.
97+
98+
## Common Pitfalls
99+
100+
### Mixing set and merge on the Same Key
101+
102+
`Onyx.set()` calls are not batched with `Onyx.merge()` calls, which can produce race conditions:
103+
104+
```typescript
105+
// BAD: merge may execute before set resolves
106+
Onyx.set(ONYXKEYS.ACCOUNT, null);
107+
Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true});
108+
109+
// GOOD: use one operation
110+
Onyx.set(ONYXKEYS.ACCOUNT, {validated: true});
111+
```
112+
113+
## Common Tasks Quick Reference
114+
115+
```typescript
116+
// Update a single field
117+
Onyx.merge(ONYXKEYS.NETWORK, {isOffline: true});
118+
119+
// Delete data
120+
Onyx.set(ONYXKEYS.ACCOUNT, null);
121+
122+
// Subscribe in component
123+
const [data] = useOnyx(ONYXKEYS.SOME_KEY);
124+
125+
// Subscribe with selector
126+
const [field] = useOnyx(ONYXKEYS.SOME_KEY, {selector: (data) => data?.specificField});
127+
128+
// Update collection member
129+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {unread: false});
130+
131+
// Batch update collection
132+
Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, updates);
133+
134+
// API call with optimistic update
135+
API.write('SomeCommand', params, {optimisticData, successData, failureData});
136+
```
137+
138+
## Related Files
139+
140+
- https://github.com/Expensify/react-native-onyx/blob/main/README.md - Full Onyx API reference (initialization, merge/set/connect, collections, loading state, cache eviction, Redux DevTools, benchmarks)
141+
- https://github.com/Expensify/App/blob/main/contributingGuides/philosophies/OFFLINE.md - Full offline UX patterns, decision flowchart, and when to use each pattern (A/B/C/D)
142+
- [offline-patterns.md](offline-patterns.md) - Code examples for each optimistic update pattern
143+
- https://github.com/Expensify/App/blob/main/src/ONYXKEYS.ts - All Onyx key definitions
144+
- https://github.com/Expensify/App/tree/main/src/libs/actions - Action files that update Onyx
145+
- https://github.com/Expensify/App/blob/main/src/hooks/useOnyx.ts - useOnyx hook implementation
146+
- https://github.com/Expensify/App/tree/main/src/types/onyx - TypeScript types for Onyx data
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Optimistic Patterns Code Examples
2+
3+
## Pattern A: Optimistic Without Feedback
4+
5+
No `successData`/`failureData` — fire and forget.
6+
7+
```typescript
8+
function pinReport(reportID: string) {
9+
const optimisticData: OnyxUpdate[] = [
10+
{
11+
onyxMethod: Onyx.METHOD.MERGE,
12+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
13+
value: {isPinned: true},
14+
},
15+
];
16+
17+
API.write('TogglePinnedChat', {reportID}, {optimisticData});
18+
}
19+
```
20+
21+
## Pattern B: Optimistic With Feedback
22+
23+
Show pending state; revert or clean up on completion.
24+
25+
```typescript
26+
function deleteReport(reportID: string) {
27+
const optimisticData: OnyxUpdate[] = [
28+
{
29+
onyxMethod: Onyx.METHOD.MERGE,
30+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
31+
value: {
32+
statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
33+
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
34+
},
35+
},
36+
];
37+
38+
const successData: OnyxUpdate[] = [
39+
{
40+
onyxMethod: Onyx.METHOD.SET,
41+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
42+
value: null,
43+
},
44+
];
45+
46+
const failureData: OnyxUpdate[] = [
47+
{
48+
onyxMethod: Onyx.METHOD.MERGE,
49+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
50+
value: {
51+
statusNum: null,
52+
pendingAction: null,
53+
errors: {[DateUtils.getMicroseconds()]: 'Failed to delete report'},
54+
},
55+
},
56+
];
57+
58+
API.write('DeleteReport', {reportID}, {optimisticData, successData, failureData});
59+
}
60+
```
61+
62+
## Example with Loading State
63+
64+
```typescript
65+
function sendMessage(reportID: string, text: string) {
66+
const optimisticData: OnyxUpdate[] = [
67+
{
68+
onyxMethod: Onyx.METHOD.MERGE,
69+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
70+
value: {
71+
isLoading: true,
72+
lastMessageText: text,
73+
},
74+
},
75+
];
76+
77+
const successData: OnyxUpdate[] = [
78+
{
79+
onyxMethod: Onyx.METHOD.MERGE,
80+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
81+
value: {
82+
isLoading: false,
83+
pendingAction: null,
84+
},
85+
},
86+
];
87+
88+
const failureData: OnyxUpdate[] = [
89+
{
90+
onyxMethod: Onyx.METHOD.MERGE,
91+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
92+
value: {
93+
isLoading: false,
94+
lastMessageText: null,
95+
pendingAction: null,
96+
errors: {[DateUtils.getMicroseconds()]: 'Failed to send message'},
97+
},
98+
},
99+
];
100+
101+
API.write('AddComment', {reportID, text}, {optimisticData, successData, failureData});
102+
}
103+
```
104+
105+
## Using finallyData
106+
107+
When `successData` and `failureData` would be identical, use `finallyData` instead:
108+
109+
```typescript
110+
const finallyData: OnyxUpdate[] = [
111+
{
112+
onyxMethod: Onyx.METHOD.MERGE,
113+
key: ONYXKEYS.SOME_KEY,
114+
value: {
115+
isLoading: false,
116+
pendingAction: null,
117+
},
118+
},
119+
];
120+
121+
API.write('SomeCommand', params, {optimisticData, finallyData});
122+
```

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c
8686
- [ ] iOS: mWeb Safari
8787
- [ ] MacOS: Chrome / Safari
8888
- [ ] I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
89-
- [ ] I verified there are no new alerts related to the `canBeMissing` param for `useOnyx`
9089
- [ ] I followed proper code patterns (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code))
9190
- [ ] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`)
9291
- [ ] I verified that comments were added to code that is not self explanatory

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15589,7 +15589,8 @@ const CONST = {
1558915589
},
1559015590
COMMENT: {
1559115591
TYPE_BOT: 'Bot',
15592-
NAME_MELVIN: 'melvin-bot',
15592+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
15593+
NAME_MELVIN_USER: 'MelvinBot',
1559315594
NAME_CODEX: 'chatgpt-codex-connector',
1559415595
NAME_GITHUB_ACTIONS: 'github-actions',
1559515596
},
@@ -15806,7 +15807,7 @@ class GithubUtils {
1580615807
PRListMobileExpensify: this.getStagingDeployCashPRListMobileExpensify(issue),
1580715808
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
1580815809
internalQAPRList: this.getStagingDeployCashInternalQA(issue),
15809-
isFirebaseChecked: issue.body ? /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body) : false,
15810+
isSentryChecked: issue.body ? /-\s\[x]\sI checked \[Sentry]/.test(issue.body) : false,
1581015811
isGHStatusChecked: issue.body ? /-\s\[x]\sI checked \[GitHub Status]/.test(issue.body) : false,
1581115812
version,
1581215813
tag: `${version}-staging`,
@@ -15888,7 +15889,7 @@ class GithubUtils {
1588815889
/**
1588915890
* Generate the issue body and assignees for a StagingDeployCash.
1589015891
*/
15891-
static generateStagingDeployCashBodyAndAssignees({ tag, PRList, PRListMobileExpensify = [], verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isFirebaseChecked = false, isGHStatusChecked = false, chronologicalSection = '', }) {
15892+
static generateStagingDeployCashBodyAndAssignees({ tag, PRList, PRListMobileExpensify = [], verifiedPRList = [], verifiedPRListMobileExpensify = [], deployBlockers = [], resolvedDeployBlockers = [], resolvedInternalQAPRs = [], isSentryChecked = false, isGHStatusChecked = false, previousTag = '', chronologicalSection = '', }) {
1589215893
return this.fetchAllPullRequests(PRList.map((pr) => this.getPullRequestNumberFromURL(pr)))
1589315894
.then((data) => {
1589415895
const internalQAPRs = Array.isArray(data) ? data.filter((pr) => !(0, isEmptyObject_1.isEmptyObject)(pr.labels.find((item) => item.name === CONST_1.default.LABELS.INTERNAL_QA))) : [];
@@ -15965,9 +15966,9 @@ class GithubUtils {
1596515966
}
1596615967
issueBody += '**Deployer verifications:**';
1596715968
// eslint-disable-next-line max-len
15968-
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).`;
15969+
issueBody += `\r\n- [${isSentryChecked ? 'x' : ' '}] I checked [Sentry](https://expensify.sentry.io/releases/new.expensify%40${tag}/?project=app&environment=staging) 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).`;
1596915970
// eslint-disable-next-line max-len
15970-
issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-mobile-app/crashlytics/app/android:org.me.mobiexpensifyg/issues?state=open&time=last-seven-days&types=crash&tag=all&sort=eventCount) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`;
15971+
issueBody += `\r\n- [${isSentryChecked ? 'x' : ' '}] I checked [Sentry](https://expensify.sentry.io/releases/new.expensify%40${previousTag}/?project=app&environment=production) for **the previous release version** and verified that the release did not introduce any new crashes. Because mobile deploys use a phased rollout, completing this checklist will deploy the previous release version to 100% of users. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`;
1597115972
// eslint-disable-next-line max-len
1597215973
issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`;
1597315974
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';

0 commit comments

Comments
 (0)