Skip to content

[$175] [Sentry: APP-C2W] Web: Copy in the report action context menu crashes when the report action is null #104746

Open

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-C2W

Impact (snapshot at filing)

  • Users (total since first seen): 24
  • Events: 88
  • Users (last 14d, all versions): 8
  • Users (last 14d, production 9.5.13 to 9.5.15): 2 (9.5.13: 1, 9.5.14: 1)
  • First seen: 2026-05-05
  • Last seen: 2026-10-21 (ongoing)
  • Platform: web (standalone)
  • App version(s): 9.5.5 to 9.5.14
  • Affected route(s) (parameterized): /r/:reportID
  • Mechanism: auto.browser.global_handlers.onerror

Stack trace (top frames, first-party only)

TypeError: Cannot read properties of null (reading 'actionName')
  src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:1187  onPress (Copy to clipboard)   isTagModificationAction(reportAction.actionName)
  src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx:481  callback
  src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx:351  interceptAnonymousUser
  src/pages/inbox/report/ContextMenu/ContextMenuItem.tsx:91  triggerPressAndUpdateSuccess
  src/hooks/useSingleExecution/index.ts:14

Suspected cause

The copy to clipboard action reads reportAction?.actionName in every branch of its else if chain except one. On current main this is ContextMenuActions.tsx:1200: isTagModificationAction(reportAction.actionName). When the context menu runs with a null report action, that branch throws.

Reproduction

Unknown. See the Sentry events linked from the Sentry issue. The crash happens when the user presses Copy in the report action context menu.

Related

  • Sibling Sentry IDs: none confirmed
  • Prior GH issues: none found
Upwork Automation - Do Not Edit

Activity

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

    @melvin-bot

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

  4. changed the title [-][Sentry: APP-C2W] Web: Copy in the report action context menu crashes when the report action is null[/-] [+][$175] [Sentry: APP-C2W] Web: Copy in the report action context menu crashes when the report action is null[/+] on Oct 22, 2026
  5. melvin-bot commented on Oct 22, 2026

    @melvin-bot
  6. Abdallah-1551 commented on Oct 22, 2026

    @Abdallah-1551

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On web, pressing Copy in the report action context menu can throw TypeError: Cannot read properties of null (reading 'actionName'). The crash is in the "Copy to clipboard" onPress in ContextMenuActions.tsx. It happens when the menu runs with a null/undefined reportAction. The exception is uncaught and surfaces through global_handlers.onerror. Sentry shows 24 users and 88 events, and it is still ongoing. Copy should never crash. With no report action it should fall through to the normal fallback copy behavior.

    What is the root cause of that problem?

    The onPress for the copy action receives reportAction typed as an optional/nullable Onyx entry. Every branch of its else if chain reads reportAction?.actionName defensively except one:

    } else if (isTagModificationAction(reportAction.actionName)) {

    If the underlying action is gone by the time the user presses Copy, this branch dereferences null. This can happen when an optimistic action is replaced or removed in Onyx while the menu is still open. Because the earlier branches are optional-chained, evaluation reaches this unguarded one and throws. The missing ?. is a plain inconsistency, so it needs no deeper state fix.

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

    In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx, in the Copy to clipboard onPress, guard the one unsafe branch:

    // before
    } else if (isTagModificationAction(reportAction.actionName)) {
        const modifyTagMessage = getTagListNameFromReportAction(reportAction) ...
        Clipboard.setString(...)
    }
    
    // after
    } else if (reportAction && isTagModificationAction(reportAction.actionName)) {
        ...
    }

    Using reportAction && rather than reportAction?.actionName has two benefits:

    • It keeps isTagModificationAction's parameter type as a non-optional string, so no signature change is needed.
    • It narrows reportAction to non-null inside the block, so any reportAction usage in the body (e.g. passing it to helpers) type-checks without ! or extra ?..

    When reportAction is null, the branch is skipped and evaluation continues to the remaining branches and the final else. Those already handle a missing action, so the user gets the default copy behavior (copy the displayed text/HTML) instead of a crash.

    While there, audit the rest of that onPress and the neighboring context-menu actions (BaseReportActionContextMenu.tsx callback path) for any other bare reportAction. dereferences, and make them consistent. No behavior change is expected for non-null actions.

    What alternative solutions did you explore? (Optional)

    • Early return when reportAction is null: This would stop the crash, but it would silently disable copying for cases that currently work, such as copying selected text or a plain-text message via the fallback branch. Rejected as a regression risk.
    • Hide/disable the Copy item when reportAction is null: This is a larger UX and logic change in shouldShow, and it would not protect against the action disappearing between render and press. Rejected as unnecessary.
    • Wrap onPress in try/catch: This hides the bug instead of fixing it. Rejected.
    • reportAction?.actionName with a widened isTagModificationAction signature: This works but spreads nullability into a utility function for no benefit. Rejected.

    Testing

    Unit test (e.g. tests/unit/ContextMenuActionsTest.ts):

    1. Find the copyToClipboard entry in ContextMenuActions.
    2. Call its onPress with reportAction: undefined and again with null (cast as needed). Use a mocked Clipboard.setString and closePopover = true. Assert that it does not throw and falls through to the default copy behavior.
    3. Call onPress with a real tag-modification report action, e.g. UPDATE_TAG / ADD_TAG. Assert Clipboard.setString is called with the expected tag message, to confirm the existing behavior is unchanged.

    Manual (web):

    1. Open a report with messages and right-click a message to open the context menu.
    2. Trigger removal of the underlying action while the menu is open, for example by deleting the message from another client, or by using an optimistic action that gets replaced.
    3. Click Copy and verify there is no console error or Sentry event.
    4. Verify normal copying still works for text messages, tag-change actions, and other action types.

    Regression: run npm run typecheck, npm run lint, and the context menu unit tests.

  7. MelvinBot commented on Oct 22, 2026

    @MelvinBot
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On web, pressing Copy in a report action context menu crashes with Cannot read properties of null (reading 'actionName') when the menu has no report action.

    What is the root cause of that problem?

    The context menu can open, or stay open, while its report action lookup returns nothing. Copy still shows, and its press handler assumes the action exists.

    1. The menu looks up the action in Onyx by ID. The lookup returns undefined when the ID is cleared or the action is not in the report's actions (BaseReportActionContextMenu.tsx:180-185).
    2. The payload passed to onPress turns this into null and casts it to a non-null ReportAction (BaseReportActionContextMenu.tsx:407). TypeScript can't see the null (ContextMenuActions.tsx:346).
    3. Copy's shouldShow returns true for a missing action, because every check in it returns false for undefined (ContextMenuActions.tsx:1048-1053).
    4. In onPress, every check before line 1200 handles null. Line 1200 reads reportAction.actionName without ?. and throws (ContextMenuActions.tsx:1200). Lines 1224-1282 have the same pattern, so fixing only line 1200 moves the crash to line 1224.
    How the action likely becomes null
    • Hide/show race (most likely on web). runAndResetOnPopoverHide clears the action ID, report ID and selection refs when the popover hides, without checking whether a new menu is already open (PopoverReportActionContextMenu.tsx:285-294). If you right-click message B while menu A is still closing, A's hide callback can wipe B's refs. The next re-render passes no action ID and an empty selection. An empty selection also explains why the crash skips the selection branch at line 1101.
    • The action is removed from Onyx while the menu is open, for example when an optimistic action is replaced.
    • The original report ID doesn't contain the action in some combined views, so the lookup is empty from the first render.

    I have not confirmed which trigger is most common in production. The Sentry breadcrumbs would show it.

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

    1. In Copy's onPress, keep the selection branch first. Then return early if reportAction is missing, before the action-type checks.
    2. Hide Copy when there is no report action by adding a reportAction check to its shouldShow.
    3. Change ContextMenuActionPayload.reportAction to OnyxEntry<ReportAction> and remove the cast in BaseReportActionContextMenu.tsx. TypeScript will then flag every unsafe access, including lines 1224-1282.
    4. Apply the same guard to Reply in thread. It has the same gap: its shouldShow passes with no action, and its onPress reads reportAction.actorAccountID (ContextMenuActions.tsx:545).
    5. Add a unit test: given a REPORT_ACTION menu with no action, Copy is hidden, and pressing it does not throw.

    What alternative solutions did you explore? (Optional)

    • Only add ?. at line 1200. This is not enough because line 1224 throws next.
    • Fix the popover race too. Make runAndResetOnPopoverHide skip clearing refs when a newer menu is visible, for example by checking an instance token. This fixes one trigger, but the guard above is still needed for the Onyx-removal case.
    Code history

    The unsafe lines are old, so this looks like a long-standing runtime race rather than a recent regression. The UPDATE_FEATURE_ENABLED branch at line 1224 came from [Policy Change Logs] Add policy changelog for "enabled/disabled <feature>". The null as ReportAction cast and line 1200 are older than June 2025.


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


    view run

  8. magomedta commented on Oct 22, 2026

    @magomedta

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~01777d047a06f0e470

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Web, clicking Copy to clipboard in the report action context menu can crash the entire application with TypeError: Cannot read properties of null (reading 'actionName') if reportAction is null or undefined (e.g. if the action was optimistically deleted, dismissed, or unloaded while the menu was open).

    What is the root cause of that problem?

    In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:
    In the onPress callback for CONST.CONTEXT_MENU_ACTIONS.COPY_TO_CLIPBOARD (around lines 1180–1240), there is an if ... else if chain checking reportAction?.actionName across different policy change log action types.
    All branches safely use optional chaining (reportAction?.actionName === ...), except line 1200:

    } else if (isTagModificationAction(reportAction.actionName)) {
        Clipboard.setString(getCleanedTagName(getWorkspaceTagUpdateMessage(translate, reportAction)));

    Line 1200 directly dereferences reportAction.actionName without checking if reportAction is null. When reportAction is null/undefined, evaluating this condition immediately throws an unhandled TypeError.


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

    In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:
    Guard the isTagModificationAction condition by ensuring reportAction exists before accessing .actionName:

    --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
    +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
    @@ -1197,7 +1197,7 @@ const getContextMenuActions = (
                         Clipboard.setString(getCleanedTagName(getTagListUpdatedMessage(translate, reportAction)));
                     } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_LIST_REQUIRED) {
                         Clipboard.setString(getCleanedTagName(getTagListUpdatedRequiredMessage(translate, reportAction)));
    -                } else if (isTagModificationAction(reportAction.actionName)) {
    +                } else if (reportAction && isTagModificationAction(reportAction.actionName)) {
                         Clipboard.setString(getCleanedTagName(getWorkspaceTagUpdateMessage(translate, reportAction)));
                     } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_UNIT) {
                         Clipboard.setString(getWorkspaceCustomUnitUpdatedMessage(translate, reportAction));

    Key Advantages:

    1. Preserves Type Safety: Narrows reportAction to non-null within the branch body, allowing getWorkspaceTagUpdateMessage(translate, reportAction) and isTagModificationAction(reportAction.actionName) to typecheck cleanly without needing non-null assertions (!) or changing utility signatures.
    2. Graceful Fallback: If reportAction is null, it cleanly passes down to subsequent branches and the default clipboard fallback (e.g. copying selected text/selection).
    3. Targeted & Minimal: Single-line fix that matches the defensive pattern used across the rest of ContextMenuActions.tsx.

    What alternative solutions did you explore? (Optional)

    • Changing isTagModificationAction(actionName?: string): boolean in ReportActionsUtils.ts:
      Unnecessary widening of a pure utility function that expects a valid string action name across many caller sites. Checking reportAction && at the callsite is cleaner and safer.
    • Early return if !reportAction:
      Avoided because the context menu copy handler also supports copying user selection text even when there is no specific action attached.

    What automated tests can you add to prevent this from re-occurring?

    In tests/unit/ContextMenuActionsTest.ts:

    • Invoke the COPY_TO_CLIPBOARD onPress callback with reportAction: null and verify that no error is thrown and the clipboard fallback is executed gracefully.
    • Invoke with a valid tag modification action (e.g. CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG) and verify that getWorkspaceTagUpdateMessage is correctly formatted and copied.
  9. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  10. sijo0218 commented on Oct 22, 2026

    @sijo0218

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~011091133bbb09cd1c

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Web, clicking Copy to clipboard in the report action context menu crashes the application with TypeError: Cannot read properties of null (reading 'actionName') when reportAction is null or undefined (e.g. if the action was optimistically replaced/unloaded in Onyx while the popover was visible, or triggered during a popover hide/reset race).


    What is the root cause of that problem?

    1. Context Menu Runtime Race & Empty Action Payload:
      In BaseReportActionContextMenu.tsx:481, when the user selects a context menu item, it calls:

      contextAction.onPress(closePopover, { reportAction, ... });

      If the report action was optimistically deleted or replaced in Onyx while the popover was active, or if runAndResetOnPopoverHide (PopoverReportActionContextMenu.tsx:285–294) cleared the action ID ref during a popover transition before the click handler executed, reportAction is passed as null or undefined.

    2. Un-guarded Accesses in the 500-Line Action Type Branching:
      In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:1091:

      • If selection is present, it copies the highlighted text and returns safely.
      • However, if selection is empty (""), execution falls through into the else if chain of action-specific copy formatting.
      • At line 1187, it evaluates:
        } else if (isTagModificationAction(reportAction.actionName)) {
        Because reportAction is null, evaluating reportAction.actionName immediately throws:
        TypeError: Cannot read properties of null (reading 'actionName').
    3. Why Single-Line Competitor Fixes are Fatally Flawed:
      A competitor proposed only adding reportAction && at line 1187 (isTagModificationAction). This is completely broken. Immediately following that branch (lines 1205 to 1238), there are 17 consecutive branches that directly access reportAction.actionName without optional chaining (e.g. reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED, UPDATE_IS_ATTENDEE_TRACKING_ENABLED, UPDATE_DEFAULT_APPROVER, UPDATE_SUBMITS_TO, etc.). Simply patching line 1187 will cause the very next branch at line 1205 to throw the identical TypeError.


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

    In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:

    1. Short-Circuit Empty Actions in COPY_TO_CLIPBOARD onPress:
      After handling if (selection), immediately return early if !reportAction. If there is no highlighted selection and no report action, there is nothing to copy. This cleanly bypasses the entire 500-line action-specific branch chain and guarantees type-safe non-null reportAction across all subsequent branches:

      --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      @@ -1093,6 +1093,8 @@ const getContextMenuActions = (
                       if (selection) {
                           // When the user has highlighted part of a message, always copy exactly what's selected,
                           // regardless of the report action type (including system messages like "marked as paid").
                           setClipboardMessageWithCleanedMentions(selection, reportIDToName);
      +                } else if (!reportAction) {
      +                    return;
                       } else if (isReportPreviewAction) {
                           const iouReportID = getIOUReportIDFromReportActionPreview(reportAction);
    2. Add Guard to shouldShow:
      Ensure Copy is suppressed when there is neither an active report action nor a highlighted selection:

      --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      @@ -1050,6 +1050,7 @@ const getContextMenuActions = (
               icon: 'Copy',
               successTextTranslateKey: 'reportActionContextMenu.copied',
               successIcon: 'Checkmark',
               shouldShow: ({type, reportAction}) =>
                   type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
      +            !!reportAction &&
                   !isReportActionAttachment(reportAction) &&
                   !isMessageDeleted(reportAction) &&

    Advantages of this Solution:

    • 100% Comprehensive: Fully protects lines 1187, 1205–1238, and all subsequent branches without needing to patch 20+ separate lines.
    • Preserves Text Selection Copying: If a user highlighted text before the action unloaded or transitioned, the highlighted text is still copied cleanly via selection.
    • Zero Overhead & Zero Breaking Changes: Avoids large type rewrites or breaking signature changes across BaseReportActionContextMenu.tsx.

    What alternative solutions did you explore? (Optional)

    1. Only adding reportAction && to isTagModificationAction (Competitor proposal):
      • Rejected because lines 1205 to 1238 immediately dereference reportAction.actionName across 17 other branches. The crash would simply shift 5 lines down.
    2. Hiding Copy in shouldShow without an onPress guard (MelvinBot proposal):
      • Rejected because shouldShow is evaluated when the menu renders. If an action is removed or replaced in Onyx after the menu is open, the click handler still fires and would crash without the defensive onPress short-circuit.

    What specific scenarios should we cover in automated tests to prevent reintroduction of this issue in the future?

    In tests/unit/ContextMenuActionsCopyMessageTest.ts:

    1. Null reportAction with Empty Selection:
      • Call copyMessageAction.onPress(false, createReportActionPayload(null)) where selection: ''.
      • Assert that it executes cleanly without throwing an unhandled TypeError and does not call Clipboard.setString or setHtml.
    2. Null reportAction with Non-Empty Selection:
      • Call copyMessageAction.onPress(false, createPayload('Hello world')) with reportAction: null.
      • Assert that the highlighted text is successfully copied to the clipboard.
    3. Valid Report Action Copy:
      • Verify existing tests continue to pass for task, modified expense, and policy change log actions.
  11. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  12. HamzaKhanBUIC commented on Oct 22, 2026

    @HamzaKhanBUIC

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/hamzakhanbuic

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Web, clicking Copy in the report action context menu can throw an unhandled TypeError: Cannot read properties of null (reading 'actionName') when reportAction is null or undefined (e.g. when an action was optimistically deleted or unloaded in Onyx while the context menu was open, or during popover dismissal/re-open race conditions).

    What is the root cause of that problem?

    The crash stems from a combination of missing shouldShow validation, an unsafe type cast, and un-guarded property dereferencing in the COPY_TO_CLIPBOARD action:

    1. Unsafe Non-Null Assertion in BaseReportActionContextMenu.tsx:407:
      In BaseReportActionContextMenu.tsx:

      // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
      reportAction: (reportAction ?? null) as ReportAction,

      When reportAction is undefined or null, it is cast to a non-nullable ReportAction. This masks nullable runtime states from TypeScript across ContextMenuActions.tsx.

    2. Missing Guard in copyMessage.shouldShow (ContextMenuActions.tsx:1048-1053):

      shouldShow: ({type, reportAction}) =>
          type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
          !isReportActionAttachment(reportAction) &&
          !isMessageDeleted(reportAction) &&
          !isTripPreview(reportAction) &&
          !isCreatedSupportTicketReportAction(reportAction),

      All four negation helpers evaluate to true when reportAction is null or undefined (e.g. !isReportActionAttachment(undefined) === true). Because !!reportAction is missing, the Copy action is rendered even when no report action exists.

    3. Cascading Unguarded Property Access in onPress (ContextMenuActions.tsx:1099-1282):
      When selection is empty (the user right-clicked without highlighting text), execution passes into the action-type else if chain:

      • Line 1200 dereferences reportAction.actionName directly without optional chaining:
        } else if (isTagModificationAction(reportAction.actionName)) {
      • Crucially, even if line 1200 is patched with optional chaining, lines 1224 to 1270 contain 17 consecutive branches that directly access reportAction.actionName without optional chaining (e.g. line 1224: reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED).
      • Thus, any single-line patch at line 1200 simply shifts the fatal TypeError down to line 1224.

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

    1. Short-Circuit Null reportAction in copyMessage.onPress (src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx):
      After handling if (selection) (which correctly preserves text copying when the user has highlighted text), immediately return if !reportAction. This completely bypasses the entire 500-line action-specific branch chain and allows TypeScript control-flow analysis to narrow reportAction to non-null for all subsequent branches:

      --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      @@ -1103,6 +1103,8 @@ const getContextMenuActions = (
                           // regardless of the report action type (including system messages like "marked as paid").
                           setClipboardMessageWithCleanedMentions(selection, reportIDToName);
      +                } else if (!reportAction) {
      +                    return;
                       } else if (isReportPreviewAction) {
                           const iouReportID = getIOUReportIDFromReportActionPreview(reportAction);
                           const displayMessage = getReportPreviewMessageForCopy({
    2. Add !!reportAction to copyMessage.shouldShow (src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx):
      Prevent the Copy item from rendering when reportAction is missing for REPORT_ACTION context menu types:

      --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      @@ -1048,6 +1048,7 @@ const getContextMenuActions = (
               successIcon: 'Checkmark',
               shouldShow: ({type, reportAction}) =>
                   type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
      +            !!reportAction &&
                   !isReportActionAttachment(reportAction) &&
                   !isMessageDeleted(reportAction) &&
                   !isTripPreview(reportAction) &&
    3. Refine Type Definition in ContextMenuActionPayload & Remove Unsafe Cast:
      In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:

      --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      @@ -343,7 +343,7 @@ type ContextMenuActionPayload = {
           childReportActions: OnyxEntry<ReportActions>;
           originalReportActions: OnyxEntry<ReportActions>;
      -    reportAction: ReportAction;
      +    reportAction: OnyxEntry<ReportAction>;
           transaction?: OnyxEntry<Transaction>;

      And in src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx:407:

      --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
      +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
      @@ -404,4 +404,4 @@
           originalReportActions,
           reportIDToName,
      -    // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
      -    reportAction: (reportAction ?? null) as ReportAction,
      +    reportAction: reportAction ?? null,
           reportID,

    What alternative solutions did you explore? (Optional)

    1. Only adding reportAction && to isTagModificationAction (as proposed in earlier comments):
      • Incomplete. Lines 1224–1270 immediately access reportAction.actionName across 17 other branches. Fixing line 1200 alone moves the crash to line 1224.
    2. Only adding !!reportAction to shouldShow:
      • Incomplete. If an action is removed or replaced in Onyx after the menu is opened, or if a hide/open race occurs, onPress can still be invoked with a null reportAction. Defense-in-depth in onPress is strictly required.
    3. Returning early at the very start of onPress before if (selection):
      • Rejected because copying highlighted text (selection) must continue to work even if the action reference is detached or transitioning.

    What specific scenarios should we cover in automated tests to prevent reintroduction of this issue in the future?

    In tests/unit/ContextMenuActionsCopyMessageTest.ts:

    1. Empty Selection with Null reportAction:
      • Call copyMessageAction.onPress(false, createReportActionPayload(null as unknown as ReportAction)).
      • Assert that execution completes without throwing a TypeError and that Clipboard.setString and Clipboard.setHtml are not called.
    2. Text Selection with Null reportAction:
      • Call copyMessageAction.onPress(false, createPayload('Sample highlighted text')) with reportAction: null as unknown as ReportAction.
      • Assert that Clipboard.setString / Clipboard.setHtml is called with the cleaned highlighted text.
    3. shouldShow with Null reportAction:
      • Assert that copyMessageAction.shouldShow({type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, reportAction: null}) returns false.
  13. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  14. HamzaKhanBUIC commented on Oct 22, 2026

    @HamzaKhanBUIC

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~015055563b6090af39

  15. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  16. camesenin commented on Oct 22, 2026

    @camesenin

    Proposal

    Problem

    On Web, pressing Copy in the report action context menu crashes with TypeError: Cannot read properties of null (reading 'actionName') when reportAction is null or undefined (e.g. if the action is detached, removed, or replaced in Onyx while the context menu is open). Sentry issue APP-C2W.

    Root Cause

    1. In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx, the onPress callback for reportActionContextMenu.copyMessage handles multiple action types in an if...else if chain.
    2. In lines 1100–1104, when selection is present (the user highlighted text), it safely calls setClipboardMessageWithCleanedMentions(selection, reportIDToName).
    3. However, when selection is empty:
      • Line 1200 unguardedly reads reportAction.actionName:
        } else if (isTagModificationAction(reportAction.actionName)) {
      • Furthermore, lines 1224–1282 contain over 20 consecutive branches that directly access reportAction.actionName:
        } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED) {
        ...
        } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_APPROVER) {
    4. If reportAction is nullish, evaluating these branches immediately throws Cannot read properties of null (reading 'actionName').
    5. Additionally, copyMessage's shouldShow in ContextMenuActions.tsx:1048-1053 checks !isReportActionAttachment(reportAction) and !isMessageDeleted(reportAction) without checking !!reportAction first, allowing the Copy item to show even if reportAction is nullish. And ContextMenuActionPayload types reportAction: ReportAction instead of OnyxEntry<ReportAction>, hiding nullability at the type level.

    Solution

    1. In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:

      • In reportActionContextMenu.copyMessage's onPress: after handling if (selection), immediately return early if !reportAction:
      if (selection) {
          setClipboardMessageWithCleanedMentions(selection, reportIDToName);
          return;
      }
      if (!reportAction) {
          return;
      }

      This completely protects all subsequent branches (including lines 1200 and 1224–1282) from null dereferences while preserving the ability to copy highlighted text.

    2. Guard shouldShow for copyMessage:

      --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      @@ -1048,6 +1048,7 @@ const ContextMenuActions: ContextMenuAction[] = [
               shouldShow: ({type, reportAction}) =>
                   type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
      +            !!reportAction &&
                   !isReportActionAttachment(reportAction) &&
                   !isMessageDeleted(reportAction) &&
                   !isTripPreview(reportAction) &&
    3. Accurately type reportAction as OnyxEntry<ReportAction> in ContextMenuActionPayload:

      --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
      @@ -343,7 +343,7 @@ type ContextMenuActionPayload = {
           childReportActions: OnyxEntry<ReportActions>;
           originalReportActions: OnyxEntry<ReportActions>;
      -    reportAction: ReportAction;
      +    reportAction: OnyxEntry<ReportAction>;
           transaction?: OnyxEntry<Transaction>;

      And in BaseReportActionContextMenu.tsx:407:

      -    reportAction: (reportAction ?? null) as ReportAction,
      +    reportAction: reportAction ?? null,

    Alternative Solutions Explored

    • Adding reportAction && solely to line 1200 (isTagModificationAction): Incomplete. It would still crash on line 1224 (UPDATE_FEATURE_ENABLED) and subsequent branches when none of the earlier branches match.
    • Returning early before if (selection): Rejected because copying user-highlighted text should remain functional even if reportAction is nullish or detaching.
  17. magomedta commented on Oct 22, 2026

    @magomedta

    Clarification / Proposal Refinement

    To comprehensively protect not only line 1200 (isTagModificationAction) but also all subsequent branches (lines 1224–1282, which also access reportAction.actionName === ...), the most robust approach is:

    1. In src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx:
      In copyMessage's onPress, immediately return early if !reportAction after handling user text selection:

      if (selection) {
          setClipboardMessageWithCleanedMentions(selection, reportIDToName);
          return;
      }
      
      if (!reportAction) {
          return;
      }

      This completely shields the entire remaining else if chain from null dereference while preserving the ability to copy highlighted text.

    2. In shouldShow for copyMessage, require !!reportAction:

      shouldShow: ({type, reportAction}) =>
          type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
          !!reportAction &&
          !isReportActionAttachment(reportAction) &&
          ...
    3. Accurately type reportAction: OnyxEntry<ReportAction> in ContextMenuActionPayload and adjust BaseReportActionContextMenu.tsx accordingly so TypeScript catches any future unguarded accesses at compile time.

  18. huult commented on Oct 23, 2026

    @huult
    Contributor

    } else if (isTagModificationAction(reportAction.actionName)) {

    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED) {
    Clipboard.setString(getWorkspaceFeatureEnabledMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_IS_ATTENDEE_TRACKING_ENABLED) {
    Clipboard.setString(getWorkspaceAttendeeTrackingUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_APPROVER) {
    Clipboard.setString(getDefaultApproverUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_SUBMITS_TO) {
    Clipboard.setString(getSubmitsToUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FORWARDS_TO) {
    Clipboard.setString(getForwardsToUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_OVER_LIMIT_FORWARDS_TO) {
    Clipboard.setString(getOverLimitForwardsToUpdateMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVAL_LIMIT) {
    Clipboard.setString(getApprovalLimitUpdateMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_PAY_APPROVED_REPORTS_ENABLED) {
    Clipboard.setString(getAutoPayApprovedReportsEnabledMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REQUIRE_COMPANY_CARDS_ENABLED) {
    Clipboard.setString(getRequireCompanyCardsEnabledMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_CATEGORIZE_NEW_EXPENSES) {
    Clipboard.setString(getAutoCategorizeNewExpensesMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REQUIRES_CATEGORY) {
    Clipboard.setString(getRequiresCategoryMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REQUIRES_TAG) {
    Clipboard.setString(getRequiresTagMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_GLOBAL_REIMBURSEMENTS_FX_PREFERENCE) {
    Clipboard.setString(getCurrencyConversionFeeMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REIMBURSEMENT) {
    Clipboard.setString(getAutoReimbursementMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY_TAX_RATE) {
    Clipboard.setString(getCategoryTaxRateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MCC_GROUP_CATEGORY) {
    Clipboard.setString(getMccGroupCategoryMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_NAME) {
    Clipboard.setString(getInvoiceCompanyNameUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_WEBSITE) {
    Clipboard.setString(getInvoiceCompanyWebsiteUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSER) {
    Clipboard.setString(getReimburserUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSEMENT_ENABLED) {
    Clipboard.setString(getWorkspaceReimbursementUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ACH_ACCOUNT) {
    Clipboard.setString(getUpdateACHAccountMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ADDRESS) {
    Clipboard.setString(getCompanyAddressUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAmountNoReceiptMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_ITEMIZED_RECEIPT) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAmountNoItemizedReceiptMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAmountMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AGE) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAgeMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_BILLABLE) {
    Clipboard.setString(getPolicyChangeLogDefaultBillableMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_REIMBURSABLE) {
    Clipboard.setString(getPolicyChangeLogDefaultReimbursableMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE_ENFORCED) {
    Clipboard.setString(getPolicyChangeLogDefaultTitleEnforcedMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_OWNERSHIP) {

    We added a new check using reportAction.actionName in the onPress handler without a null check. This can cause a TypeError: Cannot read properties of null (reading 'actionName') when reportAction is null and one of these branches executes.

    } else if (isTagModificationAction(reportAction.actionName)) {

    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED) {
    Clipboard.setString(getWorkspaceFeatureEnabledMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_IS_ATTENDEE_TRACKING_ENABLED) {
    Clipboard.setString(getWorkspaceAttendeeTrackingUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_APPROVER) {
    Clipboard.setString(getDefaultApproverUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_SUBMITS_TO) {
    Clipboard.setString(getSubmitsToUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FORWARDS_TO) {
    Clipboard.setString(getForwardsToUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_OVER_LIMIT_FORWARDS_TO) {
    Clipboard.setString(getOverLimitForwardsToUpdateMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVAL_LIMIT) {
    Clipboard.setString(getApprovalLimitUpdateMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_PAY_APPROVED_REPORTS_ENABLED) {
    Clipboard.setString(getAutoPayApprovedReportsEnabledMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REQUIRE_COMPANY_CARDS_ENABLED) {
    Clipboard.setString(getRequireCompanyCardsEnabledMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_CATEGORIZE_NEW_EXPENSES) {
    Clipboard.setString(getAutoCategorizeNewExpensesMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REQUIRES_CATEGORY) {
    Clipboard.setString(getRequiresCategoryMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REQUIRES_TAG) {
    Clipboard.setString(getRequiresTagMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_GLOBAL_REIMBURSEMENTS_FX_PREFERENCE) {
    Clipboard.setString(getCurrencyConversionFeeMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REIMBURSEMENT) {
    Clipboard.setString(getAutoReimbursementMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY_TAX_RATE) {
    Clipboard.setString(getCategoryTaxRateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MCC_GROUP_CATEGORY) {
    Clipboard.setString(getMccGroupCategoryMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_NAME) {
    Clipboard.setString(getInvoiceCompanyNameUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_WEBSITE) {
    Clipboard.setString(getInvoiceCompanyWebsiteUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSER) {
    Clipboard.setString(getReimburserUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSEMENT_ENABLED) {
    Clipboard.setString(getWorkspaceReimbursementUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ACH_ACCOUNT) {
    Clipboard.setString(getUpdateACHAccountMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ADDRESS) {
    Clipboard.setString(getCompanyAddressUpdateMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAmountNoReceiptMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_ITEMIZED_RECEIPT) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAmountNoItemizedReceiptMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAmountMessage(translate, reportAction, convertToDisplayString));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AGE) {
    Clipboard.setString(getPolicyChangeLogMaxExpenseAgeMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_BILLABLE) {
    Clipboard.setString(getPolicyChangeLogDefaultBillableMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_REIMBURSABLE) {
    Clipboard.setString(getPolicyChangeLogDefaultReimbursableMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE_ENFORCED) {
    Clipboard.setString(getPolicyChangeLogDefaultTitleEnforcedMessage(translate, reportAction));
    } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_OWNERSHIP) {

    We can fix this by adding an optional chaining check:

    - reportAction.actionName
    + reportAction?.actionName

    I suggest using reportAction?.actionName here, consistent with the existing null checks. There's no need to hide the Copy action when reportAction is null. We just need to handle the null case safely.

    Only add ?.

    We can go with @MelvinBot's proposal. @MelvinBot, could you please review my comment? Thanks!

    🎀 👀 🎀 c+ reviewed.

  19. melvin-bot commented on Oct 23, 2026

    @melvin-bot

    Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.

  20. MelvinBot commented on Oct 23, 2026

    @MelvinBot
    Contributor

    Agreed, huult. Adding ?. alone fixes the crash, and I'm fine dropping the other parts of my proposal (hiding Copy, changing the payload type, the Reply in thread guard, and the unit test).

    I checked the whole Copy onPress chain at ba97c03. These are the only places that read reportAction.actionName without ?.:

    Why ?. is enough
    • Every other branch either uses reportAction?.actionName or calls a helper such as isActionOfType that returns false for a missing action.
    • The other bare reads, like reportAction.actorAccountID at line 1161 and line 1305, run only after a type check passes, so they can't see null.
    • After the fix, line 1200 passes undefined to isTagModificationAction. That's safe because the function only does === comparisons (ReportActionsUtils.ts:2092).
    • With no action and no selection, the chain reaches the content and messageText fallbacks at lines 1589-1594. Both are empty, so Copy copies nothing and closes the menu without crashing. A user who selected text still gets it copied at line 1101.

    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/104746#issuecomment-6355600549) to create a draft PR that adds ?. on line 1200 and lines 1224-1282.


    view run

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 contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions