Skip to content

[Sentry: APP-M2G / APP-M0D / APP-M5M / APP-M4R] HybridApp crash, CONST.VALIDATE_CODE_REASONS undefined in card fraud and card replacement flows #102045

Closed

Description

@mountiny

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.89
Reproducible in staging?: Needs to be confirmed
Reproducible in production?: Yes — crash is live in production
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A — found via Sentry production crash scan
Email or phone of affected tester (no customers): N/A
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: Sentry production crash scan
Slack conversation (hyperlinked to channel name): N/A

Action Performed:

  1. Open Expensify Classic on iOS or Android (HybridApp, OldDot side).
  2. Go to an Expensify Card.
  3. Start either flow:
    • Report the card as fraudulent, which opens the request replacement virtual card modal.
    • Replace the card, which opens the card replacement step.

Expected Result:

The modal opens and asks for a magic code. No crash.

Actual Result:

The app crashes as the modal finishes its present animation. Two variants, one per flow:

TypeError: undefined is not an object (evaluating 'CONST.VALIDATE_CODE_REASONS.REPORT_CARD_FRAUD')
  at app/Controllers/Modals/RequestReplacementVirtualCardModalController.js:79:48

TypeError: undefined is not an object (evaluating 'CONST.VALIDATE_CODE_REASONS.REPLACE_CARD')
  at app/Controllers/CardReplacementStepController.js:147:56

CONST.VALIDATE_CODE_REASONS is undefined at that point, so reading a member off it throws and the surrounding native modal completion turns it into a fatal exception.

Workaround:

None. Both card flows are blocked on mobile OldDot for affected users.

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Sentry details

Sentry issues:

Sentry ID Users (7d) Events (7d) Platform Constant
APP-M2G 6 7 iOS REPORT_CARD_FRAUD
APP-M0D 4 9 iOS REPLACE_CARD
APP-M5M 1 1 Android REPORT_CARD_FRAUD
APP-M4R 1 2 Android REPLACE_CARD

Impact (snapshot at filing, 2026-09-23):

  • Users (last 7d, all four IDs): 12
  • Events (last 7d, all four IDs): 19
  • First seen: 2026-09-17
  • Last seen: ongoing
  • Platform: HybridApp iOS (com.expensify.expensifylite) and Android (org.me.mobiexpensifyg)
  • Mechanism: nsexception via -[YAPL logException:] on iOS, JSCWrapper on Android; level fatal

Stack trace — APP-M2G (iOS, innermost last):

BottomDrawerModalViewController.postViewDidLoad
GenericTwoChoiceModalViewController.postViewDidLoad
BottomDrawerModalViewController.beginLeaveAnimation
+[YAPLJS callFunction:args:]
-[JSValue callWithArguments:]
-[JSContext(Internal) valueFromNotifyException:]
-[YAPL logException:]
app/Controllers/Modals/RequestReplacementVirtualCardModalController.js:79:48

Suspected cause: a regression from [reasonCode] Pass card and validate reasons from Mobile-Expensify, which closed on 2026-09-15. The first crash event is 2026-09-17. That work introduced reads of CONST.VALIDATE_CODE_REASONS.* in these two controllers, but the VALIDATE_CODE_REASONS map does not exist on CONST in the shipped Mobile-Expensify bundle.

Likely one of: the constant was added to a file the mobile bundle does not include, it is defined under a different name, or it is populated asynchronously and read before it lands.

Note: the fix lives in Mobile-Expensify (Expensify Classic), not in the App repo. The parent issue carries Concierge (dont reopen), so this is filed fresh rather than reopening it.

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @mountiny

Activity

  1. self-assigned this
    on Sep 23, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 23, 2026
  3. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @linhvovan29546 (External)

  4. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  5. MelvinBot commented on Sep 23, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    VALIDATE_CODE_REASONS is owned by expensify-common, not by the global CONST object that Expensify Classic's native JS runtime exposes. The two Classic card controllers read a bare global CONST, so they resolve Classic's own constants object — which has no VALIDATE_CODE_REASONS key — and undefined.REPORT_CARD_FRAUD throws.

    The JSC error text confirms this precisely: undefined is not an object (evaluating 'CONST.VALIDATE_CODE_REASONS.REPORT_CARD_FRAUD') means CONST is an object and CONST.VALIDATE_CODE_REASONS is undefined. A missing CONST would instead raise Can't find variable: CONST.

    The map lives at lib/CONST.ts:903-913 in expensify-common, with REPLACE_CARD: 'replace_card' and REPORT_CARD_FRAUD: 'report_card_fraud'.

    Nothing in the App repo causes this, and nothing in the App repo can fix it. NewDot reaches the same constants through a module import, never a global, so its equivalent flows are unaffected.

    What changes do you think we should make in order to solve the problem?

    The fix has to land in Expensify Classic (Mobile-Expensify), which I don't have access to — so I can't propose a concrete patch. Please escalate to an internal engineer with access. The shape of the fix is:

    1. Make the two card controllers resolve the reason codes from expensify-common's CONST rather than the Classic global CONST, or add the VALIDATE_CODE_REASONS map to Classic's own constants.
    2. Confirm whichever expensify-common version Classic pins actually contains REPORT_CARD_FRAUD — it was added after the rest of the map, so an older pin would leave the key undefined even with a correct import.
    3. Guard both call sites so a missing reason code degrades to sending no reasonCode instead of throwing inside the modal's present-animation completion, which is what turns this into a fatal native exception rather than a caught JS error.

    Worth checking during the fix: Auth tolerates a validate code with no reasonCode, but rejects one generated with the wrong reasonCode — so a fallback must omit the field, not substitute a placeholder.

    What alternative solutions did you explore? (Optional)

    Alternatives considered and ruled out
    • NewDot shipped a bad constant. Ruled out. expensify-common@2.0.201 is pinned in App and contains the full map; every App consumer imports it explicitly as COMMON_CONST, for example ReportVirtualCardFraudVerifyAccountPage.tsx:83 and ReportCardLostConfirmValidateCodePage.tsx:95.
    • The HybridApp bridge drops the constant in transit. Ruled out. The bridge surface in modules/hybrid-app/ passes only strings, booleans, and a three-key Onyx settings blob. No constants object crosses, and App never assigns global.CONST / window.CONST or injects JS into a Classic webview. The two sides are separately bundled with separate CONST objects.
    • Async population — the constant lands late. Ruled out as the primary cause. Both crash sites fire on a user-initiated modal open, long after startup, and the failure is 100% reproducible per the report rather than intermittent.
    • App's own CONST should carry the map. Ruled out. ResendValidateCodeParams.ts documents in-code that expensify-common's VALIDATE_CODE_REASONS is the canonical home; App keeps exactly one reason locally and explicitly notes it's temporary.

    Related issues: I scanned open App issues for this crash signature and the surrounding card/magic-code flows and found no duplicates or near-matches.

    Verification note: I could not drive a browser for this run, and this crash is native-only on the Classic side, so the analysis is from code and the Sentry data in the description rather than a live repro. The expensify-common version Classic pins is the one claim I could not check directly.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102045) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  6. mountiny commented on Sep 23, 2026

    @mountiny
    ContributorAuthor

    (Vit's agent)

    Root cause found, draft fix up at Expensify/expensify-common#948.

    Why this crashes. CONST reaches OldDot through app/shared/index.js, which does require('expensify-common/lib/CONST').CONST. Mobile-Expensify pins expensify-common at 45c36bf on the chirag-mobile-old branch — the v1 compatibility line for OldDot. That branch forked from main at 2e5cff55 on 2021-05-10, and main has since migrated CONST to TypeScript (lib/CONST.jsx → lib/CONST.ts, commit e85751d1, 2024-05-17), so OldDot cannot consume the v2 tree.

    VALIDATE_CODE_REASONS was added in e06391e on 2026-04-28, to main only. It was never back-ported to v1. The reasonCode work then added three call sites in Mobile-Expensify against a constant that does not exist on the branch OldDot actually installs:

    File Line Member
    app/Controllers/Modals/RequestReplacementVirtualCardModalController.js 79 REPORT_CARD_FRAUD
    app/Controllers/CardReplacementStepController.js 147 REPLACE_CARD
    app/Controllers/InboxController.js 2445 VALIDATE_ACCOUNT

    Every other CONST.* member Mobile-Expensify uses (DATE, STATES, SMART_SCAN, EXPENSIFY_CARD) does exist in the pinned CONST.jsx. Only this one is missing, which matches the crash exactly.

    Note the third call site, InboxController.js:2445 (VALIDATE_ACCOUNT, resend validation code from the inbox task), is the same bug and is not in the Sentry issues on this ticket yet. The fix covers it.

    Fix, in two steps. This follows the established v1 back-port workflow, the same as expensify-common #932 and #944.

    1. Add VALIDATE_CODE_REASONS to v1 CONST expensify-common#948 — adds the full 14-member VALIDATE_CODE_REASONS map to lib/CONST.jsx on chirag-mobile-old. Values diffed key by key against main; all 14 identical. Draft, ready for review.
    2. A follow-up Mobile-Expensify PR bumping the pinned commit. That one is blocked until Fixes two bugs with actions found #948 merges, because package-lock.json carries an integrity hash that only npm install can compute for the new sha.

    No code change is needed in Mobile-Expensify itself — the three call sites are correct, the constant they read just was not there.

  7. mountiny commented on Sep 23, 2026

    @mountiny
    ContributorAuthor
  8. melvin-bot commented on Sep 30, 2026

    @melvin-bot

    @mountiny Whoops! This issue is 2 days overdue. Let's get this updated quick!

  9. mountiny commented on Oct 1, 2026

    @mountiny
    ContributorAuthor

    @MelvinBot can you check Sentry for this event if this is still repro in the latest versions?

  10. MelvinBot commented on Oct 1, 2026

    @MelvinBot
    Contributor

    I couldn't check Sentry. The Sentry connection was rejected for this run (HTTP 401, invalid token), so I couldn't query APP-M2G, APP-M0D, APP-M5M, or APP-M4R.

    What I could confirm: the Add VALIDATE_CODE_REASONS to v1 CONST fix merged on 2026-09-23. I can't see whether the follow-up Mobile-Expensify bump has merged or which App version ships it.

    To confirm the fix, filter each Sentry issue by release and check for events in versions after the Mobile-Expensify bump. Retry @MelvinBot once the Sentry token is fixed and I'll check it.


    view run

  11. mountiny commented on Oct 2, 2026

    @mountiny
    ContributorAuthor

    (Vit's agent)

    Checked Sentry. The crash is fixed in every App release that ships the fix.

    Events for APP-M2G, APP-M0D, APP-M5M and APP-M4R in the last 30 days:

    Release Has fix? Crash events
    9.4.77 to 9.4.92-8 No 37
    9.4.93-3 to 9.5.0-5 Yes 0

    The fixed releases have real traffic: 3,897 mobile error events from 2,622 users in the last 14 days, and none of them are this crash. The last event came on 2026-09-28 from 9.4.92-8, which does not include the fix. A few more events can still come from users on old builds.

    Closing this issue as fixed.

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

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorReviewingHas a PR in review

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions