[NO QA] Create DynamicVerifyAccountPage Component (BATCH-4) v2#82256
Conversation
|
🚧 @mjasikowski has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
|
@huult Could you check if you can reproduce this issue here?
Screen.Recording.2026-02-12.at.14.05.50.mov
Screen.Recording.2026-02-12.at.14.11.21.mov |
Screen.Recording.2026-02-12.at.21.03.42.mp4@WojtekBoman Yes, this PR can’t reproduce my issue |
| /** | ||
| * Checks if a screen name is a dynamic route screen | ||
| */ | ||
| function isDynamicRouteScreen(screenName: Screen): boolean { |
There was a problem hiding this comment.
❌ PERF-13 (docs)
The function call Object.values(DYNAMIC_ROUTES) inside the for loop does not use the loop iterator and produces the same result on every iteration. This creates redundant computation that scales with the number of dynamic route entries.
Suggested fix: Hoist the Object.values() call outside the function:
const dynamicRouteEntries = Object.values(DYNAMIC_ROUTES);
function isDynamicRouteScreen(screenName: Screen): boolean {
const screenPath = normalizedConfigs[screenName]?.path;
if (!screenPath) {
return false;
}
for (const {path} of dynamicRouteEntries) {
if (screenPath === path) {
return true;
}
}
return false;
}Or better yet, use .some() which is more idiomatic:
const dynamicRouteEntries = Object.values(DYNAMIC_ROUTES);
function isDynamicRouteScreen(screenName: Screen): boolean {
const screenPath = normalizedConfigs[screenName]?.path;
if (!screenPath) {
return false;
}
return dynamicRouteEntries.some(({path}) => screenPath === path);
}Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| type DynamicRouteKey = keyof typeof DYNAMIC_ROUTES; | ||
|
|
||
| // Find the dynamic route key that matches the extracted suffix | ||
| const dynamicRoute: string = Object.keys(DYNAMIC_ROUTES).find((key) => DYNAMIC_ROUTES[key as DynamicRouteKey].path === dynamicRouteSuffix) ?? ''; |
There was a problem hiding this comment.
❌ PERF-13 (docs)
The function call Object.keys(DYNAMIC_ROUTES) does not use the key iterator variable and produces the same result on every iteration. This creates O(n) redundant computation.
Suggested fix: Hoist the call outside the loop or use a more efficient approach:
// Option 1: Hoist outside
const dynamicRouteSuffix = getLastSuffixFromPath(normalizedPathAfterRedirection);
if (isDynamicRouteSuffix(dynamicRouteSuffix)) {
const pathWithoutDynamicSuffix = normalizedPathAfterRedirection.replace(`/${dynamicRouteSuffix}`, '');
type DynamicRouteKey = keyof typeof DYNAMIC_ROUTES;
const dynamicRouteKeys = Object.keys(DYNAMIC_ROUTES) as DynamicRouteKey[];
// Find the dynamic route key that matches the extracted suffix
const dynamicRoute: string = dynamicRouteKeys.find((key) => DYNAMIC_ROUTES[key].path === dynamicRouteSuffix) ?? '';
// ...
}
// Option 2: Use Object.entries (more efficient)
const dynamicRouteEntry = Object.entries(DYNAMIC_ROUTES).find(
([, value]) => value.path === dynamicRouteSuffix
);
const dynamicRoute = dynamicRouteEntry?.[0] ?? '';Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| type RouteNode = LeafRoute | NestedRoute; | ||
|
|
||
| function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): string[] | null { | ||
| const configEntries = Object.entries(normalizedConfigs); |
There was a problem hiding this comment.
❌ PERF-13 (docs)
The function call Object.entries(normalizedConfigs) does not use the loop iterator and produces the same result on every iteration. This creates redundant computation.
Suggested fix: Hoist the call outside the function:
const normalizedConfigEntries = Object.entries(normalizedConfigs);
function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): string[] | null {
// Search through normalized configs to find matching path and extract navigation hierarchy
// routeNames contains the sequence of screen/navigator names that should be present in the navigation state
for (const [, config] of normalizedConfigEntries) {
if (config.path === dynamicRouteName) {
return config.routeNames;
}
}
return null;
}Or use .find() which is more idiomatic:
const normalizedConfigEntries = Object.entries(normalizedConfigs);
function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): string[] | null {
const entry = normalizedConfigEntries.find(([, config]) => config.path === dynamicRouteName);
return entry?.[1].routeNames ?? null;
}Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| const pathWithoutParams = path?.split('?').at(0); | ||
|
|
||
| if (!pathWithoutParams) { | ||
| throw new Error('[getLastSuffixFromPath.ts] Failed to parse the path, path is empty'); |
There was a problem hiding this comment.
❌ CONSISTENCY-6 (docs)
This function throws an error when the path is empty, but there is no try-catch handling at the call sites. If an unexpected empty path is passed, the error will propagate uncaught, potentially causing the app to crash.
Suggested fix: Add proper error handling or make the return type explicit about the possibility of errors:
/**
* Extracts the last segment from a URL path, removing query parameters and trailing slashes.
*
* @param path - The URL path to extract the suffix from (can be undefined)
* @returns The last segment of the path as a string, or empty string if path is invalid
*/
function getLastSuffixFromPath(path: string | undefined): string {
const pathWithoutParams = path?.split("?").at(0);
if (\!pathWithoutParams) {
Log.warn("[getLastSuffixFromPath.ts] Failed to parse the path, path is empty");
return "";
}
const pathWithoutTrailingSlash = pathWithoutParams.endsWith("/") ? pathWithoutParams.slice(0, -1) : pathWithoutParams;
const lastSuffix = pathWithoutTrailingSlash.split("/").pop() ?? "";
return lastSuffix;
}Or wrap calls in try-catch blocks to handle the error gracefully.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| type NestedRoute = { | ||
| name: string; | ||
| state: { | ||
| routes: [RouteNode]; |
There was a problem hiding this comment.
❌ CONSISTENCY-2 (docs)
The magic number 0 is used twice in the type definition without explanation. The literal value 0 as an index type constraint is not self-explanatory and makes the code harder to understand.
Suggested fix: Add a comment explaining why the index is constrained to 0, or use a more descriptive approach:
type NestedRoute = {
name: string;
state: {
routes: [RouteNode]; // Single route array - index must be 0 to focus on the only route
index: 0; // Always 0 since routes array contains exactly one element
};
};Or better yet, remove the tuple constraint if not strictly necessary:
type NestedRoute = {
name: string;
state: {
routes: RouteNode[];
index: number;
};
};Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| function DynamicVerifyAccountPage() { | ||
| const backPath = useDynamicBackPath(DYNAMIC_ROUTES.VERIFY_ACCOUNT.path); | ||
| // currently, the default behavior of this component after completing verification is to navigate back | ||
| const forwardPath = backPath; |
There was a problem hiding this comment.
❌ PERF-6 (docs)
The forwardPath variable is redundantly assigned the same value as backPath. This is unnecessary state derivation that could be simplified.
Suggested fix: Either use backPath directly in both places, or if there is a plan for different forward behavior, add a comment explaining the future intent:
function DynamicVerifyAccountPage() {
const backPath = useDynamicBackPath(DYNAMIC_ROUTES.VERIFY_ACCOUNT.path);
return (
<VerifyAccountPageBase
navigateBackTo={backPath}
navigateForwardTo={backPath}
/>
);
}Or with future intent documented:
function DynamicVerifyAccountPage() {
const backPath = useDynamicBackPath(DYNAMIC_ROUTES.VERIFY_ACCOUNT.path);
// TODO: In the future, forwardPath may navigate to a different destination after verification
const forwardPath = backPath;
return (
<VerifyAccountPageBase
navigateBackTo={backPath}
navigateForwardTo={forwardPath}
/>
);
}Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| newPath += `?${params}`; | ||
| // This should never happen as the path should always be defined | ||
| if (!normalizedPath) { | ||
| throw new Error('Path is undefined or empty'); |
There was a problem hiding this comment.
❌ CONSISTENCY-6 (docs)
This function throws an error when the path is undefined or empty, but there is no error handling at the call sites. If splitPathAndQuery returns undefined for normalizedPath, the error will propagate uncaught.
Suggested fix: Add proper error handling or ensure the function is more defensive:
const combinePathAndSuffix = (path: string, suffix: string): Route => {
const [normalizedPath, query] = splitPathAndQuery(path);
if (!normalizedPath) {
Log.warn("[createDynamicRoute.ts] Path is undefined or empty, returning suffix only", {path, suffix});
return suffix as Route;
}
let newPath = `${normalizedPath}/${suffix}`;
if (query) {
newPath += `?${query}`;
}
return newPath as Route;
};This gracefully handles the error case while maintaining functionality.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| } | ||
|
|
||
| // Handle dynamic routes: find the appropriate full screen route | ||
| if (route.path) { |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
The dynamic route handling logic in this function duplicates similar logic from getStateFromPath.ts (lines 24-54). Both functions:
- Extract the dynamic route suffix using
getLastSuffixFromPath - Check if it is a dynamic suffix using
isDynamicRouteSuffix - Remove the suffix and process the base path
- Handle special cases for navigation state
Suggested fix: Extract the shared logic into a reusable helper function:
// In a shared helper file
function parseDynamicRoute(path: string) {
const dynamicRouteSuffix = getLastSuffixFromPath(path);
if (!isDynamicRouteSuffix(dynamicRouteSuffix)) {
return null;
}
const pathWithoutDynamicSuffix = path.replace(`/${dynamicRouteSuffix}`, "");
return {
suffix: dynamicRouteSuffix,
basePath: pathWithoutDynamicSuffix,
};
}Then use this helper in both locations to avoid duplication.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| const focusedRoute = findFocusedRoute(getStateFromPath(pathWithoutDynamicSuffix as Route) ?? {}); | ||
| const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? []; | ||
|
|
||
| // Check if the focused route is allowed to access this dynamic route |
There was a problem hiding this comment.
❌ PERF-2 (docs)
The recursive call to getStateFromPath(pathWithoutDynamicSuffix as Route) is expensive, but happens before checking if focusedRoute?.name exists. If focusedRoute?.name is falsy, all the work done to get and process entryScreens is wasted.
Suggested fix: Return early if focusedRoute?.name does not exist:
const dynamicRouteSuffix = getLastSuffixFromPath(normalizedPathAfterRedirection);
if (isDynamicRouteSuffix(dynamicRouteSuffix)) {
const pathWithoutDynamicSuffix = normalizedPathAfterRedirection.replace(`/${dynamicRouteSuffix}`, "");
type DynamicRouteKey = keyof typeof DYNAMIC_ROUTES;
// Find the dynamic route key that matches the extracted suffix
const dynamicRoute: string = Object.keys(DYNAMIC_ROUTES).find((key) => DYNAMIC_ROUTES[key as DynamicRouteKey].path === dynamicRouteSuffix) ?? "";
// Get the currently focused route from the base path to check permissions
const focusedRoute = findFocusedRoute(getStateFromPath(pathWithoutDynamicSuffix as Route) ?? {});
// Early return if no focused route name
if (!focusedRoute?.name) {
// Fall through to default handling
} else {
const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? [];
// Check if the focused route is allowed to access this dynamic route
if (entryScreens.includes(focusedRoute.name as Screen)) {
// Generate navigation state for the dynamic route
const dynamicRouteState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey);
return dynamicRouteState;
}
// ... rest of the logic
}
}Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d69c9b1eae
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| throw new Error('Path is undefined or empty'); | ||
| } | ||
|
|
||
| let newPath = `${normalizedPath}/${suffix}`; |
There was a problem hiding this comment.
Preserve single slash when appending dynamic suffix
When the current route is /, this new concatenation builds //<suffix> (for example //verify-account) instead of /verify-account. That malformed path can break navigation matching and browser history sync for root-origin flows that call createDynamicRoute, because downstream parsing expects a normalized single-leading-slash URL.
Useful? React with 👍 / 👎.
|
@WojtekBoman Please take a look at the AI comment |
|
|
8135b93 to
a66aacd
Compare
|
@huult Hi, all comments have been addressed 😄 |
|
Removing Tim, Stephanie and Design from reviewers, they were added by a faulty code merge |
|
Thank you, I’ll review it within the next hour |
|
@collectioneur Please run npm run prettier and commit the changes afterward |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-02-13.at.20.22.13.mp4Android: mWeb ChromeScreen.Recording.2026-02-13.at.20.24.30.mp4iOS: HybridAppScreen.Recording.2026-02-13.at.20.27.13.mp4iOS: mWeb SafariScreen.Recording.2026-02-13.at.20.28.41.mp4MacOS: Chrome / SafariScreen.Recording.2026-02-13.at.20.15.40.mp4 |
|
@collectioneur Please update the lint checks so I can approve this PR. Thanks! |
|
@huult updated ✅ |
|
🚧 @mjasikowski has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/mjasikowski in version: 9.3.26-0 🚀
|
|
🚀 Deployed to production by https://github.com/puneetlath in version: 9.3.26-8 🚀
|
Explanation of Change
This PR is the next version of this PR #81392. It includes a fix for useDynamicBackPath, because we were using
useNavigationStateinstead ofuseRootNavigationState, the backPath was generated incorrectly.Fixed Issues
$ #80909
PROPOSAL:
Tests
Offline tests
QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari