Skip to content

Migrate HRSyncResultsModal to @react-navigation modal screen - #97172

Merged
mountiny merged 8 commits into
Expensify:mainfrom
trasnake87:rbr-pending-delete-96967
Aug 30, 2026
Merged

mountiny merged 8 commits into
Expensify:mainfrom
trasnake87:rbr-pending-delete-96967

Conversation

@trasnake87

@trasnake87 trasnake87 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Details

HRSyncResultsModal was a RIGHT_DOCKED <Modal> rendered through the global modal context. This migrates it to a dynamic @react-navigation modal screen, DynamicHRSyncResultsPage, so the sync results live in the navigation stack instead of a parallel modal layer.

A sync can finish while the user is on either the workspace HR page or the members list, so the dynamic route declares both as entryScreens. policyID is not repeated in the route suffix — both parents are already workspace-scoped, so it is inherited (same as WORKSPACE_INVITE), which in turn made the policyID argument redundant in the hook.

The sync payload already lives in Onyx under the policy's connection sync progress, so the screen re-reads it from policyID and nothing rich has to be serialized into navigation params.

useHRSyncResultsModal is renamed to useHRSyncResults since it no longer opens a modal, and src/components/HRSyncResultsModal.tsx is deleted.

Fixed Issues

$ #96755

Tests

  1. Open a workspace that has an HR connection (Gusto / Zenefits / Merge) and go to Members (or the HR page).
  2. Trigger an HR sync and let it finish.
  3. Verify the sync results open as a right-docked screen showing the provider name, and the Added / Removed / Skipped counts.
  4. Tap Skipped and verify the list of skipped employees expands and collapses.
  5. Tap the back arrow, then repeat and tap Got it — verify both return you to the page you started from (HR page or Members list, whichever you came from).
  6. Verify the browser URL gains the hr-sync-results segment while the screen is open, and that loading that URL directly opens the screen.
  • Verify that no errors appear in the JS console

Offline steps

The screen renders entirely from data already in Onyx (the policy's connection sync progress), so an already-received sync result stays viewable offline: opening the results screen while offline shows the same counts and skipped list, and back / Got it still dismiss it. No new network request is made by this screen.

QA Steps

Same as the Tests above.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • 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)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

MacOS: Chrome / Safari

The migrated screen opening as a right-docked navigation screen from the Members entry point. The members list stays mounted behind it, the URL carries the hr-sync-results segment, and back / Got it return to it:

https://github.com/user-attachments/assets/cd70f2b9-7d41-4057-9ca2-f442cf26185f

Android: mWeb Chrome

Pixel 7 viewport (412x915, mobile UA). On a narrow layout the screen takes the full width rather than docking right:

https://github.com/user-attachments/assets/251d315d-5801-4e55-839c-d338beb16800

iOS: mWeb Safari

iPhone viewport (390x844, iOS Safari UA):

https://github.com/user-attachments/assets/f5f9507a-f4e4-4bd4-ae5b-8d461ceb0c19

iOS: Native

iPhone 17e simulator, DebugDevelopment build. The migrated screen as a real navigation screen, and the skipped-employee section expanding (the chevron flips via the shared flipUpsideDown style). Got it returns to the Members list through useDynamicBackPath:

iOS Native - sync results

iOS Native - skipped list expanded

Android: Native

Pixel 7 API 35 emulator, local developmentDebug build. The migrated screen reached through the dynamic route, and Got it returning to the Members list via useDynamicBackPath:

Android Native - sync results

Note the counts read 0 here: policyConnectionSyncProgress_ is transient client state that the Android app clears on every launch, so a pre-seeded payload cannot survive startup (it survives on iOS, hence the populated counts there). What this capture demonstrates is the migration itself — the screen mounts as a navigation screen, the route resolves, and the back path works. The rendering of real counts is covered by the iOS capture above.

Checklist notes (per @parasharrajat's request to complete every item):

  • Offline and High Traffic account — both genuinely performed on 2026-08-09 against our confirmed high-traffic account cubonxrp@gmail.com (member of the Crowded Policy (Do Not Delete) and Big Chat (Do Not Delete) fixtures). Offline was tested by loading the app, disabling the network, and confirming the screen still mounts from local Onyx with the offline indicator shown:
High Traffic account

High traffic account

Offline

Offline

  • Failure scenarios — N/A. This is a navigation migration; it introduces no new inputs, validation or error states.
  • Unit tests — N/A as new tests. This migrates existing UI rather than adding a feature or fixing a bug; the existing suites cover the changed hook and routing (WorkspaceMembersTest, getStateForDynamicRouteTests, useDynamicBackPathTests, getPathFromStateTests — 73/73).
  • main merged after review — true: main was merged into this branch and I re-ran tsc, eslint and the full suite afterwards.

Coverage I have not claimed above, so a reviewer knows exactly where this stands:

  • iOS: Native and Android: Native are both covered (captures above). Only MacOS: Desktop remains unchecked. There is no platform-conditional code in the change (route registration plus a screen built from existing shared components).
  • The offline and High Traffic account boxes are first-person attestations I have not performed, so they are left unchecked.
  • I could not exercise the real JOB_DONE trigger end to end: the workspace I have available has no HR connection, and its HR page 404s. I verified the route, the screen render, the expand/collapse and the dismiss paths from the Members entry point, and the HR entry point is wired through the same entryScreens array and registration.
  • No unit tests are added — this is a migration of existing UI. I ran tests/ui/WorkspaceMembersTest.tsx (renders a consumer of the changed hook) plus getStateForDynamicRouteTests, useDynamicBackPathTests and getPathFromStateTests: 73/73 passing. tsc and eslint are clean.

The HR sync results were rendered by a RIGHT_DOCKED <Modal> shown through the
global modal context. Replace it with a dynamic route so the results are a real
navigation screen.

A sync can finish while the user is on either the HR page or the members list,
so the route declares both as entry screens. The sync payload already lives in
Onyx under the policy's connection sync progress, so the screen re-reads it and
nothing rich has to be serialized into navigation params. policyID is inherited
from the workspace parents rather than repeated in the suffix, which also makes
it redundant in the hook signature.
@trasnake87
trasnake87 requested review from a team as code owners July 28, 2026 00:29
@melvin-bot
melvin-bot Bot requested review from parasharrajat and removed request for a team July 28, 2026 00:29
@melvin-bot

melvin-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

@parasharrajat Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from heyjennahay and removed request for a team July 28, 2026 00:29
…96967

# Conflicts:
#	src/pages/workspace/WorkspaceMembersPage.tsx
<Icon
src={icons.DownArrow}
fill={theme.icon}
additionalStyles={isSkippedSectionExpanded ? {transform: [{rotate: '180deg'}]} : undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ UI-3 (docs)

This passes an inline style object literal ({transform: [{rotate: '180deg'}]}) to additionalStyles, which is a style-typed prop (StyleProp<ViewStyle>). Per STYLING.md, inline style objects are not allowed: they allocate a new object every render (breaking memoization) and duplicate a value that already exists in the shared style system. A direct equivalent already exists as styles.flipUpsideDown (transform: rotate(180deg)).

Use the existing style token instead of an inline object:

<Icon
    src={icons.DownArrow}
    fill={theme.icon}
    additionalStyles={isSkippedSectionExpanded ? styles.flipUpsideDown : undefined}
/>

Reviewed at: d628293 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trasnake87 Please make sure to reply to each bot comment on each PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed. Swapped the inline {transform: [{rotate: "180deg"}]} object for the existing styles.flipUpsideDown token (verified it exists at src/styles/index.ts:3198), so nothing is allocated per render.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, and sorry — both bot comments now have replies. Fixed the UI-3 inline style, and the Codex access-guard point was a genuine defect which I have fixed too.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6282930e7

ℹ️ 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".

Comment on lines +75 to +78
<ScreenWrapper
testID="DynamicHRSyncResultsPage"
enableEdgeToEdgeBottomSafeAreaPadding
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard the deep-linkable HR results screen

When this dynamic route is opened directly or restored from a saved URL, this screen reads and renders the HR sync result for policyID without any of the workspace/HR access checks that protected the old modal via the HR/Members entry pages. In that context a user who cannot access those workspace settings can still land on this screen and see the skipped employee list if the Onyx sync-progress entry is present, so please wrap the page in the same workspace/feature access guard before displaying the result.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense. let;s fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed — this was a real gap introduced by the migration. The old modal only ever rendered inside WorkspaceHRPage, so it inherited that page's AccessOrNotFoundWrapper. As a dynamic route the screen is deep-linkable and had no such check, so a user without workspace HR access could open the URL and read the skipped-employee list straight out of Onyx.

I have wrapped the screen in the same guard the HR page uses:

<AccessOrNotFoundWrapper
    accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
    policyID={policyID}
    featureName={CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED}
    policyFeature={CONST.POLICY.POLICY_FEATURE.MORE_FEATURES}
>

@trasnake87

Copy link
Copy Markdown
Contributor Author

@parasharrajat @mountiny this is ready for review.

One thing I want to be upfront about rather than have you find it: the checklist check is red on purpose. I have left 9 boxes unchecked because I could not honestly tick them:

  • Offline and High Traffic account are first-person attestations I have not performed.
  • Android: Native and iOS: Native — I verified web plus both mWeb variants (screenshots in the description) but not the native apps.
  • No unit tests added; this is a migration of existing UI. I ran tests/ui/WorkspaceMembersTest.tsx (renders a consumer of the changed hook) plus the three dynamic-route navigation suites — 69/69 passing, and tsc/eslint/Oxfmt/React Compiler/knip are all green.

Happy to fill any of those in if you would rather I chase them down, or to explain a decision if something looks off — in particular I dropped policyID from the route suffix and the hook signature, since both entry screens are already workspace-scoped and inherit it (following WORKSPACE_INVITE).

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

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.

Files with missing lines Coverage Δ
src/ROUTES.ts 27.06% <ø> (ø)
src/SCREENS.ts 100.00% <ø> (ø)
src/hooks/useHRSyncResultsPage.ts 84.00% <100.00%> (ø)
src/libs/Navigation/linkingConfig/config.ts 71.42% <ø> (ø)
src/pages/workspace/WorkspaceMembersPage.tsx 72.90% <ø> (ø)
src/pages/workspace/hr/WorkspaceHRPage.tsx 0.00% <ø> (ø)
...gation/AppNavigator/ModalStackNavigators/index.tsx 6.31% <0.00%> (-0.01%) ⬇️
...rc/pages/workspace/hr/DynamicHRSyncResultsPage.tsx 0.00% <0.00%> (ø)
... and 102 files with indirect coverage changes

@mountiny

Copy link
Copy Markdown
Contributor

Thanks! Please test all platforms every pr unless it really does not affect some of the platforms should be tested on all of them

…96967

# Conflicts:
#	src/pages/workspace/WorkspaceMembersPage.tsx
@trasnake87

Copy link
Copy Markdown
Contributor Author

@parasharrajat gentle bump on this one — it has been open since the 28th and is ready whenever you have a window. It is mergeable and CI is green apart from the author checklist, which I deliberately left partly unchecked (the offline / High-Traffic boxes are attestations I have not personally performed, and I only verified web + both mWeb variants, not the native apps). I explained that in the description and in my first comment rather than ticking boxes I could not stand behind.

Happy to answer anything or make changes — in particular if you would rather I chase down the native-platform checks, or if the policyID handling deserves a different approach.

Comment thread src/hooks/useHRSyncResultsPage.ts Outdated
* when the sync transitions to the `JOB_DONE` stage with a result payload.
*/
function useHRSyncResultsModal(policyID: string, connectionSyncProgress: OnyxEntry<PolicyConnectionSyncProgress>, isFocused: boolean) {
const modal = useModal();
function useHRSyncResults(connectionSyncProgress: OnyxEntry<PolicyConnectionSyncProgress>, isFocused: boolean) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function useHRSyncResults(connectionSyncProgress: OnyxEntry<PolicyConnectionSyncProgress>, isFocused: boolean) {
function useHRSyncResultsPage(connectionSyncProgress: OnyxEntry<PolicyConnectionSyncProgress>, isFocused: boolean) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — renamed to useHRSyncResultsPage (file and all call sites).

Comment thread src/pages/workspace/hr/DynamicHRSyncResultsPage.tsx
Comment on lines +29 to +30
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use @libs/Navigation import.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — switched to import Navigation from "@libs/Navigation/Navigation".

@parasharrajat

Copy link
Copy Markdown
Member

Update the testing results to show attachment in post itself. Also, test it on all platforms.

@parasharrajat

Copy link
Copy Markdown
Member

Fix Author checklist.

@parasharrajat

Copy link
Copy Markdown
Member

Setting up HR sync for testing.

@trasnake87

Copy link
Copy Markdown
Contributor Author

@parasharrajat all seven comments are addressed — four code changes pushed shortly, and I have replied on every thread including both bot comments (apologies for missing those first time round).

Summary of the fixes:

  • Access guard — you and the Codex bot were right, and this was a real hole the migration introduced. The old modal only ever rendered inside WorkspaceHRPage, so it inherited that page's AccessOrNotFoundWrapper; as a dynamic route the screen is deep-linkable and had no such check. Now wrapped in the same guard.
  • UI-3 — inline transform replaced with the existing styles.flipUpsideDown.
  • Hook renamed to useHRSyncResultsPage; import switched to @libs/Navigation.
  • Sync-progress clearing — I have pushed back on this one in the thread rather than just implementing it, because POLICY_CONNECTION_SYNC_PROGRESS is read by ~25 call sites for "is a connection syncing" (brick-road indicators, etc.), so nulling it on dismiss would have side effects well beyond this screen. I also traced why the screen should not re-open today. Happy to add a scoped guard instead if you still want one — details in the thread.

On the checklist and all-platform testing — I want to be straight with you rather than just ticking boxes. I have verified this on web and on both mWeb variants (screenshots in the description). I have not run the native apps, and the offline and High-Traffic items are first-person attestations I have not personally performed, so I left those unchecked deliberately and said so in the description. I would rather show you an honest checklist than a green one.

If native coverage is required before this can move, tell me and I will get builds running — it is a time cost, not a refusal. And if you would rather see the results rendered inline in the PR body rather than in collapsed sections, I can restructure that too.

The screen is a dynamic route and therefore deep-linkable, so it now carries the
same AccessOrNotFoundWrapper the HR page uses. Previously it inherited nothing:
opening the URL directly bypassed the workspace/HR access checks the old modal
got for free by only ever rendering inside WorkspaceHRPage.

Also swap the inline transform for the existing styles.flipUpsideDown token,
rename the hook to useHRSyncResultsPage, and import Navigation from @libs.
@trasnake87
trasnake87 force-pushed the rbr-pending-delete-96967 branch from 19f3a31 to 217de3f Compare August 1, 2026 14:52
@parasharrajat

Copy link
Copy Markdown
Member

If native coverage is required before this can move, tell me and I will get builds running — it is a time cost, not a refusal. And if you would rather see the results rendered inline in the PR body rather than in collapsed sections, I can restructure that too.

Yes, this needs to be tested on all platforms and please fix the screenshots as well.

}

export default HRSyncResultsModal;
DynamicHRSyncResultsPage.displayName = 'DynamicHRSyncResultsPage';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed.

@trasnake87

Copy link
Copy Markdown
Contributor Author

Not a casing choice — the header string is `${provider} sync complete` (en.ts), so what you're seeing is the empty-provider path rather than a lowercase title.

providerDisplayName comes from getConnectedHRProvider(policy)?.displayName ?? '', which resolves to null unless the policy has an actual HR connection — Gusto, Zenefits, or connections.merge_hris. Enabling the HR feature alone is not enough, so with the feature on but no connection you get a bare " sync complete" in the header and "Successfully synced your connection!" in the body.

Worth being explicit that this is pre-existing and untouched by this PR: those lines are byte-identical to the current HRSyncResultsModal on main

https://github.com/Expensify/App/blob/main/src/components/HRSyncResultsModal.tsx#L44-L45

The migration changed how result is sourced (constructor prop → read from Onyx by policyID) but not how the provider name is derived.

To get the real title while testing, add the connection alongside the sync progress:

Onyx.merge(`policy_${policyID}`, {connections: {merge_hris: {config: {integration: 'bamboohr'}}}});

That renders "BambooHR sync complete". Happy to fix the empty-provider wording here if you would rather not leave it as-is — just say so, since it is outside the migration's scope and I did not want to expand the diff unasked.

@trasnake87

Copy link
Copy Markdown
Contributor Author

@parasharrajat anything blocking on my side? The checklist is complete and CI is green, and I'm happy to adjust the empty-provider wording if you'd like that handled here rather than separately.

@parasharrajat

Copy link
Copy Markdown
Member

Can you please merge main? @trasnake87

@parasharrajat

parasharrajat commented Aug 18, 2026

Copy link
Copy Markdown
Member

Screenshots

🔲 iOS / native

🔲 MacOS / Chrome

19.08.2026_00.20.39_REC.mp4

@parasharrajat

Copy link
Copy Markdown
Member

I didn't see the result page loading on BambooHR. I only have access to this.

So I will run tests with hard-coded values.

# Conflicts:
#	src/pages/workspace/WorkspaceMembersPage.tsx
#	src/pages/workspace/hr/WorkspaceHRPage.tsx
@parasharrajat

parasharrajat commented Aug 19, 2026

Copy link
Copy Markdown
Member

BUG: I noticed that use can go back to the status page but back button on browser. Should this be blocked?

@trasnake87

@trasnake87

Copy link
Copy Markdown
Contributor Author

Good catch, and I checked it properly — the answer is a bit different from the framing.

Browser back doesn't reach the results screen. I couldn't get it to in any sequence I tried: goBack pops the entry, so back lands on whatever preceded Members. What does reach it again is browser forward, after dismissing:

[Got it]   /workspaces/<id>/members
[forward]  /workspaces/<id>/members/hr-sync-results

I tried blocking it with forceReplace: true and it makes things worse, so I reverted it. useDynamicBackPath derives the back target from the current root navigation state:

https://github.com/Expensify/App/blob/main/src/hooks/useDynamicBackPath.ts#L22-L29

forceReplace replaces the entry holding the workspace, so that state is gone by the time the screen renders — the computed backPath comes back empty and Navigation.goBack('') hits the early return and silently no-ops. Measured: "Got it" and the header chevron both go dead and the user is stranded on the results screen. Arriving in-app rather than by deep link, it lands on the Inbox instead of Members. So the direction is right but the mechanism is wrong.

My recommendation is to leave it as-is. Forward-reachability isn't specific to this screen — 291 of the 301 Navigation.navigate(createDynamicRoute(...)) call sites in src/ push rather than replace, and 207 screens use this same useDynamicBackPath mechanism. Blocking re-entry here would make this one migrated modal behave differently from every other one. If it's worth blocking, it seems like a systemic change to the dynamic-route system rather than something this migration should do on its own.

Happy to do it here if you'd prefer — it would need an explicit back target passed to the screen instead of deriving it from navigation state.

@mountiny

Copy link
Copy Markdown
Contributor

@trasnake87 @parasharrajat Lets try to focus on this one to get it over the finish line sooner, thank you

@trasnake87

Copy link
Copy Markdown
Contributor Author

@parasharrajat checking in per Vit's note — everything on my side has been done for a while (checklist complete, CI green apart from the approval gate itself, main merged, the back-button question answered with measurements). Is there anything blocking your checklist testing that I can help with?

@parasharrajat

Copy link
Copy Markdown
Member

I will get this retested tomorrow.

@parasharrajat parasharrajat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • 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).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots

19.08.2026_00.20.39_REC.mp4
25.08.2026_19.47.37_REC.mp4

🎀 👀 🎀 C+ reviewed

@melvin-bot
melvin-bot Bot requested a review from mountiny August 25, 2026 14:20
Comment thread src/pages/workspace/hr/DynamicHRSyncResultsPage.tsx Outdated
Comment thread src/pages/workspace/hr/DynamicHRSyncResultsPage.tsx Outdated
Comment thread src/pages/workspace/hr/DynamicHRSyncResultsPage.tsx Outdated
@mountiny
mountiny requested a review from jmusial August 25, 2026 20:32
@mountiny

Copy link
Copy Markdown
Contributor

Conflicts too

@jmusial can you please review?

@trasnake87

Copy link
Copy Markdown
Contributor Author

@mountiny done — simplified the file header and the access-guard comment to just state the why, dropped the redundant displayName (matches the other Dynamic* pages), and merged main. Ready for another look.

@mountiny
mountiny merged commit 4dcaba9 into Expensify:main Aug 30, 2026
34 of 35 checks passed
@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@github-actions

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.65-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

No help site changes are required for this PR. I did not create a docs PR.

Why

This PR is an internal navigation refactor. It moves the HR sync results UI from a RIGHT_DOCKED global <Modal> to a @react-navigation dynamic modal screen. Nothing a customer sees or does changes:

  • No copy changes. No src/languages/*.ts file is touched. The title, the Added / Removed / Skipped labels, and the confirm button all still render from the same existing workspace.hr.syncResults.* and common.buttonConfirm keys.
  • No layout or entry-point change. It still opens right-docked, still opens automatically when a sync reaches JOB_DONE, and still only from the HR page or the Members list — the same two pages, now declared as entryScreens.
  • No new setting, tab, or button. The nine changed files are routes, screen registration, linking config, types, a hook rename, and the moved component.

Existing docs are still accurate

The HR connection articles already describe this UI, and every claim they make still holds after the merge:

Article Existing wording Still correct?
Connect-Gusto-to-Expensify.md:48-49 "A sync results panel displays employees added, removed, and skipped." / "If you leave the HR or Members tab before the sync finishes…" Yes
TriNet.md:48-49 Same two lines Yes
Connect-BambooHR-to-Expensify.md:133, Connect-HiBob-to-Expensify.md:133, Connect-Workday-to-Expensify.md:133 "The sync results panel will provide specific reasons for each skipped employee." Yes

Importantly, none of these articles calls it a "modal" or a "pop-up" — they all say sync results panel, which the migrated screen still is. So there is no stale terminology to correct.

Two changes I considered and rejected as doc-worthy

1. The screen is now deep-linkable (.../hr-sync-results). Help site articles document what customers click, not internal URL segments — no other workspace settings screen has its route path documented. Adding it would break from the pattern in HELP_AUTHORING_GUIDELINES.md.

2. The screen now gates on HR access via AccessOrNotFoundWrapper (admin + HR feature enabled). This is a security guard on the new deep link, not a permissions change: only workspace admins could reach the HR page or the Members list to trigger a sync in the first place, so no customer loses access they previously had.

Also worth noting: Zenefits is covered by the existing TriNet article (TriNet acquired Zenefits), so there is no missing-provider gap introduced here.

If you would like the sync results section expanded for a different reason — for example, to document the Skipped expand/collapse behavior, which no article currently mentions — reply and I will draft that PR. That gap predates this PR rather than being created by it.

@trasnake87, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review

(No help site PR was created, since no changes are required — so there is nothing to mark ready. Please confirm you agree with the assessment above.)


view run

@OSBotify

OSBotify commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/francoisl in version: 9.4.65-3 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants