Skip to main content

Member Profile — feature architecture

This page is the source of truth for the Member Profile feature. The ERD, state machines, and feature table below are what the flow tests, unit tests, and implementation are derived from. When the feature set changes, change this page first, then the tests, then the code.

The Member Profile (MemberProfilePage, route /member/:memberId) is the single place to view and manage one household member: identity, traits, roles, wallet, and personal goals. It is opened from the Household tab's member tiles.

It follows the one data path: Bloc → Repository → Client facade → Service → Adapter (domain rules live in the SDK service; the bloc/repos are thin).

Feature set

Status legend: ✅ built · 🔨 to build (SDK-ready, UI-only) · 🧩 needs SDK work · 📍 by design lives elsewhere.

AreaFeatureStatusSDK surfaceNotes
IdentityDisplay nameHouseholdMember.displayNameheader
IdentityAge tagHouseholdMember.ageAsOf(now) (DOB-derived; falls back to manual age)DsTag — a DOB-only member now shows a derived age (DOB-M2)
IdentityWatch-only tagHouseholdMember.watchOnlyDsTag
IdentityAvatar colour🔨HouseholdMember.colorKeypresent but avatar uses a flat surface colour
IdentityHome-room chip🔨HouseholdMember.homePlaceId + getPlaces()bloc must load places + resolve name
IdentityBirthday (DOB)HouseholdMember.birthDate (+ ageAsOf / advisory isUnderCoppaAge)CB-15 shipped a single birthDate: DateTime? floating calendar date (NOT birthMonth/birthDay); derives a non-staling age
TraitsTrait chips + starter suggestionsupdateMember(traits:)rewhaven is ahead of the POC here
RolesRole tags (admin/helper/member)✅ (read-only)HouseholdMember.rolesdisplay only
RolesRole editing📍Members-management page (Profile → Account), per T7 P2
WalletBuckets spend/save/give + totalwalletOf(memberId)3-bucket model (POC bank dropped by design)
WalletTransfer / sort sheet🧩no transferTokens on the facade — Money-sprint SDK work
GoalsList + progress barsgetGoals(memberId:) + goalProgress(id)
GoalsAdd goalcreateGoal(...)dialog
GoalsArchive goalarchiveGoal(id)
GoalsRequest → approve lifecyclerequestGoal / approveGoalshipped as P2-G
GoalsdueLabel subtitle🔨Goal.dueLabelpopulated but never rendered
GoalsComplete state ("Reached")🔨Goal.status == complete + completeGoal(id)a complete goal looks identical to active
GoalsEdit goal🔨updateGoal(Goal)only add/archive exist today
GoalsEmoji / image thumbnail🔨Goal.emoji + Goal.imageUrlemoji is plain text; imageUrl ignored
Member mgmtAdd / remove / invite📍Members-management page, per T7 P2
EngagementAchievement badges (Wave-1 A3)AchievementsCubit — pure projection over facade readsRendered on this page; see features/achievements.md
EngagementWeekly streak section (Wave-2 A4)StreakProjection — pure projection, zero new storageRendered on this page; see features/streaks.md
EngagementChild timeline "My story" teaser + full page (Wave-2 A5)TimelineBloc / timeline repos — pure projection over ledger readsTeaser rendered on this page; full page is a separate route; see features/child-timeline.md

"Done 💯" = close the six 🔨 (UI-only) gaps. The two 🧩 items (token transfer, birthday) are tracked SDK work; the 📍 items are intentionally on the Members-management page.

ERD

Notes:

  • WALLET is 3 buckets (spend/save/give); the POC's unallocated bank bucket was removed in the rebuild. wallet.total is derived.
  • Goal progress is derived, not stored: goalProgress(id) sums the member's earn LEDGER_ENTRY rows against targetTokens. The profile shows progress / targetTokens and a DsProgressBar.
  • A member is linked to an account via the nullable authUserId (shadow → claim).

State machines

Goal lifecycle

The economy service owns these transitions and their guards (rules live in the service, not the UI). The profile surfaces the actions per status.

Guards (enforced in EconomyService, throw DomainRuleException otherwise):

  • requestGoal requires current status active.
  • approveGoal requires current status requested and a parental actingMemberId (MemberKind.isParental).
  • completeGoal requires current status active.

UI surface per status on a goal tile:

  • activeRequest (→ requested) + Edit + Archive; "Reached" when progress ≥ target.
  • requestedApprove (parental, when an approver is resolved) + "Awaiting approval" tag.
  • complete → "Reached" badge (read-only) + Archive.
  • archived → hidden from the default list (getGoals filters them).

MemberProfile bloc

Events (MemberProfileEvent): MemberProfileStarted, MemberProfileTraitToggled, MemberProfileGoalAdded, MemberProfileGoalRequested, MemberProfileGoalApproved, MemberProfileGoalArchived — plus to add: MemberProfileGoalEdited, MemberProfileGoalCompleted.

Data flow

MemberProfileBloc currently injects HouseholdRepository + WalletRepository

  • GoalsRepository. The home-room chip adds a PlacesRepository dependency (or resolves the place name from the already-provided household roster load).

SDK surface (facade methods used)

MethodUsed for
watchMembers() / updateMember(member)load member, persist traits
walletOf(memberId)wallet buckets
getGoals(memberId:) / goalProgress(goalId)goals + progress
createGoal / updateGoal / archiveGoal / restoreGoalgoal CRUD
requestGoal / approveGoal / completeGoalgoal lifecycle
getPlaces()home-room name resolution

Gap: there is no transferTokens on the facade — wallet transfer/sort is future Money-sprint SDK work, not a UI-only task.

Test plan

Derive tests from the state machines + feature table. Anchors:

Unit / blocapp/test/unit/member_profile_bloc_test.dart:

  • load: Startedready carrying member, wallet, goals, progress; member not found → loadFailure.
  • trait toggle: optimistic add, rollback + saveFailure on repo throw.
  • goal requestGoal: active → requested, reflected in state ✅; saveFailure on reject ✅.
  • goal approveGoal: requested → active with the parental acting id.
  • goal archiveGoal: removed from the visible list.
  • to add: GoalEditedupdateGoal called with the patched goal; GoalCompletedactive → complete.

Flow / goldenapp/test/flows/member_profile_test.dart:

  • profile shows identity + wallet + traits + goals ✅.
  • request goal → "Awaiting approval" tag ✅.
  • approve goal uses the parental id ✅.
  • to add: edit-goal sheet round-trips; a complete goal shows "Reached"; dueLabel renders; home-room chip renders when homePlaceId is set.

Widgetapp/test/widget/:

  • goal tile renders dueLabel, the emoji/image thumbnail, and the complete ("Reached") state by status.

Build order — closing the gaps ("done 💯")

  1. Goal display polish (widget-only): dueLabel subtitle, "Reached" state for complete, emoji/imageUrl thumbnail.
  2. Edit goal: MemberProfileGoalEdited event + handler (updateGoal) + an edit dialog reachable from the goal tile.
  3. Home-room chip: load places in the bloc, resolve homePlaceId → name, show a chip in the identity header.
  4. Avatar colour: map colorKey to the avatar in the identity header.

Deferred (need SDK first, tracked): wallet transfer/sort sheet (no facade method yet). Birthday shipped (CB-15) as a single birthDate: DateTime? floating calendar date (+ ageAsOf / advisory isUnderCoppaAge) — NOT the once-planned birthMonth/birthDay pair — and the profile age tag is now DOB-derived (DOB-M2). By design elsewhere: role editing and member add/remove live on the Members-management page (Profile → Account).