Skip to content

Commit 60d50c5

Browse files
committed
Merge branch 'main' into korytko/ecuk/3ds/implement-native-hooks
2 parents 8a2590c + 44e5a05 commit 60d50c5

File tree

140 files changed

+3613
-1930
lines changed

Some content is hidden

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

140 files changed

+3613
-1930
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:**

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 1009031201
118-
versionName "9.3.12-1"
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"
Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,77 @@
11
---
2-
title: Personal Cards
3-
description: Learn how to track and manage your personal credit card expenses in Expensify through automatic imports or manual uploads.
4-
keywords: [New Expensify, personal cards, credit card import, CSV upload, SmartScan, eReceipts, reimbursements]
2+
title: Manage personal cards in New Expensify
3+
description: Learn how to view and manage personal credit cards in New Expensify.
4+
keywords: [personal cards, Wallet, reimbursable, credit card import, Expensify Classic, New Expensify, assigned cards, card details]
5+
internalScope: Audience is members with personal credit cards already connected to their account. Covers viewing and managing existing personal cards in the Wallet, including reimbursable settings. Does not cover adding new personal cards or company card programs.
56
---
67

8+
# Manage personal cards in New Expensify
79

8-
Tracking personal credit card expenses in Expensify is simple and flexible. Whether you're getting reimbursed or just organizing your spending, you can connect a personal card or upload transactions manually.
10+
Expensify lets you view and manage your personal credit cards in one place, making it easier to track spending, submit reimbursable expenses, and stay organized for tax time.
11+
12+
If you previously connected a personal card in Expensify Classic, you can now manage that card directly in Expensify on both web and mobile.
13+
14+
---
15+
16+
## Who can manage personal cards
17+
18+
You can manage personal cards if you have a personal credit card that was already connected to your account. You can manage these cards on both web and mobile, alongside any company cards on your account.
19+
20+
---
21+
22+
## Where to find personal cards in the Wallet
23+
24+
1. Navigate to **Account > Wallet** on web or mobile.
25+
2. Under **Assigned cards**:
26+
- Personal cards imported from Expensify Classic will appear here.
27+
- You'll see the card name, bank icon, and last 4 digits.
28+
3. Tap the personal card you want to manage.
29+
30+
If you have both company and personal cards, you'll see them separated into **Company cards** and **Personal cards** sections.
31+
32+
![Tap a personal card to open card details]({{site.url}}/assets/images/personal-card-01.png){:width="100%"}
933

1034
---
1135

12-
# Overview
36+
## What you can do with personal cards
37+
38+
You can:
39+
- View all personal cards imported from Expensify Classic
40+
- Automatically import transactions from your linked personal cards
41+
- Generate IRS-compliant eReceipts for eligible USD purchases
42+
- Update personal card settings
43+
- Filter expenses by card in the **Expenses** tab on the **Reports** page
44+
45+
---
1346

14-
Once available in **New Expensify**, personal card connections will allow you to:
47+
## How to update personal card settings
1548

16-
- **Automatically import transactions** from your linked card
17-
- **Manually upload CSV files** with your expense history
18-
- **Merge imported transactions with SmartScanned receipts**
19-
- Generate **IRS-compliant eReceipts** for eligible USD purchases
49+
1. Navigate to **Account > Wallet** on web or mobile.
50+
2. Under **Assigned cards**, select a personal card.
51+
3. On the **Card details** page, you can:
52+
- Rename the card
53+
- Update the card to pull in new transactions (if not CSV-imported)
54+
- Unassign the card if it's no longer needed
55+
- Toggle **Mark transactions as reimbursable**
56+
- The reimbursable setting applies only to **new** transactions and is turned on by default.
2057

21-
These features make it easy to capture personal expenses for reimbursement, tax reporting, or budgeting.
58+
**Note:** Unassigning a personal card permanently deletes any unreported expenses or expenses on draft reports from that card.
59+
60+
![Update personal card settings from the card details page]({{site.url}}/assets/images/personal-card-02.png){:width="100%"}
2261

2362
---
2463

25-
# Feature Status
64+
# FAQ
65+
66+
## Can I add another personal card?
67+
68+
Not yet. Only cards connected to your account in Expensify Classic are available to manage in New Expensify. Support for adding new personal cards will be available in a future update.
69+
70+
## Can I change the reimbursable setting for past transactions?
2671

27-
We’re currently building the personal card connection functionality for New Expensify. This feature is not yet available but will be released soon.
72+
No. Changes to the reimbursable setting only apply to transactions imported after the change.
2873

29-
Once it's live, we’ll update this article with full setup instructions, screenshots, and tips for managing your imported expenses.
74+
## Why don’t I see my personal card in the Wallet?
3075

31-
**Stay tuned!** You can follow product announcements or contact Concierge for updates on availability.
76+
Personal cards appear in the Wallet only if they were previously connected to your account in Expensify Classic. If your card doesn’t show up, it means it wasn’t connected before and isn’t available to manage yet.
3277

0 commit comments

Comments
 (0)