Skip to main content

Member Profile — requirements

Epic. MVP-1 requirements breakdown for the Member Profile surface (see the feature architecture). Each FR is sized to become one or a few user stories. Most of this surface is already built by the P1/P2 domain-gaps work — statuses reflect that. Six UI-only 🔨 gaps close the "done 💯" milestone.

TypeFunctional
Layer (build approach)Frontend — design_system atoms + app bloc/page + flow-test
RICER 8 × I 2 × C 100% / E 2 = 8.0 · Tier MVP-1
KPI (summary)Goals created/member → token-fade
Traces tofeature member-profile · C4 components
Depends onMoney & Envelopes (goal envelopes) · Personas/authz

Success criteria (definitive KPI)

Success = ≥ 50% of non-watch-only members have ≥ 1 active goal by end of week 2 AND ≥ 20% of goals created in weeks 1–4 reach goal_reached status by week 8 post-activation.

  • Metric A (adoption): % of non-watch-only members with ≥ 1 active goal at day 14.
  • Target A: ≥ 50% of members across activated households.
  • Metric B (completion rate): goal_reached events / goal_created events, per cohort-week.
  • Target B: ≥ 20% of goals created in weeks 1–4 reach complete by week 8.
  • Window: both targets measured within 8 weeks of household activation (activation = first chore_completed approved in the household).
  • Token-fade thesis tie: a member with an active goal has an intrinsic reason to earn beyond the raw token value. If median effectiveTokens per completion trends down over 8 weeks without a completion-frequency drop among goal-holding members, the thesis holds. A goal_reached rate that climbs as token values fall is the strongest confirmation signal.
  • Baseline: no install base — treat 50% / 20% as the launch hypothesis. Re-baseline from the first 4-week cohort before adjusting the token schedule.

Analytics — events to record

All member references are opaque/hashed; no minor PII (see COPPA). Emitted through the consent-gated analytics substrate.

EventWhenKey propertiesFeeds
profile_viewedMemberProfilePage openedisOwnProfile, watchOnly, goalCount, activeGoalCountengagement denominator; adoption funnel
goal_createdcreateGoal succeedsscope (member|family), targetTokens, hasEmoji, memberRefKPI-A numerator; goal adoption
goal_requestedrequestGoal succeedsgoalId (opaque), memberReflifecycle funnel
goal_approvedapproveGoal succeedsgoalId (opaque), approverRefapprove-rate; funnel
goal_reachedcompleteGoal succeedsgoalId (opaque), targetTokens, daysToComplete, memberRefKPI-B numerator; token-fade signal
funds_moved_to_goalwallet transfer allocates to a goal envelopefromBucket (spend|save|give), amount, memberRefgoal-funding behaviour (deferred — no facade method yet)

profile_viewed is the session-level denominator for goal engagement. goal_created and goal_reached are the two load-bearing KPI events — both ship with the surface, not in a follow-up.

Scope

Member Profile (MemberProfilePage, route /member/:memberId) is the single surface for viewing and managing one household member: identity, traits, roles, personal wallet, and personal goals. Opened from the Household tab's member tiles. MVP-1 delivers the complete identity header, trait management, wallet display, and the full goal lifecycle (add / request / approve / edit / complete / archive). Six UI-only gaps close the surface to "done 💯". Token transfer and birthday fields are deferred (SDK work); role editing and member management live by design on the Members-management page.

Functional requirements

FR-PROFILE-1 — Identity header (name, age, watch-only)

Priority: P1 · Status: ✅ built Statement. As a household member, I see the member's display name, age tag, and watch-only indicator in the profile header so I know at a glance who this profile belongs to and whether they act or are only tracked. Acceptance

  • Given a member with displayName, age, and watchOnly = true When the profile loads Then all three render in the header (displayName prominent, DsTag for age, DsTag(watchOnly) visible).
  • Given watchOnly = false When the profile loads Then no watch-only tag appears.

FR-PROFILE-2 — Avatar colour from colorKey

Priority: P2 · Status: 🔨 to build (UI-only; colorKey SDK-present) Statement. As a member, my avatar shows my assigned colour so I am visually distinct across all household surfaces. AcceptanceGiven HouseholdMember.colorKey is non-null When the profile renders Then the avatar surface maps colorKey to the design-system token palette. Null colorKey → deterministic fallback colour (e.g. hash of member id).

FR-PROFILE-3 — Home-room chip

Priority: P2 · Status: 🔨 to build (SDK-ready: homePlaceId + getPlaces()) Statement. As a household member, I see the member's home room in the identity header so I know their primary location context. AcceptanceGiven homePlaceId is set When the profile loads (bloc fetches getPlaces() or resolves from the roster) Then a chip displays the room name. Given homePlaceId = null Then no chip renders. Anchored by the home-room flow test (to add per feature architecture).

FR-PROFILE-4 — Neurodiversity trait chips and starter suggestions

Priority: P1 · Status: ✅ built (updateMember(traits:)) Statement. As an admin, I can view and toggle a member's neurodiversity traits (ADHD, sensory, dyslexia, etc.) so the app can adapt its presentation to their profile. Acceptance

  • Given a member with traits [adhd, sensory] When the trait section renders Then both chips show as selected.
  • Toggling a chip calls updateMember(traits:) optimistically; rollback + saveFailure snackbar on repository throw.
  • Starter-suggestion chips appear for trait-less members.

FR-PROFILE-5 — Role tags (read-only display)

Priority: P1 · Status: ✅ built (read-only; role editing lives on Members-management) Statement. As a member, I see my household roles (admin / helper / member) so I understand my permissions in the app. AcceptanceGiven HouseholdMember.roles = {admin, helper} When the profile renders Then both role tags display as DsTag chips. No edit affordance on this surface.

FR-PROFILE-6 — Personal wallet buckets (spend / save / give)

Priority: P1 · Status: ✅ built (walletOf(memberId)) Statement. As a member, I see my token wallet split across spend, save, and give buckets so I know my balance and how my tokens are allocated. AcceptanceGiven wallet.spend = 40, .save = 10, .give = 5 When the wallet section renders Then all three buckets and the derived total (55) display. Watch-only members see the wallet read-only (no transfer affordance in MVP-1).

FR-PROFILE-7 — Goals list with progress bars

Priority: P1 · Status: ✅ built (getGoals(memberId:) + goalProgress(id)) Statement. As a member, I see my active goals with a progress bar showing tokens earned toward each target so I can track how close I am. AcceptanceGiven a goal with targetTokens = 100 and earned progress = 60 When the goals section renders Then a DsProgressBar shows 60% and the fraction (60 / 100) is visible. Archived goals are hidden from the default list.

FR-PROFILE-8 — Add and archive goal

Priority: P1 · Status: ✅ built (createGoal / archiveGoal) Statement. As a member, I can add a personal goal with a name and token target, and archive a goal I no longer want tracked. Acceptance

  • Given I tap "Add goal" When I complete the dialog Then createGoal(...) is called and the goal appears in the list with a 0 / target bar.
  • Given an active goal When I archive it Then archiveGoal(id) is called and the goal disappears from the visible list.

FR-PROFILE-9 — Goal request → approve lifecycle

Priority: P1 · Status: ✅ built (commit 7d6e032) Statement. As a member, I can request a goal for parental approval; as an admin I can approve it so that both agency and oversight are respected. Acceptance

  • Given an active goal When a member taps "Request" Then requestGoal is called, status → requested, and the tile shows "Awaiting approval."
  • Given a requested goal and an admin viewer When they tap "Approve" Then approveGoal(actingMemberId: adminId) is called; status → active.
  • Non-parental actingMemberIdDomainRuleException surfaced as snackbar.

FR-PROFILE-10 — Goal complete state ("Reached") and dueLabel

Priority: P1 · Status: 🔨 to build (SDK-ready: completeGoal, Goal.dueLabel) Statement. As a member, when progress reaches the target I see a "Reached" badge and can mark the goal complete; the optional due-date label also renders. Acceptance

  • Given goal.status == complete When the tile renders Then a "Reached" badge appears and no earn-action affordances are shown.
  • Given goal.dueLabel is non-null When the tile renders Then the label renders as a subtitle below the goal name.
  • MemberProfileGoalCompleted event → completeGoal(id) → state patched to complete.

FR-PROFILE-11 — Edit goal

Priority: P2 · Status: 🔨 to build (SDK-ready: updateGoal(Goal)) Statement. As a member, I can edit a goal's name, token target, emoji, or due label so I can adjust it as circumstances change. AcceptanceGiven an active goal When I open the edit sheet and save Then updateGoal(patchedGoal) is called and the tile updates optimistically. saveFailure rolls back and shows a snackbar. requested and complete goals are read-only (no edit affordance).

FR-PROFILE-12 — Goal emoji / image thumbnail

Priority: P2 · Status: 🔨 to build (Goal.emoji + Goal.imageUrl SDK-present) Statement. As a member, a goal can carry an emoji or image thumbnail so it feels personal and motivating. AcceptanceGiven Goal.emoji = '🎮' When the tile renders Then the emoji displays as the goal icon. Given Goal.imageUrl is set Then it renders as a thumbnail; emoji takes precedence when both are present. Null → generic goal icon.

Architecture considerations

  • One data pathMemberProfileBloc → {HouseholdRepository, WalletRepository, GoalsRepository, PlacesRepository} → Client facade → {HouseholdService, EconomyService} → StoragePort → Drift / Supabase. No direct I/O in the bloc or page.
  • Personal wallet + goal envelopes — wallet is 3 buckets (spend/save/give); wallet.total is derived. Goal progress is derived from earn LEDGER_ENTRY rows via goalProgress(id) — nothing is stored on the GOAL row itself. The goal-envelope transfer shape (funds_moved_to_goal) ties to the Money & Envelopes sprint; transferTokens does not yet exist on the facade.
  • Identity (home-room, colour)PlacesRepository (or the roster load) resolves homePlaceId → place name; colorKey maps to DsTheme palette tokens in the DS layer. Neither requires a new SDK method — both are UI-only gaps.
  • Roles displayHouseholdMember.roles is read directly from the member record; display-only here. Role editing is a Members-management responsibility.
  • Household-scoped RLSGoalsRepository, WalletRepository, and HouseholdRepository rely on household-scoped Supabase policies (SP3). A member never reads another household's goals or wallet via the cloud adapter.
  • Watch-only members tracked for goals, not choreswatchOnly = true members appear on Member Profile (wallet + goal display) but are excluded from Today's assignable list and completion flows. Goal creation on behalf of a watch-only member is an open decision (see below).
  • Goal lifecycle guards in the servicerequestGoal, approveGoal, and completeGoal throw DomainRuleException on invalid transitions; the bloc maps these to saveFailure events → snackbar. Guards never live in the bloc or page.

Design work (ahead of build)

Mostly already designed + built in the P1/P2 pass. Remaining design work:

  • Avatar colour palette (FR-PROFILE-2) — define the colorKey value set and their DsTheme token mappings; a11y contrast check for all entries against the card surface. Small item — can ship alongside the FR.
  • "Reached" visual treatment (FR-PROFILE-10) — badge shape, colour, and optional first-reach animation (pulse?). Motion off by default per prefers-reduced-motion and neurodiversity-affirming defaults.
  • Goal-envelope progress visual — deferred until the Money & Envelopes envelope-reshape lands; may evolve DsProgressBar to reflect wallet-bucket allocation toward goal target. No blocking design work for MVP-1; the existing progress bar suffices.
  • Identity header layout, trait chips, role tags, wallet buckets, and goals list are designed and built — no new work needed.

Decisions (resolved for MVP-1)

Resolved — see the MVP-1 decisions log for the canonical record, rationale, and status legend (✅ decided · ⚖️ counsel confirms · 🔜 MVP-1.x).

  • D-PROFILE-1 — colorKey palette & ownership (FR-PROFILE-2). ✅ A fixed palette of 8 contrast-safe colours; admin assigns at invite, member may later self-select from the palette (editable). Not free-form — keeps the DS coherent and contrast-safe.
  • D-PROFILE-2 — Archive / restore UI. ✅ Ship an "archived goals" filter toggle (default hidden) in MVP-1 — restoreGoal exists; this avoids invisible rows.
  • D-PROFILE-3 — Watch-only goal creation. ✅ A watch-only member cannot create their own goals; an admin creates on their behalf. SDK guard: goal-create requires a non-watch-only actor or an admin actor.
  • D-PROFILE-4 — funds_moved_to_goal event. ✅ With money in MVP-1, it emits from the service layer (not the facade) — analytics stays tied to the domain mutation, single source.

Out of scope (MVP-1)

  • Token transfer / sort sheet — no transferTokens on the facade; tracked as Money-sprint SDK work.
  • Birthday month/dayHouseholdMember.age is int?; birthMonth/birthDay fields and the required migration are deferred.
  • Role editing — lives on the Members-management page (Profile → Account), per T7 P2.
  • Member add / remove / invite — Members-management page, not this surface.
  • Archived goals togglearchiveGoal is built but re-surfacing archived goals in the list view is out of MVP-1 scope.
  • Goal image uploadGoal.imageUrl is in the model; the upload flow and storage bucket are deferred (emoji covers MVP-1 personalisation).