================================================================================
    TAG App - Frontend Implementation Plan: Conference Rooms & Ephemeral
                        Anonymous Space Chat
================================================================================

1. SCOPE OF THIS PLAN
   Two backend features are complete and live-tested at
   https://tag.nsamaandcompany.com/tag_api — this plan is the frontend
   (Flutter) build-out for both:

   A. Group audio/video rooms (conference calls, up to 5 people) — see
      documentation/room.txt for the full API contract.
   B. Anonymous ephemeral 1:1 chat, launched by tapping a member's tag code
      inside a TagSpace — see documentation/ephemeral_space_chat.txt for
      the full API contract.

   Both are ADDITIVE to the existing 1:1 call flow (flutter_webrtc +
   Firestore, documentation/directcall.txt) — nothing in that flow changes.

2. NEW DEPENDENCIES
   - Metered.ca's Flutter/client SDK (for feature A only — do NOT reuse
     flutter_webrtc for rooms; Metered's SDK consumes the metered_token
     directly and manages the SFU connection itself).
   - No new dependency for feature B — it reuses cloud_firestore (already
     in the app for direct-call signaling) against a new collection.

3. PHASE 1 — GROUP ROOMS (feature A)

   3.1 Screens/UI
     - "Start a room" entry point (e.g. from a contact list or a space):
       title field, room type toggle (audio/video), max 5 participants
       (fixed — don't expose this as user-editable, it's a hard server cap).
     - In-room screen: Metered SDK's built-in video grid/audio-only view,
       plus TAG's own controls overlay (mute, camera toggle, leave, invite).
     - Invite sheet: contact picker (multi-select up to remaining capacity)
       + a "copy/share room code" action (the room_id IS the join code).
     - Incoming-invite handling: push of type room_invite should surface a
       tappable notification/in-app banner — tapping it calls join()
       directly (see 3.2), there is no separate accept/decline endpoint.

   3.2 Call sequence (client-side)
     1. Host: POST /rooms {title, max_participants, room_type} -> room_id.
     2. Host: POST /rooms/{room_id}/join -> metered_token -> hand token to
        Metered SDK, connect.
     3. Host: POST /rooms/{room_id}/invite {user_ids: [...]} and/or share
        room_id as a code.
     4. Invitee (via push tap or manual code entry): POST
        /rooms/{room_id}/join -> metered_token -> connect via Metered SDK.
     5. Any participant leaving: POST /rooms/{room_id}/leave, then tear
        down the Metered SDK connection locally.

   3.3 Error handling (map directly to UI states, don't just show raw text)
     - 403 on join ("Room is full") -> show a blocking dialog, don't retry.
     - 404 on join ("Room not found or already ended") -> if reached via a
       shared code, tell the user the code is invalid/expired.
     - 502 on create/join -> transient provider failure — show "couldn't
       start the call, try again" and allow retry (safe: nothing is
       persisted locally on a 502 per the API contract).

   3.4 Testing checklist
     - Create + join as host, confirm Metered SDK connects with your own
       audio/video.
     - Invite a second real device, confirm push arrives, join, confirm
       both sides see/hear each other.
     - Hit the 5-participant cap with a 6th device -> confirm the 403 UI.
     - Force-kill a non-host participant's app without leaving -> confirm
       the room stays alive for the rest (server-side sweep only ends a
       room when EVERYONE is gone).
     - Host leaves -> confirm all other participants are kicked out of the
       call UI (room status flips to ended; poll or handle via Metered's
       own participant-left signal, whichever the SDK exposes first).

4. PHASE 2 — ANONYMOUS EPHEMERAL CHAT (feature B)

   4.1 Screens/UI
     - Inside a TagSpace's existing chat, tapping a member's tag code
       (wherever it's currently rendered) should now open a new lightweight
       "anonymous chat" screen — NOT the existing full-identity DM screen
       that GET /members/{code} + POST /conversations produces today.
       Both entry points can coexist; this is a new, separate action.
     - Ephemeral chat screen renders participants by tag code only (yours
       and theirs) — no name, no avatar, until a "contact_share" message
       appears in the Firestore stream (see 4.3).
     - A "Share my contact" button/action that writes the contact_share
       message (see 4.3) — this is the ONLY way real identity ever appears
       in this screen.
     - A "Keep talking" action that appears once a contact_share has been
       received FROM THE OTHER SIDE (you need their real user_id to start
       a real conversation) — wired to 4.4's graduation flow.
     - Closing the screen (back button, explicit "end chat") must delete
       the Firestore document — see 4.3, this is a hard requirement, not
       an optimization.
     - Invites panel: a small badge/icon inside the space's chat screen
       (e.g. a bell icon in the app bar) showing a count and a dropdown/
       sheet listing pending invites — each entry shows only from_tag_code
       and a relative time ("2m ago"). This is the panel from 4.2 below —
       it's the primary way invites are discovered now; the push is just a
       courtesy nudge for when the app is already open/foregrounded.

   4.2 Start sequence (client-side)
     1. Tap a tag code -> POST /tag-spaces/{uuid}/members/{code}/ephemeral-chat.
     2. On 200: open the ephemeral chat screen using the returned chat_id,
        start listening to ephemeral_chats/{chat_id} in Firestore.
     3. On 404 "This person is not in the space at the moment.": show that
        message directly, don't open the chat screen.
     4. On 400 (self) / 403 (blocked): show a generic inline error, these
        shouldn't normally be reachable from the UI (don't render a tap
        target for your own code; blocked members shouldn't be visible in
        the space's presence in the first place if that's already enforced
        elsewhere — confirm with backend if not).
     5. Target side, panel-driven (primary path): on opening a space's chat
        screen, call GET /tag-spaces/{uuid}/ephemeral-chat-invites and
        render the results in the invites panel (4.1). Tapping an entry
        opens the ephemeral chat screen using its chat_id (same as step 2),
        THEN calls DELETE .../ephemeral-chat-invites/{chatId} to remove it
        from the panel. A manual "dismiss" affordance on an entry (without
        opening the chat) calls the same DELETE without opening anything.
        Poll this endpoint (e.g. every 30-60s while the space chat screen
        is open) or refetch on screen focus — there's no Firestore listener
        for the invite list itself, only for the chat content once opened.
     6. Target side, push-driven (secondary/courtesy path): on push type
        ephemeral_chat_invite while foregrounded, show an in-app banner;
        tapping it behaves identically to tapping a panel entry (open chat,
        then DELETE the invite). If the push arrives while the panel is
        already open, just refetch the list rather than reconciling in
        place.

   4.3 Firestore usage (client-owned, see ephemeral_space_chat.txt §4 for
       the exact schema)
     - Listen to ephemeral_chats/{chatId}/messages ordered by sent_at.
     - Sending a message: write {sender_uid: myUid, body, sent_at,
       type: "text"} to the subcollection — no backend call.
     - "Share my contact": write one message with
       type: "contact_share", plus the sender's OWN already-known local
       profile fields (full_name, avatar_url, public_tag_id) — never fetch
       or send the OTHER user's identity, you don't have it.
     - Rendering rule: for each sender_uid seen in the stream, default to
       showing their tag code (yours: known locally; theirs: known from
       the tap or the push payload's from_tag_code). Only switch to
       showing real name/avatar for a sender_uid once a contact_share
       message from that sender_uid has been seen.
     - Closing: delete all documents in the messages subcollection, then
       delete the ephemeral_chats/{chatId} document itself. Firestore does
       not cascade-delete subcollections automatically — do the messages
       first or use a batched delete.

   4.4 Graduating to a real conversation
     - Only actionable once you've received a contact_share message FROM
       THE OTHER SIDE. The contact_share message itself is just a
       "permission granted" signal — it does not need to carry a user_id.
       The real user_id was already delivered when the chat opened: the
       ephemeral_chats/{chatId} document (per §4.3 / ephemeral_space_chat
       .txt §4) carries participant_a_uid and participant_b_uid. Whichever
       one isn't your own uid is the other participant's real user_id —
       just don't use it for anything (including rendering) until their
       contact_share message has actually arrived.
     - Once contact_share is received: POST /conversations
       {recipient_id: <the other participant's uid from the doc>,
       space_uuid} -> tags the new conversation as source_type='space_dm'.
       Navigate to the normal, existing conversation screen from here on
       — the ephemeral chat can now be closed (per 4.3).

   4.5 Testing checklist
     - Two devices, both active in the same space, tap each other's tag —
       confirm both screens show tag codes only, no names.
     - One side shares contact — confirm ONLY that side's name/avatar now
       renders, the other stays anonymous.
     - Close the chat on one device — confirm the Firestore doc is gone
       and the other device's screen reflects the chat ending.
     - Tap a tag belonging to someone who has left the space — confirm the
       404 message renders correctly instead of opening a broken chat.
     - After a mutual contact_share, use "Keep talking" — confirm a real,
       persistent conversation opens and (ask backend to confirm via DB)
       is tagged source_type='space_dm'.
     - Tap another user's tag, do NOT open the resulting chat on the
       target's device — background/kill the app, reopen the space chat
       screen later, confirm the invite still appears in the panel.
     - Dismiss a panel entry without opening it — confirm it disappears
       from the panel and does not reappear on refetch.
     - Leave an invite untouched past 10 minutes — confirm it no longer
       appears in the panel after the next server-side sweep.

5. SUGGESTED BUILD ORDER
   1. Phase 1 (rooms) first — it's the more isolated, self-contained piece
      (own screen, own SDK, clear success/fail states).
   2. Phase 2 (ephemeral chat) — no blocking contract questions; §4.4
      already resolves how the graduation step gets the other side's real
      user_id (it's in the document fields, not the contact_share message).

================================================================================
                         End of Documentation
================================================================================
