Skip to content

Duplicate "self" attendee on expenses — attendee dedup ignores accountID and formatCurrentUserToAttendee can emit an empty attendee #94057

Description

@melvin-bot

Problem

A user reported a duplicate attendee on one of their own expenses. In OldDot it renders as a bare avatar; in NewDot it shows the user "alongside themselves" — two attendee entries that resolve to the same accountID but carry different identifiers (one with email populated, one with empty email/displayName).

Example: transaction 8310454824587380428.

Root cause

Two compounding issues:

1. Attendee dedup is keyed on email (falling back to displayName), never on accountID

  • getFilteredRecentAttendees (src/libs/OptionsListUtils/index.ts) dedups with attendee.email || attendee.displayName, and cross-filters selected attendees as attendee.email ? email === attendee.email : displayName === attendee.displayName.
  • updateMoneyRequestAttendees (src/libs/actions/IOU/UpdateMoneyRequest.ts) only unionBys the recent attendees list (not the saved list), again on email || displayName.
  • accountID is never used as a dedup key anywhere.

Backend Auth (Transaction::validateAttendees in auth/lib/Transaction.cpp) strips accountID/login, persisting only email/displayName/avatarUrl, and does no per-attendee dedup (it only drops an attendee when both email and displayName are empty). So two entries for the same account with different identifiers both survive.

2. formatCurrentUserToAttendee can emit an attendee with accountID set but empty email/displayName

formatCurrentUserToAttendee (src/libs/IOUUtils.ts) is the default attendee seeded onto every new expense (wired in at MoneyRequest.ts:301). It always sets accountID: currentUser.accountID but falls everything else back to '':

const initialAttendee: Attendee = {
    email: currentUser?.login ?? '',
    login: currentUser?.login ?? '',
    displayName: currentUser.displayName ?? '',
    accountID: currentUser.accountID,   // ← always set
    ...
};

If currentUserPersonalDetails has an accountID (always present) but login/displayName aren't hydrated yet, you get {accountID: <you>, email: '', displayName: ''}.

By contrast, getReportOwnerAsAttendee bails out (return undefined) when login and displayName are both empty, so it can't produce the empty entry. formatCurrentUserToAttendee has no such guard — it only checks !currentUser.

Caveat: Auth's validateAttendees filters out attendees with empty email and empty displayName, so an empty attendee persisting on the saved transaction means it reached storage via a path that didn't re-run that filter (likely the optimistic Onyx draft, or a later edit re-adding the current user).

Suggested fix direction

  1. Guard formatCurrentUserToAttendee the same way getReportOwnerAsAttendee is guarded — don't emit an attendee when there's no login/displayName.
  2. Dedup by a stable identity that prefers accountID when present (e.g. accountID ?? email ?? displayName) in getFilteredRecentAttendees and when assembling the saved attendee list, and/or normalize the owner attendee's email to the canonical primary login. Since Auth strips accountID, the App must dedup before save (or validateAttendees should be taught to dedup on email/displayName).

Reported in Slack.

Issue OwnerCurrent Issue Owner: @truph01

Activity

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions