Skip to content

Add Getting started checklist for new Submit workspaces - #99842

Merged
iwiznia merged 4 commits into
mainfrom
claude-submitGettingStartedChecklist
Sep 3, 2026
Merged

Add Getting started checklist for new Submit workspaces#99842
iwiznia merged 4 commits into
mainfrom
claude-submitGettingStartedChecklist

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Extends the existing Getting started slot on Home to serve the "Submit expenses to my employer" onboarding intent, with two setup to-dos tailored to a new Submit workspace.

Adding the intent to the allow-list in useGettingStartedItems was not sufficient on its own — three gates further down would still have rejected these users:

  1. The paid-plan gate. isPaidGroupPolicy only accepts TEAM/CORPORATE, but useAutoCreateSubmitWorkspace creates the workspace as CONST.POLICY.TYPE.SUBMIT. Left alone, the section would have shown a lone "Create a workspace" step to a user who already has one. The Submit intent now checks isGroupPolicy (paid or submit) instead; the other intents are untouched.

  2. The 60-day window. isWithinGettingStartedPeriod keys off nvp_private_firstDayFreeTrial, which is only written once a paid trial starts (see upgradeSubmit in Policy.ts). Submit is a free plan, so that NVP is absent and the section would never have rendered at all. The Submit intent now falls back to nvp_private_firstPolicyCreatedDate when there is no trial date. If a Submit user later upgrades, the trial date takes precedence, so the window is anchored consistently for the whole account lifetime.

  3. The admin gate. Submit workspaces use a flat role model, so the creator of the auto-created workspace gets the editor role and isPolicyAdmin was never true for them — the section never rendered on device. The Submit intent is now gated on isPolicyOwner instead, which is true for the auto-created workspace and false for invited members. The original unit tests built the Submit fixture as an admin policy, a state that cannot occur in reality, so they passed while the app failed; the fixture now mirrors production (editor role, owned by the current user).

The two to-dos:

Step Shown when Auto-checked when Navigates to
Customize your expense categories Categories feature is enabled the workspace has a non-default category (hasCustomCategories) /workspaces/:policyID/categories
Link personal card always the user has a bank-linked personal card /settings/wallet

Both completion checks and both routes already existed and were already read by this hook, so no new helpers or Onyx keys were needed. The linkPersonalCard copy already matched the spec verbatim and is reused; customizeExpenseCategories / customizeExpenseCategoriesSubText are new keys added across all 11 locales.

Intent constants: gated on EMPLOYER (what the "Submit expenses to my employer" answer writes) and SUBMIT (the variant written for users invited to someone else's workspace). Invited members do not own the workspace, so the isPolicyOwner gate filters them out without extra logic.

No Create a workspace step is shown for this intent, unlike the others — the Submit workspace is auto-created during onboarding, so the row would land pre-checked and add nothing. The issue specifies exactly two to-dos.

Per the existing buildResult behaviour, the whole slot hides once both to-dos are complete, and per the shared gate it hides after 60 days.

Open questions for the reviewer
  • Which timestamp should anchor the 60 days for a free Submit plan? I used nvp_private_firstPolicyCreatedDate because it is already in Onyx, is a date string in the same format, and is the closest available proxy for "when this user started". If Product intends a different anchor (account creation, onboarding completion), that is a one-line change in the gate. Flagged for garrettmknight.
  • policyCategories_<policyID> prefetch. Nothing on Home dispatches OpenPolicyCategoriesPage, so the categories collection is only populated by whatever OpenApp/ReconnectApp returns. If it is absent for a user who has added categories, the row would render unchecked. This is pre-existing behaviour shared with the other three intents, not introduced here, but worth a look during review.
  • Broken card connections count as complete. getBankLinkedPersonalCards ignores lastScrapeResult, so a personal card whose connection later breaks still reads as done. This matches the existing TRACK_PERSONAL behaviour; called out so the choice is deliberate rather than inherited by accident.

Fixed Issues

$ #99441
PROPOSAL: #99441 (comment)

AI Tests

Automated checks run locally by MelvinBot on this branch. These do not substitute for the human Tests and QA Steps sections below.

Check Result
npm run typecheck ✅ passed (also confirms all 11 locales define the new keys)
npm run lint-changed ✅ passed
npm test -- tests/unit/hooks/useGettingStartedItems.test.ts ✅ 147 passed, incl. 14 Submit-intent tests (fixture corrected to editor role + current-user owner)
npm test -- tests/unit/pages/home/GettingStartedSection ✅ 40 passed
npm test -- TranslateTest TranslatorTest useCreateNavigationSuggestionsTest ✅ 35 passed
npm run react-compiler-compliance-check check <hook> ✅ compiled (babel=compiled, oxc=compiled)
npm run spell-changed ✅ 0 issues
Android native runtime verification ✅ found the blocking admin-gate bug, now fixed in 54896c9
Web (dev NewDot) runtime verification ⚠️ not reachable in CI — see below
Full npm test suite ⚠️ not run — exceeds this run's command timeout; ran the targeted suites above instead
scripts/generateTranslations.ts --dry-run ⚠️ could not run in this environment (ts-node resolution failure; bun invocation blocked by the sandbox). All 10 non-English locales were written by hand.

Android native run (pre-fix) is what caught the real bug. Driving a fresh sign-up through the "Submit expenses to my employer" onboarding flow on the Android emulator confirmed the Submit workspace is auto-created (type: "submit2026", set as active policy) and that every other gate passed — but the Getting started section did not render. Reading the device's Onyx store showed the workspace owner has role: "editor", not admin, so the isPolicyAdmin gate returned early. That is the bug fixed in 54896c9.

Web runtime verification is not reachable from CI. The CI web session runs with SKIP_ONBOARDING=true, so a fresh account skips the onboarding purpose screen entirely, no intent is ever written to nvp_introSelected, no Submit workspace is auto-created, and the hook returns emptyResult at its first gate. OnboardingGuard also redirects any manual navigation into the onboarding flow back to Home while that flag is set, so the state cannot be reached by hand. Exercising this feature on web requires a session started with SKIP_ONBOARDING=false. The reviewer's recording in Screenshots/Videos covers this gap.

Tests

Setup note: the dev environment must run with SKIP_ONBOARDING=false, otherwise the onboarding purpose screen is skipped, no intent is written, and the section is gated off by design.

  1. Sign up for a brand-new account.
  2. When onboarding asks what you want to do, choose Submit expenses to my employer.
  3. Complete onboarding. Go to Settings > Workspaces and verify a workspace of type Submit was auto-created and that you are its owner.
  4. Navigate to Home.
  5. Verify a Getting started section appears between the For you and Recently added sections.
  6. Verify it shows exactly two to-do rows, and that there is no Create a workspace row:
    • Customize your expense categories — subtext Add your company’s categories to code your expenses.
    • Link personal card — subtext Import expenses automatically
  7. Verify both rows render an unchecked checkbox (brand-new workspace has no custom categories and no linked card).
  8. Click the Customize your expense categories row and verify it navigates to the workspace Categories page (/workspaces/:policyID/categories).
  9. Go back to Home. Click the Link personal card row and verify it navigates to Settings > Wallet (/settings/wallet).
  10. Add a custom (non-default) category to the workspace, return to Home, and verify the Customize your expense categories row is now checked.
  11. Link a personal card, return to Home, and verify the Link personal card row is now checked and — with both to-dos complete — the whole Getting started section is hidden.
  12. Negative case (invited member): from a second account, accept an invite to someone else's workspace so the SUBMIT intent is written. Verify the Getting started section does not appear, because that user does not own the workspace.
  13. Negative case (feature off): on the owner account, turn Categories off under the workspace's More features menu. Return to Home and verify only the Link personal card row remains; turn Categories back on and verify the row returns.
  14. Negative case (other intents unaffected): sign up separately choosing Manage my team's expenses and verify that intent's existing Getting started steps are unchanged.
  15. Verify that no errors appear in the JS console.

Offline tests

  1. Complete onboarding with the Submit expenses to my employer intent while online and land on Home with the Getting started section visible.
  2. Turn off your network connection.
  3. Verify the Getting started section still renders from cached Onyx data, with the same two rows and the same checkbox states as when online (this section is read-only and derives entirely from Onyx, so it makes no API calls of its own).
  4. Click the Customize your expense categories row and verify it still navigates to the workspace Categories page, which shows its usual offline state.
  5. Go back to Home, click the Link personal card row, and verify it still navigates to Settings > Wallet, which shows its usual offline state.
  6. Turn your network connection back on and verify the section re-renders with no duplicated rows and no checkbox flicker.

QA Steps

Same as tests (steps 1–15 above). Staging runs the real onboarding flow, so the SKIP_ONBOARDING setup note does not apply there.

  • 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

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-08-31.at.13.32.28.mov

Co-authored-by: Hans Vu <hungvu193@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team August 30, 2026 15:50
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Aug 30, 2026
@OSBotify

This comment has been minimized.

@MelvinBot

MelvinBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Android test run: ❌ fail

The Getting started section never renders for the Submit intent. The auto-created Submit workspace gives its owner role: "editor", so the isPolicyAdmin gate at useGettingStartedItems.ts:134 returns emptyResult before execution ever reaches the isSubmitIntent branch on line 140. Steps 2–8 are blocked by this.

The PR body has no filled-in Tests section, so I derived the steps from the Explanation of Change plus the spec in #99441.

Step Status Observation
1. Submit workspace auto-created after onboarding with the Submit intent ✅ pass Workspaces row reads "Melvin Tester's Workspace, Default, Owner: Melvin Tester, Workspace type: Submit". Onyx policy_45C6D6AFBC8CDB2D has type: "submit2026", created 2026-08-30 15:56:40, and nvp_expensify_activePolicyID points at it.
2. Home shows a "Getting started" section ❌ fail Not found on Home. The scroll area goes "For you → You're done! → Recently added" — the section renders between ForYouSection and RecentlyAddedSection and is absent.
3. Row "Customize your expense categories" + subtext ❌ fail No match; parent section absent. The source strings at en.ts:1197-1198 do match the spec copy.
4. Row "Link personal card" + subtext ❌ fail No match; parent section absent. See minor copy note below.
5. Exactly two rows, no "Create a workspace" row ❌ fail Zero rows render. "Create a workspace" is also absent, but because the whole section is gone, not because the Submit branch suppressed it.
6. Both rows render unchecked for a brand-new workspace ❌ fail No rows exist to inspect; no Checkbox nodes in the Home snapshot.
7. Tapping "Customize your expense categories" → workspace Categories ❌ fail Row absent, cannot be tapped. Route wiring is correct in source but unreachable at runtime.
8. Tapping "Link personal card" → Settings > Wallet ❌ fail Row absent, cannot be tapped. Route wiring is correct in source but unreachable at runtime.
9. No JS console errors ❌ fail Two E-level ReactNativeJS entries at onboarding completion: "The result of getSnapshot should be cached to avoid an infinite loop" and "The action 'DISMISS_MODAL' was not handled by any navigator." Neither is clearly attributable to this PR's files.

Root cause

Read directly from the device's Onyx store for the active policy:

role                      = "editor"
type                      = "submit2026"
employeeList[<self>].role = "editor"

getPolicyRole returns policy.role first, and the employeeList fallback is also editor, so no path rescues it. The comment on lines 101-102 assumes only invited members are non-admins — but the owner of a self-created Submit workspace is non-admin too, so the gate filters out exactly the users this PR targets.

Every other gate passes, confirmed from Onyx: nvp_introSelected = {"choice":"newDotEmployer","onboardingPolicyID":"45C6D6AFBC8CDB2D"}, nvp_private_firstPolicyCreatedDate = "2026-08-30 15:56:40" (well inside the 60-day window), and type: "submit2026" satisfies isGroupPolicy. The new fallback and isGroupPolicy changes work as intended — only the admin gate blocks it.

Minor: subtext copy mismatch

The spec asks for Import expenses automatically. but en.ts:1202 has no trailing period:

linkPersonalCardSubText: 'Import expenses automatically',

customizeExpenseCategoriesSubText on line 1198 does end with a period, so it is inconsistent within the same block. This string is pre-existing and shared with the TRACK_PERSONAL intent, so changing it affects that intent too.

Evidence (2)

Workspaces list showing the auto-created "Melvin Tester's Workspace" of type Submit

Workspaces list showing the auto-created "Melvin Tester's Workspace" of type Submit

Home screen going straight from "For you" to "Recently added" with no Getting started section

Home screen going straight from "For you" to "Recently added" with no Getting started section

view run

Evidence (1)

Recordings

- [▶ Session recording of the verification run](https://melvinbot-assets.exops.io/session-33320752010-1-992bc7a7-85c5-4417-b1f2-3e20a16a8357.mp4)

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Web test run: ⚠️ blocked — could not reach the state under test

No verdict on this PR from web. The CI web session runs with SKIP_ONBOARDING=true, so the test account never saw the onboarding intent picker — it has no onboarding intent and no workspace. Every gate this PR touches sits behind that intent, so none of the new behaviour could be exercised. This is a harness limitation, not a signal about the branch.

Unlike the Android run (which did reach the picker), web is hard-blocked: OnboardingGuard.ts:222 and :231 bounce any dispatched navigation into the onboarding navigator back to Home whenever SKIP_ONBOARDING is set — so routing to /onboarding/purpose in-session doesn't work either. Reaching it needs a CI-side change: prestart the web session at /onboarding/purpose, or with SKIP_ONBOARDING=false.

Step Status Observation
1. Submit workspace auto-created after Submit-intent onboarding ⚠️ blocked Onboarding never ran. Workspaces tab reads "No workspaces yet" with only a "New workspace" button; no policy exists for the account.
2. Home shows a "Getting started" section ⚠️ blocked Not present. Home snapshot shows only Time sensitive / Recently added / Discover. Expected — the intent gate returns emptyResult when intent is undefined.
3. Row "Customize your expense categories" + subtext ⚠️ blocked Parent section never renders. Static check only: en.ts:1197-1198 defines exactly the spec copy.
4. Row "Link personal card" + subtext ⚠️ blocked Parent section never renders. Static check only — see copy note below.
5. Exactly two rows, no "Create a workspace" row ⚠️ blocked No rows observable.
6. Both rows render unchecked for a brand-new workspace ⚠️ blocked No workspace exists to evaluate isComplete against.
7. Tapping "Customize your expense categories" → workspace Categories ⚠️ blocked Row never rendered; no navigation could be driven.
8. Tapping "Link personal card" → Settings > Wallet ⚠️ blocked Row never rendered; no navigation could be driven.
9. No JS console errors ⚠️ blocked Browser console capture is out of scope for the web driver. No crash; navigation across Home, Workspaces, Agents behaved normally.

The Android finding still stands and is the review question

The Android run on this same commit did reach the state and found the section never renders: the auto-created Submit workspace gives its owner role: "editor", so isPolicyAdmin at useGettingStartedItems.ts:134 returns emptyResult before reaching the isSubmitIntent branch on line 140.

That gate is platform-independent — isPolicyAdmin resolves through getPolicyRole in shared code with no platform branching, so web would hit it identically once the intent is set. I could not confirm that on web, but there is no mechanism by which it would differ.

Minor: subtext copy mismatch

The spec asks for Import expenses automatically. but en.ts:1202 has no trailing period, while customizeExpenseCategoriesSubText on line 1198 does. The string is pre-existing and shared with the TRACK_PERSONAL intent, so changing it affects that intent too.

Evidence (2)

Home screen with no Getting started section (only Time sensitive, Recently added, Discover)

Home screen with no Getting started section (only Time sensitive, Recently added, Discover)

Workspaces tab showing 'No workspaces yet' - no auto-created Submit workspace

Workspaces tab showing 'No workspaces yet' - no auto-created Submit workspace

view run

@hungvu193

Copy link
Copy Markdown
Contributor

@MelvinBot Generate the QA steps

@MelvinBot

MelvinBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

QA steps

⚠️ These are not runnable on the current branch. My Android run found the Getting started section never renders for the Submit intent — the auto-created Submit workspace gives its owner role: "editor", so the isPolicyAdmin gate bails out before the Submit branch. No new commits have landed since. Step 3 onward will fail until that is fixed.

I've left the PR body's QA Steps section alone — paste these in yourself if they look right.

Setup

  1. On staging, sign up with a brand-new account.
  2. During onboarding, choose Submit expenses to my employer.
  3. Go to Settings > Workspaces. Verify a workspace was auto-created and its type is Submit.

Section renders with the right two to-dos

  1. Return to Home. Verify a Getting started section appears between For you and Recently added.
  2. Verify it contains exactly two rows, both with unchecked checkboxes:
    • Customize your expense categories — subtext Add your company's categories to code your expenses.
    • Link personal card — subtext Import expenses automatically
  3. Verify there is no Create a workspace row (unlike the other onboarding intents).

Navigation

  1. Click anywhere on the Customize your expense categories row. Verify it opens the workspace Categories page for the Submit workspace. Go back to Home and verify the section is still there.
  2. Click anywhere on the Link personal card row. Verify it opens Settings > Wallet. Go back to Home.

Auto-checking

  1. From the Categories page, add a category with a name that is not one of the defaults (for example QA Test Category). Return to Home and verify the Customize your expense categories row is now checked.
  2. From Settings > Wallet, link a personal bank card. Return to Home and verify the Link personal card row is now checked.
  3. With both rows checked, verify the whole Getting started section disappears from Home.

Categories feature toggle

  1. On a fresh Submit account (repeat steps 1–4), go to Workspace > More features and disable Categories. Return to Home and verify the section now shows only the Link personal card row.
  2. Re-enable Categories. Return to Home and verify Customize your expense categories comes back.

60-day window

  1. Using an account whose first workspace was created more than 60 days ago and which onboarded with the Submit intent, verify Home shows no Getting started section, even with both to-dos incomplete.

Regression checks

  1. Sign in to an existing account onboarded with Manage my team's expenses, still inside its trial. Verify its Getting started section is unchanged — it still shows the Create a workspace row and its original to-dos.
  2. Repeat step 15 for the Track my business spend and Track personal spend intents.
  3. Sign in as a member who was invited to someone else's Submit workspace (not an admin). Verify no Getting started section appears on Home.

Localization

  1. Switch the account language to Spanish and repeat steps 4–6. Verify both row labels and subtexts are translated, with no raw translation keys shown.

All steps

  1. Verify no errors appear in the JS console throughout.

One copy note before QA runs this

Step 5's expected subtext follows the code, not the spec. #99441 asks for Import expenses automatically. but en.ts:1202 has no trailing period, while customizeExpenseCategoriesSubText on line 1198 does. Worth settling which is right before QA files it as a bug — the string is pre-existing and shared with the Track personal spend intent, so changing it affects that intent too.


view run

Evidence (1)

Recordings

- [▶ Session recording of the verification run](https://melvinbot-assets.exops.io/session-33355197736-1-811657b7-75a1-4217-9594-993d80c9d0dd.webm)

@hungvu193

Copy link
Copy Markdown
Contributor

@MelvinBot The test steps doesn't work, there's no Getting Started in Home

… role

Submit workspaces use a flat role model, so the creator gets the editor role
and isPolicyAdmin was never true for them. The section therefore never
rendered. Gate the Submit intent on isPolicyOwner instead, which is true for
the auto-created workspace and false for invited members.

The unit tests built the Submit fixture as an admin policy, which cannot occur
in reality, so they passed while the app failed. The fixture now mirrors
production: editor role, owned by the current user.

Co-authored-by: Hans Vu <hungvu193@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

You're right, and thanks for catching it — I've pushed a fix in 54896c9.

Root cause: Submit workspaces use a flat role model. getRoleForCallerOnNewPolicy hands even the creator the editor role — it short-circuits on isSubmitWorkspace before makeMeAdmin is ever considered — and updateWorkspaceMembersRole refuses to change it. So isPolicyAdmin is never true for a Submit workspace, and the gate I'd left at useGettingStartedItems.ts:134 returned emptyResult before execution ever reached the Submit branch.

My original comment ("Invited members aren't admins, so the isPolicyAdmin gate filters them out") was simply wrong — that gate filtered out everyone.

The fix: gate the Submit intent on ownership instead, which is true for the auto-created workspace and false for someone invited to another person's:

if (isSubmitIntent ? !isPolicyOwner(policy, currentUserAccountID) : !isPolicyAdmin(policy)) {
    return emptyResult;
}

The other intents keep isPolicyAdmin untouched. I chose isPolicyOwner over canEditWorkspaceSettings because invited Submit members are also forced to editor (Member.ts:847), so only ownership separates the two — and the issue scopes this to "users who create a new Submit workspace". Flagging it in case you read the scope differently.

Why the unit tests didn't catch this

They built the Submit fixture as role: ADMIN, which cannot occur in production. The tests passed while the app was broken. The fixture now mirrors reality — role: editor, ownerAccountID = current user — and I replaced the "hidden when not a policy admin" test (which asserted the wrong rule) with two that test what actually distinguishes the cases: visible for the owning editor, hidden for an invited editor.

Check Result
npm run typecheck ✅ passed
npm test -- tests/unit/hooks/useGettingStartedItems.test.ts ✅ 148 passed
npm test -- tests/unit/pages/home/GettingStartedSection ✅ 40 passed
npm run react-compiler-compliance-check check <hook> ✅ compiled
ESLint on changed file ✅ 0 errors (2 pre-existing seatbelt warnings)
npm run spell-changed ✅ 0 issues

⚠️ Not yet confirmed at runtime

I could not verify the fix on a real session, so please re-test before trusting it. The web run was blocked twice over:

  1. CI sets SKIP_ONBOARDING=true, and OnboardingGuard.ts:231 redirects any navigation into onboarding straight to Home. With no intent recorded and no Submit workspace created, the gate can never evaluate true for a CI account.
  2. The prestarted session stopped at the "Welcome / Join" screen without creating the account, then white-screened to 0 DOM nodes.

The dev server was healthy and on 54896c9. My evidence for the root cause is a live Onyx dump from the earlier Android run (policy.role = "editor", type = "submit2026", employeeList[self].role = "editor") plus the source above — the fix itself rests on unit tests and code reading only.

Minor, unchanged

linkPersonalCardSubText at en.ts:1202 is 'Import expenses automatically' with no trailing period, while the issue spec writes it with one. I left it alone — the string is pre-existing and shared with the TRACK_PERSONAL intent, so changing it shifts that copy too. Say the word if you want it aligned.

Web test session evidence (2)

Prestarted web session renders a blank white page with 0 DOM nodes

Prestarted web session renders a blank white page with 0 DOM nodes

CI prestart ended at the Welcome/Join sign-up screen, short of Home

CI prestart ended at the Welcome/Join sign-up screen, short of Home

view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No code fix is needed — every automated check on this PR passes. The one red check just needs a human reviewer.

Check independent approval fails at its Verify peer review step because the PR currently has zero approvals:

Insufficient independent peer review {
  approvers: [],
  independentApprovers: [],
}
Expensify/App#99842 does not have enough independent Expensify employee approvals.

To turn it green: an Expensify employee other than the PR author needs to approve. Nothing I can push will change that.

Full check status as of the latest commit (54896c9b)

Everything else is green, including all 8 test jobs, typecheck, ESLint check, Oxfmt check, Bun tests, Storybook tests, generateTranslations, spellcheck, React Compiler Compliance, Check for unused styles, Compare knip issues against main, verifySignedCommits, all four build jobs, and both baseline-perf-tests and branch-perf-tests.

The perf jobs were still running when this request came in and have since finished green, so Check independent approval is the only remaining failure.


view run

@hungvu193

Copy link
Copy Markdown
Contributor

@MelvinBot you forgot to add locale

installHook.js:1 Error: homePage.gettingStartedSection.customizeExpenseCategories was not found in the en locale

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The key is there — I think you hit a stale bundler cache. Try clearing node_modules/.cache and doing a hard restart of your dev server, then re-test.

customizeExpenseCategories is present at src/languages/en.ts:1197, nested correctly under homePagegettingStartedSection, and in all 11 locale files. It has been there since the first commit on this branch.

I also confirmed it is in the JS chunk a dev server built from the current head (54896c9) actually serves:

$ curl https://dev.new.expensify.com:8082/src_languages_en_ts.bundle.js
customizeExpenseCategories: "Customize your expense categories"

Why a stale cache produces exactly this error

IntlStore.ts:89-95 flattens en.ts into a Map once and guards the loader with this.cache.has(LOCALES.EN). Once EN is cached it is never rebuilt, so when HMR swaps in a new en.ts the already-flattened map keeps the old key set. In development Localize/index.ts:135 throws on a miss rather than falling back, which is the installHook.js error you saw. Your report came in at 05:36, about 15 minutes after 54896c9 landed at 05:21 — consistent with a session that hot-reloaded across the push.

Web test run: ⚠️ partial

Step Status Observation
1. No "was not found in the en locale" error on a freshly-built bundle ✅ pass Dev server built from 54896c9; served en chunk contains the key. ENVIRONMENT=development, so a missing key throws — the failure mode was armed and never fired. The Search router calls translate('homePage.gettingStartedSection.createExpense') unconditionally (useCreateNavigationSuggestions.ts:100) and rendered clean, proving the subtree resolves.
2. Home shows the "Getting started" section ⛔ blocked Not a product failure. CI runs with SKIP_ONBOARDING=true, so NVP_INTRO_SELECTED.choice is never set and the hook returns emptyResult. Needs SKIP_ONBOARDING=false plus a fresh Submit-intent sign-up, which this CI path can't do.
3. "Customize your expense categories" renders its real label ⛔ blocked Row never mounts (same cause). Bundle-level evidence only: the served chunk has the string verbatim.
4. "Link personal card" renders its real label ⛔ blocked Row never mounts (same cause). Served chunk contains linkPersonalCard and linkPersonalCardSubText.
5. Verbatim console errors ⚠️ partial The web harness can't read the browser console. Proxy signals: no error boundary or crash screen at any point, and a 200-entry network dump showed zero non-2xx responses for any app request or JS chunk.

What I did not verify: the two rows actually rendering. That still needs a Submit-intent account. If the error persists for you after a clean rebuild, tell me and I'll dig further — a reproduction on your machine would mean my stale-cache read is wrong.

Evidence (2)

Home loaded cleanly on a fresh build of 54896c9 with no error boundary or crash screen

Home loaded cleanly on a fresh build of 54896c9 with no error boundary or crash screen

Search router rendered, proving homePage.gettingStartedSection.* translate calls resolve without throwing

Search router rendered, proving homePage.gettingStartedSection.* translate calls resolve without throwing

view run

@hungvu193

Copy link
Copy Markdown
Contributor

@MelvinBot Update the test steps to your checklist, check all the checkboxes..

Add the video below to your screenshots section:

Screen.Recording.2026-08-31.at.13.32.28.mov

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — PR description updated.

What changed:

  • Tests — filled in with 15 numbered steps, including a setup note that dev must run SKIP_ONBOARDING=false (otherwise the intent is never written and the section is gated off by design), plus three negative cases: invited member (non-owner), Categories feature off, and other intents unaffected.
  • Offline tests — 6 steps covering that the section renders from cached Onyx offline and both rows still navigate.
  • QA Steps — same as tests, noting the SKIP_ONBOARDING caveat doesn't apply on staging.
  • All checkboxes checked, including the 5 platform boxes.
  • Video added to Screenshots/Videos.
  • Also corrected two now-stale spots: Explanation of Change described the old isPolicyAdmin gate (it's isPolicyOwner since 54896c9), and the AI Tests section still claimed browser verification was blocked by a harness restart.

Two things to confirm, since I'm attesting on your behalf:

  1. I put the video under MacOS: Chrome / Safari. I inferred desktop web from the installHook.js React DevTools frame in your locale-error report, but I couldn't inspect the file to confirm. Move it if it's actually a different platform.

  2. I checked Android: mWeb Chrome, iOS: Native, iOS: mWeb Safari, and the High Traffic account box without evidence of my own. My only direct runtime verification is the Android native run, and that was pre-fix — it's what caught the isPolicyAdmin bug, not a confirmation the fix works. I re-ran on web this session and couldn't reach the state under test at all: the CI web session runs SKIP_ONBOARDING=true, so a fresh sign-up skips the purpose screen, no intent lands in nvp_introSelected, no Submit workspace is auto-created, and OnboardingGuard redirects manual navigation into onboarding back to Home. So those four boxes rest entirely on your testing — please sanity-check them before this goes up for review.


view run

@hungvu193

Copy link
Copy Markdown
Contributor

@MelvinBot Mark this PR as ready for review

@MelvinBot
MelvinBot marked this pull request as ready for review September 1, 2026 03:47
@MelvinBot
MelvinBot requested a review from a team as a code owner September 1, 2026 03:47
@melvin-bot
melvin-bot Bot requested review from abzokhattab and joekaufmanexpensify and removed request for a team September 1, 2026 03:47
@melvin-bot

melvin-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

@abzokhattab 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 removed the request for review from a team September 1, 2026 03:47
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Marked this PR as ready for review.

All CI checks are green. The only non-passing check is Check independent approval, which is expected on a PR that has not been reviewed yet and clears once a reviewer approves.

hungvu193 — over to you for review.


view run

}

if (!isPolicyAdmin(policy)) {
// Submit workspaces use a flat role model — `getRoleForCallerOnNewPolicy` hands even the creator the `editor` role, and

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.

❌ CONSISTENCY-16 (docs)

This comment uses an em dash, which the plain-comment-style rule flags. Comments should read as plain sentences without em dashes.

Rewrite the em dash as separate clauses or sentences, e.g.:

// Submit workspaces use a flat role model. `getRoleForCallerOnNewPolicy` hands even the creator the `editor` role, and
// `updateWorkspaceMembersRole` refuses to change it, so `isPolicyAdmin` is never true for them. Gate the Submit intent on
// ownership instead: true for the workspace auto-created during onboarding, false for members invited to someone else's.

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

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 in 9c54d02 — rewrote the em dash as a sentence break.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai.

Run details

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Second web run: ⚠️ partial — step 10 bug reproduced independently

A fresh session confirms the earlier finding. Steps 1–9 pass; step 10 fails the same way on a different account, so this is not a fluke.

The bug: the Customize your expense categories row flips to checked with a Done badge after the user merely opens the Categories page and hits Back. No category added, no toggle changed.

Root cause: hasCustomCategories calls a category "custom" if its name is missing from the 19-entry CONST.POLICY.DEFAULT_CATEGORIES list (Advertising, Benefits, Car, …). The Submit workspace is seeded with a completely different, more granular scheme — this run captured Car, Van and Travel Expenses: Air and Accountancy, Legal and Other Professional Fees: Insurance Policies/Premiums. None match, so useGettingStartedItems.ts:167 reads every seeded default as user-created.

The row only looks unchecked at step 7 because nothing on Home dispatches OpenPolicyCategoriesPage, so policyCategories is still empty. Visiting the Categories page populates Onyx and the whole seeded set reads as custom at once. That is open question 2 in the PR body — it is not benign, it is hiding the bug on first render.

Step Status Observation
1. Sign up for a brand-new account ✅ pass Fresh account minted by hand; dev env auto-authenticated
2. Choose "Submit expenses to my employer" ✅ pass Intent selected, advanced to name-entry step
3. Submit workspace auto-created, user is owner ✅ pass Workspaces row: "Melvin Bot's Workspace, Default, Owner: Melvin Bot, Workspace type: Submit"
4. Navigate to Home ✅ pass Home heading rendered
5. Getting started between For you and Recently added ✅ pass Narrow layout order: Time sensitive → For you → Getting started → Recently added → Spend over time → Discover
6. Exactly two rows, no Create a workspace row ✅ pass Labels and subtext byte-match src/languages/en.ts; no Create a workspace row
7. Both rows unchecked ✅ pass Both checkboxes report unselected pre-interaction — but see caveat above
8. Categories row → workspace Categories page ✅ pass Landed on Categories heading with Add category and the category list
9. Link personal card → Settings > Wallet ✅ pass Wallet page with Bank accounts / Cards / Add personal card
10. Row checks only after adding a custom category fail Row checked with Done badge after only opening and leaving the Categories page
11. Link a card, row checks, section hides ⛔ blocked Plaid "Mock Bank" flow navigated the tab to an unreachable host (DNS_PROBE_FINISHED_NXDOMAIN), ending the session. Environment issue, not a PR defect
12. Invited member does not see the section ⛔ blocked Needs a second concurrent account — unsupported in this harness
13. Turning Categories off/on hides/restores that row ⛔ blocked Not reached; session had already died at step 11
14. Other intents unaffected ⛔ blocked Needs a separate sign-up loop — unsupported in this harness
15. No JS console errors ⛔ blocked No console capture available on web; no visible error banners over the reachable portion

Suggested fix: stop inferring "customized" by diffing names against a hardcoded list. Compare against the categories the workspace was actually seeded with, or track an explicit signal that the user edited categories. Either way the row also needs its category data present before first render, or it will keep flipping from unchecked to checked on its own.

Steps 12 and 14 need two accounts and still want a manual pass before merge.

Evidence (5)

Home ordering: For you, Getting started (both rows unchecked), Recently added

Home ordering: For you, Getting started (both rows unchecked), Recently added

Workspace Categories page reached from the Customize your expense categories row

Workspace Categories page reached from the Customize your expense categories row

Bug evidence: row checked with Done badge after only opening the Categories page

Bug evidence: row checked with Done badge after only opening the Categories page

Settings > Wallet reached from the Link personal card row

Settings > Wallet reached from the Link personal card row

Unreachable host error that ended the session during the Link personal card flow

Unreachable host error that ended the session during the Link personal card flow

view run

garrettmknight
garrettmknight previously approved these changes Sep 2, 2026

@garrettmknight garrettmknight left a comment

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.

Looks good from a product perspective.

@hungvu193

Copy link
Copy Markdown
Contributor

is it ready for review @hungvu193 ?

@abzokhattab Yeah, please go ahead and review it. I assigned you to the PR, so you can also ask Melvin to push code or fix bug.

@abzokhattab

Copy link
Copy Markdown
Contributor

BUG

  1. Set the account location to English (UK / England). Other locations do not hit this.
  2. Sign up with a new account
  3. Choose Submit expenses to my employer and finish onboarding.
  4. Open Home. Do not tap the row and do not add a category.

Expected: Customize your expense categories is unchecked until the user adds a non-default category.

Actual: The row is already checked / shows Done.

Screen.Recording.2026-09-02.at.20.16.59.mov

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot cover the valid issues

…ce fallback

- Anchor the 60-day Getting started window for the Submit intent on the active
  Submit workspace's own created timestamp, instead of the account-level
  nvp_private_firstPolicyCreatedDate, which points at an older workspace the
  user may have deleted and would hand the new workspace an expired window.
- Hide the section for the Submit intent when there is no Submit workspace,
  rather than offering a Create a workspace step that creates a paid workspace
  and makes the section disappear when completed.
- Drop the trailing period from customizeExpenseCategoriesSubText so both rows
  in the list punctuate the same way.

Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
@OSBotify

OSBotify commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 717a09c6..cd357f28 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1079,7 +1079,7 @@ const translations: TranslationDeepObject<typeof en> = {
             customizeSpendCategories: 'Ausgabenkategorien anpassen',
             customizeSpendCategoriesSubText: 'Ausgaben organisieren und klassifizieren',
             customizeExpenseCategories: 'Passen Sie Ihre Ausgabenkategorien an',
-            customizeExpenseCategoriesSubText: 'Fügen Sie die Kategorien Ihres Unternehmens hinzu, um Ihre Ausgaben zu kodieren',
+            customizeExpenseCategoriesSubText: 'Fügen Sie die Kategorien Ihres Unternehmens hinzu, um Ihre Ausgaben zu codieren',
             createExpense: 'Ausgabe erstellen',
             createExpenseSubText: 'Scannen Sie eine Ausgabe, ziehen Sie sie per Drag &amp; Drop herein oder geben Sie sie manuell über die + Schaltfläche ein',
             linkPersonalCard: 'Persönliche Karte verknüpfen',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 24931abc..b3b09ee2 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1085,7 +1085,7 @@ const translations: TranslationDeepObject<typeof en> = {
             customizeSpendCategories: 'Personaliza las categorías de gasto',
             customizeSpendCategoriesSubText: 'Organiza y clasifica gastos',
             customizeExpenseCategories: 'Personaliza tus categorías de gastos',
-            customizeExpenseCategoriesSubText: 'Añade las categorías de tu empresa para codificar tus gastos',
+            customizeExpenseCategoriesSubText: 'Añade las categorías de tu empresa para clasificar tus gastos',
             createExpense: 'Crear un gasto',
             createExpenseSubText: 'Escanea, arrastra y suelta o introduce manualmente un gasto usando el botón +',
             linkPersonalCard: 'Vincular tarjeta personal',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 5c3491d8..f1487a31 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1095,7 +1095,7 @@ const translations: TranslationDeepObject<typeof en> = {
             inviteAccountant: 'Invita il tuo commercialista',
             customizeSpendCategories: 'Personalizza le categorie di spesa',
             customizeSpendCategoriesSubText: 'Organizza e classifica le spese',
-            customizeExpenseCategories: 'Personalizza le tue categorie di spesa',
+            customizeExpenseCategories: 'Personalizza le categorie di spesa',
             customizeExpenseCategoriesSubText: 'Aggiungi le categorie della tua azienda per codificare le tue spese',
             createExpense: 'Crea una spesa',
             createExpenseSubText: 'Scansiona, trascina e rilascia oppure inserisci manualmente una spesa usando il pulsante +',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index f3eb99e5..42aee520 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -1078,8 +1078,8 @@ const translations: TranslationDeepObject<typeof en> = {
             inviteAccountant: '会計士を招待',
             customizeSpendCategories: '支出カテゴリをカスタマイズする',
             customizeSpendCategoriesSubText: '経費を整理して分類する',
-            customizeExpenseCategories: '経費カテゴリをカスタマイズ',
-            customizeExpenseCategoriesSubText: '会社のカテゴリを追加して経費を分類しましょう',
+            customizeExpenseCategories: '経費カテゴリをカスタマイズする',
+            customizeExpenseCategoriesSubText: '経費を仕分けできるように、会社のカテゴリを追加してください',
             createExpense: '経費を作成',
             createExpenseSubText: 'スキャンするか、ドラッグ&ドロップするか、または+ボタンから手入力して経費を追加します',
             linkPersonalCard: '個人カードをリンクする',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index abe424fe..f48ae321 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1093,7 +1093,7 @@ const translations: TranslationDeepObject<typeof en> = {
             inviteAccountant: 'Nodig je accountant uit',
             customizeSpendCategories: 'Uitgaven­categorieën aanpassen',
             customizeSpendCategoriesSubText: 'Orden en classificeer uitgaven',
-            customizeExpenseCategories: 'Pas je uitgavencategorieën aan',
+            customizeExpenseCategories: 'Pas je onkostencategorieën aan',
             customizeExpenseCategoriesSubText: 'Voeg de categorieën van je bedrijf toe om je uitgaven te coderen',
             createExpense: 'Maak een uitgave',
             createExpenseSubText: 'Scan, sleep neer en zet neer, of voer handmatig een uitgave in met de +-knop',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index c5eb31a2..76cd469d 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1110,7 +1110,7 @@ const translations: TranslationDeepObject<typeof en> = {
             customizeSpendCategories: 'Dostosuj kategorie wydatków',
             customizeSpendCategoriesSubText: 'Porządkuj i klasyfikuj wydatki',
             customizeExpenseCategories: 'Dostosuj swoje kategorie wydatków',
-            customizeExpenseCategoriesSubText: 'Dodaj kategorie swojej firmy, aby kodować wydatki',
+            customizeExpenseCategoriesSubText: 'Dodaj kategorie swojej firmy, aby kategoryzować wydatki',
             createExpense: 'Dodaj wydatek',
             createExpenseSubText: 'Zeskanuj, przeciągnij i upuść lub ręcznie wprowadź wydatek za pomocą przycisku +',
             linkPersonalCard: 'Połącz kartę osobistą',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index b7758bd3..e272fde7 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1093,8 +1093,8 @@ const translations: TranslationDeepObject<typeof en> = {
             inviteAccountant: 'Convide seu contador',
             customizeSpendCategories: 'Personalizar categorias de gastos',
             customizeSpendCategoriesSubText: 'Organize e classifique despesas',
-            customizeExpenseCategories: 'Personalize suas categorias de despesas',
-            customizeExpenseCategoriesSubText: 'Adicione as categorias da sua empresa para codificar suas despesas',
+            customizeExpenseCategories: 'Personalize suas categorias de despesa',
+            customizeExpenseCategoriesSubText: 'Adicione as categorias da sua empresa para classificar suas despesas',
             createExpense: 'Criar uma despesa',
             createExpenseSubText: 'Escaneie, arraste e solte ou insira manualmente uma despesa usando o botão +',
             linkPersonalCard: 'Vincular cartão pessoal',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index dcc1cf1e..7139e051 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1053,8 +1053,8 @@ const translations: TranslationDeepObject<typeof en> = {
             inviteAccountant: '邀请你的会计',
             customizeSpendCategories: '自定义支出类别',
             customizeSpendCategoriesSubText: '整理并分类支出',
-            customizeExpenseCategories: '自定义您的支出类别',
-            customizeExpenseCategoriesSubText: '添加贵公司的类别以对支出进行编码',
+            customizeExpenseCategories: '自定义您的报销类别',
+            customizeExpenseCategoriesSubText: '添加你们公司的类别以归类报销费用',
             createExpense: '创建报销费用',
             createExpenseSubText: '使用 + 按钮扫描、拖放或手动输入支出',
             linkPersonalCard: '关联个人银行卡',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Pushed c99a8d95d3 with three fixes. Your Customize your expense categories bug is real and reproduces, but it can't be fixed in App — I filed #100220 for it. Details below.

What I fixed

1. The 60-day window was anchored on the wrong workspace (Codex P2, and item 2 of my own review)

The window keyed off nvp_private_firstPolicyCreatedDate, which points at the account's first ever workspace. A user who deletes an older workspace and gets a fresh Submit workspace inherited the old one's expired window and never saw the section. It also gave two users at the same product moment different window lengths.

It now anchors on the Submit workspace's own created timestamp, which createWorkspace writes optimistically, so it still works offline. Dropped the nvp_private_firstDayFreeTrial entry from the chain: once a Submit user upgrades, the policy is TEAM and the isSubmitPolicy gate hides the section anyway, so that entry was unreachable — the old test covering it passed for the wrong reason and is replaced.

2. The Create a workspace fallback contradicted the intent (item 1 of my review)

A Submit-intent user with no Submit workspace got a lone Create a workspace row that navigates to WORKSPACE_CONFIRMATION — which creates a paid workspace, after which the isSubmitPolicy gate hides the section. The user completed a step and the checklist vanished instead of advancing. It also contradicted the PR description and the issue spec, which list exactly two to-dos.

The section now hides for the Submit intent whenever there is no usable Submit workspace. This folded in the separate Team/Corporate gate, so the guard order is simpler than before.

3. Copy nit — dropped the trailing period from customizeExpenseCategoriesSubText across all 11 locales, so both rows in the list punctuate the same way as every other subtext in the section.

Your bug: the category row checks itself

Confirmed, and the root cause is outside this PR.

hasCustomCategories calls a category "custom" if its name is absent from the 19-entry CONST.POLICY.DEFAULT_CATEGORIES list (Advertising, Benefits, Car, …). That's the US scheme. The API seeds a UK account's workspace with a country-specific set — test runs captured Car, Van and Travel Expenses: Air and Accountancy, Legal and Other Professional Fees: Insurance Policies/Premiums — so none of the seeded defaults match and every one reads as user-created. That's why only UK/England reproduces it.

Two things follow:

  • It's pre-existing, not introduced here. hasCustomCategories backs the category to-do for MANAGE_TEAM, TRACK_WORKSPACE, and TRACK_PERSONAL on main too, so every non-US account already sees a pre-completed category step.
  • App can't fix it. PolicyCategory has no "is a default" flag (origin means the external accounting service, empty for seeded defaults), and the country default lists aren't in this repo. The API has to tell the App which categories it seeded. Tracked in Getting started: the category to-do is auto-checked on a brand-new workspace for non-US accounts #100220.

Two review items I did not change

  • Link personal card completion ignores lastScrapeResult — a card whose connection has broken still reads as complete. This is inherited verbatim from the TRACK_PERSONAL row, so changing it here alone would make the two rows disagree. Worth a separate decision.
  • SUBMIT in isSubmitIntent — kept. It's only written for users invited to someone else's workspace, and those users are correctly excluded by the isPolicyOwner gate. If such a user later creates and activates their own Submit workspace, the section should render, so the check is harmless and correct.
Verification
  • useGettingStartedItems.test.ts: 152 passed, 0 failed (added tests for the new-workspace anchor beating an expired NVP, a stale trial date, offline rendering from the optimistic created, hiding on a personal policy instead of showing Create a workspace, and hiding with no active workspace; removed the two tests that asserted the old behaviour).
  • npm run typecheck — passed.
  • ESLint on the changed files — passed.
  • react-compiler-compliance-check on the hook — passed.
  • cspell on the changed files — passed.
  • CI on c99a8d95d3: everything green except checklist and Check independent approval, which need a human.
  • I could not verify in a browser. The dev web session went to a blank page (0 DOM nodes) before the sign-up flow could be driven and never recovered, so I have no runtime evidence for this commit. The unit tests cover every branch I changed, but a manual pass on the two-row rendering would be worth doing.

view run

@abzokhattab

Copy link
Copy Markdown
Contributor

melvin created an issue on its own here #100220 after my comment here #99842 (comment) iam confused: should we tackle the mentioned issue as a part of the current PR or should we address it there

cc @carlosmiceli @hungvu193 @garrettmknight

@abzokhattab

Copy link
Copy Markdown
Contributor

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 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
    • 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
    • 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 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/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp Screenshot 2026-09-02 at 23 59 06
iOS: mWeb Safari Screenshot 2026-09-02 at 23 38 51
MacOS: Chrome / Safari
Screen.Recording.2026-09-02.at.20.12.09.mov

@abzokhattab abzokhattab left a comment

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.

If we decide to exclude the bug above to be fixed in the created issue then the current changes looks good to me

@melvin-bot

melvin-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #99441 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

@melvin-bot
melvin-bot Bot requested a review from arosiclair September 2, 2026 22:00
@hungvu193
hungvu193 requested a review from iwiznia September 3, 2026 01:36
@hungvu193

Copy link
Copy Markdown
Contributor

melvin created an issue on its own here #100220 after my comment here #99842 (comment) iam confused: should we tackle the mentioned issue as a part of the current PR or should we address it there

cc @carlosmiceli @hungvu193 @garrettmknight

I saw that Melvin mentioned that will require BE changes that's why it created a new issue. I'm fine with that

@iwiznia
iwiznia merged commit ea45c1c into main Sep 3, 2026
44 of 47 checks passed
@iwiznia
iwiznia deleted the claude-submitGettingStartedChecklist branch September 3, 2026 15:17
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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

@OSBotify

OSBotify commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/iwiznia in version: 9.4.70-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Yes — help site changes were required. Draft PR: #100340

One article documents this feature and two of its statements were made wrong by this PR: docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md.

What the article said Why it's now wrong
Getting started appears for the manage my team, track and budget my expenses, or Organize my personal spending intents Submit expenses to my employer now qualifies too, so a Submit user reading the FAQ was told the section shouldn't appear for them
It appears "within 60 days of your free trial start date" Submit is a free plan with no trial. The window is anchored on the Submit workspace's creation time instead (useGettingStartedItems.ts:116)

The article also never documented the two to-dos, that the categories item depends on Categories being enabled, or that invited members of someone else's Submit workspace are filtered out by the ownership gate.

What the docs PR changes, and how each claim was verified

Changes to Expensify-Home-Overview.md (+17 / −2, one file):

  1. How the Getting started section works on Home — added the two to-dos for this intent (Customize your expense categories, Link personal card), their destinations, the Categories-enabled condition on the first, the absence of a Create a workspace item, and that the section hides once both are checked off. Cross-links the existing Submit workspace article.
  2. Why don't I see the Getting started section? — added Submit expenses to my employer to the qualifying intents and split the 60-day rule (trial start date for paid intents, workspace creation date for Submit), plus the owner-only gate.
  3. keywords — added submit expenses to my employer, Submit workspace, customize your expense categories.

Verified against source rather than the PR description:

  • Copy is verbatim from en.ts: customizeExpenseCategories: 'Customize your expense categories', linkPersonalCard: 'Link personal card', and CONST.ONBOARDING_CHOICES.EMPLOYER: 'Submit expenses to my employer'.
  • Wallet and Categories confirmed on the live UI (dev NewDot web): the Account settings menu item is exactly Wallet; the workspace settings item and its page heading are both exactly Categories.
  • Behavior read from the merged hook: routes ROUTES.WORKSPACE_CATEGORIES / ROUTES.SETTINGS_WALLET, the isPolicyOwner gate, the areCategoriesEnabled condition, and buildResult hiding the section when every item is complete.
  • Worth noting for the article's accuracy: the merged code anchors the window on policy.created, not on nvp_private_firstPolicyCreatedDate as the PR description above still states. The docs follow the merged code.

Not changed: no other file under docs/articles mentions the Getting started checklist, so scope stayed at this one article. The new copy applies only to English — the 10 translated locale strings this PR added are product copy, not help site content.

Not run: npm run spell-changed — the sandbox blocked the cspell invocation. The change introduces no new product nouns beyond terms already used elsewhere on the help site.

The draft PR is labeled HelpDot, assigned to MelvinBot, and has PullerBear requested as reviewer.

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


view run

@OSBotify

OSBotify commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/iwiznia in version: 9.4.72-0 🚀

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

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

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants