Skip to content

Friends feature: hardening & completion (plans A-D)#67

Merged
Jshewmaker merged 84 commits into
mainfrom
feat/friends-hardening
Jul 8, 2026
Merged

Friends feature: hardening & completion (plans A-D)#67
Jshewmaker merged 84 commits into
mainfrom
feat/friends-hardening

Conversation

@Jshewmaker

Copy link
Copy Markdown
Owner

Summary

Completes and hardens the friends feature per the approved design (spec, plan index + deploy gates, as-shipped README). Built in four independently-reviewed plans:

  • A — Backend foundation: Cloud Functions scaffold + emulator; versioned firestore.rules; PIN hashes moved to owner-only users/{uid}/private/credentials (salted, lazily migrated); rate-limited friends-only validatePin callable (5 fails → 15-min lockout).
  • B — Legacy gate & game sync: onboarding gate on UserProfileModel.isComplete (legacy users complete name+PIN in the pre-filled wizard); server-side game fan-out (onGameCreated trigger) with cross-user matches writes denied by rules; game-over firebaseId overwrite bug fixed ("I'm not playing" option, linked badges, disowned-slot unlink).
  • C — Social graph & blocking: deterministic friend-request ids with a full rules lifecycle matrix (block-gated creates, pending-gated friendship edges, declined docs as permanent suppression markers); full blocking (block-aware searchByFriendCode callable, blocked-users screen); trigger injection hardening (games create requires own hostId).
  • D — Profile & cleanup: profile page rebuilt on UserProfileModel with PIN change (no old-PIN prompt) and friend-code copy; onUserDeleted cleanup trigger (+ required COLLECTION_GROUP indexes); includeIfNull: false across repository models; save-failure UX on game over; l10n sweep (en+es).

⚠️ Deploy gates (read before releasing)

Deployment is manual and ordered — full details in the INDEX:

  1. Export prod indexes & rules from the console and diff (both were never versioned; the CLI will offer deletions).
  2. firebase deploy --only firestore:indexes → wait READY (the emulator cannot validate index requirements).
  3. firebase deploy --only functions
  4. firebase deploy --only firestore:rules
  5. App release — consider pairing with force-upgrade: old clients break on migrated PINs and can no longer write cross-user match copies (named, accepted breakage).

Testing

  • Root: 115/115 · package: 49/49 · functions unit: 8/8 · rules/emulator: 55/55 (5 suites) · flutter analyze: 165 (net improved vs pre-branch baseline)
  • Every task was TDD'd with an independent per-task review; each plan closed with a whole-branch review + fix wave (three production-breaking issues were caught and fixed by those reviews: rules-denied client guard reads, a blocked-user edge-write bypass, and missing collection-group indexes).

Notes

  • CI will be red for pre-existing reasons (repo-wide formatting + spell-check gate the analyze/test jobs on main today) — not a branch regression; tracked as a follow-up.
  • Accepted residuals and open follow-ups (users-list wire read, block-status wire leak, etc.) are documented in the INDEX and the feature README's security model.

🤖 Generated with Claude Code

Jshewmaker and others added 30 commits July 3, 2026 15:14
Reviewed docs/friends_feature_plan.md against main: most of it is already
implemented. This spec covers the remaining work — legacy name+PIN
enforcement via the onboarding gate, Cloud Functions backend (PIN
validation with rate limiting, game fan-out, social-graph writes),
versioned Firestore rules, full blocking, profile page completion, and
the game-over firebaseId overwrite fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ns only where required

Per review feedback: keep Firebase's direct read/write/stream strengths
for the social graph (secured by rules + deterministic request doc IDs)
and setPin (client-side salted hash to an owner-only doc). Cloud
Functions shrink to the four jobs rules cannot do: validatePin,
searchByFriendCode block-hiding, onGameCreated fan-out, onUserDeleted
cleanup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…malformed targetUserId

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yer customization

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reorders _onSubmitted so setPin runs before updateUserProfile. Previously
a setPin failure after a successful profile save left the user durably
marked onboarded-with-PIN with no credentials doc, permanently locking
them out of validatePin with nothing to re-prompt. Adds tests for the
untouched-pin keep path and pins the new ordering with a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion, PIN dialog reset, deploy-order docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also serializes the test:rules jest run (--runInBand): parallel workers
racing against the shared Firestore emulator caused beforeEach's
recursiveDelete in one suite to wipe data another suite's transaction
was mid-read on, now that a third rules suite runs alongside it.
Jshewmaker and others added 29 commits July 5, 2026 13:11
… l10n

- Add firestore.indexes.json with COLLECTION_GROUP field overrides for
  friendList/blocks.userId; onUserDeleted's collection-group queries
  otherwise throw FAILED_PRECONDITION in production even though the
  emulator can't catch it (indexes unenforced there). Deploy order now
  starts with firestore:indexes.
- Guard ProfileBloc._onSubmitted against saving a present-but-invalid
  username, which previously flipped isComplete false and bounced users
  into onboarding.
- Localize the profile page's field/button labels (8 new keys, en+es)
  and drop the dead shareFriendCodeTooltip key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… peer dep

Cloud Build's clean npm install enforces peer dependency resolution
strictly and failed with ERESOLVE (rules-unit-testing@4.0.1 requires
firebase@^11, we had ^10.12.0 pinned). Dev-tooling only — the deployed
function code never imports the client firebase SDK. Verified: build
clean, unit suite 8/8, full emulator rules suite 55/55.
Replaces the tile-list friend picker with a searchable dropdown that
also lists the account owner, and fixes the underlying rehydration bug
where reopening an already-linked seat could silently drop or reassign
the link. Also scopes two PIN dialog bug fixes (loading state, small-
device keyboard overlap) surfaced while touching the same code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…screens

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…alog

The previous commit's small-screen test had been narrowed to a same-size,
no-keyboard scrollable-flag check that passed trivially and no longer
exercised the actual bug (Verify button unreachable once the keyboard
covers the PIN dialog on a small screen). Restore the brief's original
375x667 + FakeViewPadding(bottom: 300) keyboard-inset scenario.

Reordered the keyboard-inset simulation to happen after the dialog opens
(matching the real sequence: dialog opens, user focuses the PIN field,
then the OS keyboard slides in) rather than before the friend tile is
even tapped, which was causing the tap to miss its target.

CustomizePlayerPage's two-panel layout is a pre-existing landscape-style
design that already overflows at phone widths in several unrelated
widgets (TrackingPreview's two Rows, _FriendSection's header Row,
CommanderSearchBar, and _FriendLinkRow once linked) - out of scope for
this task, flagged separately for a follow-up fix. Added
_expectOnlyKnownOverflow to drain and fingerprint exactly those known,
pre-existing RenderFlex-overflow exceptions via
WidgetTester.takeException(), so the test stays honest: it still fails
loudly on any different, unexpected exception, while not being blocked
by a bug this task doesn't own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nctions/FirebaseAuth

Xcode 26.4's Swift 6.3 miscompiles async-let teardown in optimized builds,
corrupting the Concurrency runtime's task-local stack inside HTTPSCallable.call()
and any FirebaseAuth call — this was the real cause of the on-device
friend-search crash, not the earlier-suspected IAM invoker gap. Forces
SWIFT_OPTIMIZATION_LEVEL=-Onone on the FirebaseFunctions and FirebaseAuth pods
until firebase-ios-sdk ships the upstream fix (firebase/firebase-ios-sdk#15974).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…riend codes

Adds a block-aware searchByUsername callable so the search box now finds
people by name (auto-detecting friend-code-shaped input to route to the
existing exact-match search instead) — usernames aren't unique, so friend
code remains the precise way to add a specific person.

Fixes the confusion between username and friend code on the profile page:
friend requests were denormalizing the Firebase Auth display name instead
of the actual profile username, so a request could show a name that didn't
match anything on the sender's own profile. SearchBloc now looks up the
sender's real profile before creating a request, and friend code is shown
consistently on every card where another user's identity appears (search
results, friends list, friend requests, blocked users) since it's the only
field that's actually guaranteed unique.

Also drops the "YETI-" prefix from generated friend codes in favor of a
plain random 8-character code — the prefix carried no information (every
code had the same one), and a username-derived alternative was rejected
since usernames are editable and would make the code go stale on rename.
Existing users keep their already-issued YETI-XXXX codes; the search
auto-detect and callable both still recognize the legacy shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p reachability

A re-reviewer empirically verified (isolated worktree, scrollable:true
stripped from production code to reproduce the genuine pre-fix bug) that
the previous small-screen test's full tap-through-to-dismissal flow still
passed against the broken code: at Size(375, 667) with a 300px keyboard
inset, the dialog's Material card fits comfortably either way in the
widget-test harness, so tester.tap()/hit-testing never actually
discriminated fixed-vs-broken at these parameters. Flutter's widget-test
harness doesn't faithfully reproduce real on-device keyboard occlusion for
button hit-testing here.

Scoped the test down to what it can actually prove: dialog.scrollable ==
true is present once the dialog is open at a small screen size with a
keyboard inset applied. Kept the small screen + FakeViewPadding setup as
context for why scrollable matters, and a code comment explaining the
scope decision and pointing to Task 6's manual on-device verification
step for real reachability. Trimmed _expectOnlyKnownOverflow's usage to
only the pre-existing overflow widgets that still fire in this shorter
flow (_FriendLinkRow no longer applies, since the friend link no longer
happens in this test).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ed dropdown

Replaces _FriendSection's tile list + Clear button with a single
DropdownMenu<String?> (Not linked / Me / each friend, alphabetical,
type-to-search). _FriendTile is deleted.

Also sets requestFocusOnTap: true on the DropdownMenu — without it,
DropdownMenu.canRequestFocus() defaults to false on iOS/Android (this
app's primary platforms), which makes the internal TextField readOnly
and silently disables type-to-search on a real device.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…into the name field

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on save

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lidation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ations

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Jshewmaker Jshewmaker merged commit 56ce50e into main Jul 8, 2026
0 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant