Skip to content

feat(family): Manage Family (part 2: blocs, navigation, notifications) - #542

Open
plusmobileapps wants to merge 10 commits into
mainfrom
claude/pr-remaining-items-404470
Open

plusmobileapps wants to merge 10 commits into
mainfrom
claude/pr-remaining-items-404470

Conversation

@plusmobileapps

Copy link
Copy Markdown
Collaborator

Summary

Completes Manage Family, picking up the checklist left in #541. That PR landed the schema, the data layer and the screen contracts; nothing was wired into the app. This wires it all up.

Remaining items from #541

  • client/family/manage/impl: bloc and view-model implementations with unit tests.
  • More tab: a Manage Family row, shown behind the flag in every auth state.
  • Root navigation: open Manage Family when signed in. Otherwise open sign-in first, then land on Manage Family once it succeeds.
  • Notifications: show family invites with Accept and Decline.
  • Snapshot tests (previews, screenshot-test wrappers, reference PNGs).
  • impl-robots and a flow test through runRootBlocTest.
  • Apply the migration (supabase db push) — left to you, see below.
  • Follow-up: invite emails for family invites through send-invite-email.

Notable decisions

The list reloads on resume, not in init. FamilyListBlocImpl refreshes via lifecycle.doOnResume, so a family renamed, left or deleted on the detail screen is reflected on the way back without the navigation bloc having to poke the list. Only the first load shows a spinner; a later refresh that fails keeps the list already on screen rather than swapping it for an error.

Per-row actions are derived, not hand-set. FamilyDetailViewModel builds every MemberItem through FamilyPermissions, so the UI can only offer what the server would accept. The same helper generates the per-role snapshot previews, so the owner/admin/member references can't drift from the rules.

Manage Family is offered in every auth state. Unlike the Notifications row (signed-in, non-anonymous only), this one shows whenever the flag is on. It's the feature's only entry point, so hiding it from a signed-out user would leave them nothing to tap. RootBlocImpl remembers the destination, opens sign-in, and lands them on Manage Family once auth succeeds — at the credentials screen or at the OTP screen. Backing out of that sign-in forgets the destination, so an unrelated sign-in later doesn't strand them somewhere they didn't ask for. An anonymous guest counts as signed out here: family membership is keyed on an account email, which they don't have.

Notification sources are now caught independently. Adding families as a third source meant a single runCatching around the merge would let one unreachable source blank the others. Each is caught on its own now.

The family invite email trigger fires on INSERT or UPDATE. Grocery and recipe-book invites are plain inserts; invite_family_member instead flips a previously rejected row back to pending. The trigger guards on the row having just become pending, so accepting an invite or changing a role doesn't re-send the mail. The edge function's per-kind parent table and wording moved into one lookup, so a fourth kind is a single entry.

Still needed before this can ship

  • supabase db push for 20260916_add_families.sql (from feat(family): Manage Family (part 1: schema, data layer, screens) #541) and 20260921_family_invite_email.sql. I left these unapplied — prod's migration history is unreconciled with the CLI, so that's a call for you to make.
  • supabase functions deploy send-invite-email.
  • Flip manage_family above rollout_percent = 0 when you're ready.

Test plan

  • ./gradlew test — green, including 26 new view-model tests, 8 root-navigation tests and 6 flow tests.
  • ./gradlew :client:ui:screenshot-test:validateDebugScreenshotTest — green; 12 new Manage Family references plus refreshed Notifications and More-tab ones, all visually inspected.
  • ./gradlew :client:composeApp:compileDebugKotlinAndroid :client:composeApp:compileKotlinJvm :client:composeApp:compileKotlinIosSimulatorArm64
  • Apply both migrations to a dev project and exercise invite → email → accept/decline end to end as owner, admin and member.

🤖 Generated with Claude Code

plusmobileapps and others added 10 commits September 21, 2026 17:27
Adds the client/family/manage/impl module and the first of its three BLoCs:
the list of families the user belongs to, with the "New family" dialog.

The list loads on every resume rather than in init, so a family renamed, left
or deleted on the detail screen is reflected on the way back. Only the first
load shows a spinner — a later refresh that fails keeps the list already on
screen rather than replacing it with an error.

Creating a family opens it straight away: a brand-new family has nobody in it,
so inviting is the only thing left to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One family's members and invites, with the actions the viewer's role allows.

Every per-row action is derived from FamilyPermissions rather than hand-set, so
the UI only offers what the server would accept: an owner manages admins and
deletes, an admin invites and removes members and can leave, a member can only
leave. The server stays the authority — this just avoids offering a button that
would fail.

Leaving and deleting take the family away from the viewer, so they close the
screen instead of reloading it. Failures that have no field to attach to (role
changes, rename, leave, delete) surface as an app-wide toast; a rejected invite
stays inline under the email field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The stack behind the Manage Family entry: the family list, pushing a detail
screen per family.

Back and Closed both pop to the list. Closed needs no extra signal to refresh
it — the list reloads on resume, which popping triggers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the Manage Family row, shown while the manage_family flag is on.

Unlike Notifications, the row appears in every auth state rather than only for
a signed-in, non-anonymous user: it is the feature's only entry point, and
hiding it from a signed-out user would leave them nothing to tap. Root
navigation takes them through sign-in on the way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Routes the More tab's Manage Family row into the root stack.

A signed-in user goes straight there. Anyone else — signed out, or on an
anonymous guest session — is sent to sign-in first and lands on Manage Family
once it succeeds, whether that finished at the credentials screen or at the OTP
screen. A guest counts as signed out here because family membership is keyed on
an account email, which they don't have.

Backing out of that sign-in forgets the destination, so an unrelated sign-in
later doesn't strand the user on a screen they no longer asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds AppNotification.FamilyInvite and merges pending family invites into the
same stream that already backs the Notifications screen and the More-tab badge.

Each one-shot source is now caught on its own, so a family fetch that fails
while the user is offline no longer blanks the grocery and recipe-book invites
alongside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Previews for the family list and detail screens, wrappers in screenshot-test,
and the recorded references.

The detail previews are generated per role through FamilyPermissions — the
viewer is whichever accepted row holds that role — so the owner, admin and
member references show exactly the actions each of them really gets rather than
a hand-picked set that could drift from the rules.

Also refreshes the Notifications references for the new family invite card and
adds one for the flagged Manage Family row in the More tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the family list and detail robots, a Manage Family row on MoreRobot, and a
flow test through runRootBlocTest covering: the row hidden while the flag is
off, opening the list from the More tab, opening a family, a member seeing
neither Delete nor the invite form, creating a first family landing on its
detail screen, and a signed-out user being sent to sign-in instead.

Families are online-only, so TestFamilyRepository replaces the production
repository in the test graph — without it every Manage Family screen would
render its offline error state. FakeFamilyRepository enforces the same
permission rules the server does, so the flows behave realistically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends the existing invite-email trigger to families and teaches the edge
function 'family' as a third kind.

Two things differ from the other kinds. A family invite is created by the
invite_family_member RPC, which inserts a new row or flips a previously
rejected one back to pending — so the trigger fires on INSERT OR UPDATE OF
status and, on update, only when the row has just become pending, otherwise
accepting an invite or changing a role would re-send the email. And the copy
reads "invited you to join the family" rather than "to collaborate on", with a
matching heading.

The per-kind parent table and wording move into one lookup rather than a pair
of ternaries, so a fourth kind is a single entry.

Needs `supabase functions deploy send-invite-email` alongside the migration.
Operator setup (the project_url / invite_hook_secret Vault secrets) is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`a_signed_out_user_is_sent_through_sign_in_first` passed on JVM but failed on
iOS with "LocalInteropContainer not provided": tapping the row composes the
auth screen, whose PlusAutofillTextField.ios renders a UIKitView, and
runComposeUiTest provides no interop container. No other common UI test opens
that screen, so nothing had hit this before.

The test now stops at asserting the row is offered while signed out, which is
the part this layer is actually good at proving. The routing it used to reach
for — sign-in first, landing on Manage Family once it succeeds, and the
anonymous and abandoned-flow variants — is already covered by four RootBlocTest
cases that drive the outputs directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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