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.
| Type | Functional |
| Layer (build approach) | Frontend — design_system atoms + app bloc/page + flow-test |
| RICE | R 8 × I 2 × C 100% / E 2 = 8.0 · Tier MVP-1 |
| KPI (summary) | Goals created/member → token-fade |
| Traces to | feature member-profile · C4 components |
| Depends on | Money & 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_reachedevents /goal_createdevents, per cohort-week. - Target B: ≥ 20% of goals created in weeks 1–4 reach
completeby week 8. - Window: both targets measured within 8 weeks of household activation (activation =
first
chore_completedapproved 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
effectiveTokensper completion trends down over 8 weeks without a completion-frequency drop among goal-holding members, the thesis holds. Agoal_reachedrate 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.
| Event | When | Key properties | Feeds |
|---|---|---|---|
profile_viewed | MemberProfilePage opened | isOwnProfile, watchOnly, goalCount, activeGoalCount | engagement denominator; adoption funnel |
goal_created | createGoal succeeds | scope (member|family), targetTokens, hasEmoji, memberRef | KPI-A numerator; goal adoption |
goal_requested | requestGoal succeeds | goalId (opaque), memberRef | lifecycle funnel |
goal_approved | approveGoal succeeds | goalId (opaque), approverRef | approve-rate; funnel |
goal_reached | completeGoal succeeds | goalId (opaque), targetTokens, daysToComplete, memberRef | KPI-B numerator; token-fade signal |
funds_moved_to_goal | wallet transfer allocates to a goal envelope | fromBucket (spend|save|give), amount, memberRef | goal-funding behaviour (deferred — no facade method yet) |
profile_viewedis the session-level denominator for goal engagement.goal_createdandgoal_reachedare 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, andwatchOnly = trueWhen the profile loads Then all three render in the header (displayNameprominent,DsTagfor age,DsTag(watchOnly)visible). - Given
watchOnly = falseWhen 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.
Acceptance — Given 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.
Acceptance — Given 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 +saveFailuresnackbar 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.
Acceptance — Given 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.
Acceptance — Given 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.
Acceptance — Given 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
activegoal When a member taps "Request" ThenrequestGoalis called, status →requested, and the tile shows "Awaiting approval." - Given a
requestedgoal and an admin viewer When they tap "Approve" ThenapproveGoal(actingMemberId: adminId)is called; status →active. - Non-parental
actingMemberId→DomainRuleExceptionsurfaced 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 == completeWhen the tile renders Then a "Reached" badge appears and no earn-action affordances are shown. - Given
goal.dueLabelis non-null When the tile renders Then the label renders as a subtitle below the goal name. MemberProfileGoalCompletedevent →completeGoal(id)→ state patched tocomplete.
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.
Acceptance — Given 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.
Acceptance — Given 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 path —
MemberProfileBloc → {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.totalis derived. Goal progress is derived from earnLEDGER_ENTRYrows viagoalProgress(id)— nothing is stored on theGOALrow itself. The goal-envelope transfer shape (funds_moved_to_goal) ties to the Money & Envelopes sprint;transferTokensdoes not yet exist on the facade. - Identity (home-room, colour) —
PlacesRepository(or the roster load) resolveshomePlaceId → place name;colorKeymaps toDsThemepalette tokens in the DS layer. Neither requires a new SDK method — both are UI-only gaps. - Roles display —
HouseholdMember.rolesis read directly from the member record; display-only here. Role editing is a Members-management responsibility. - Household-scoped RLS —
GoalsRepository,WalletRepository, andHouseholdRepositoryrely 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 chores —
watchOnly = truemembers 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 service —
requestGoal,approveGoal, andcompleteGoalthrowDomainRuleExceptionon invalid transitions; the bloc maps these tosaveFailureevents → 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
colorKeyvalue set and theirDsThemetoken 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-motionand neurodiversity-affirming defaults. - Goal-envelope progress visual — deferred until the Money & Envelopes envelope-reshape
lands; may evolve
DsProgressBarto 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 —
colorKeypalette & 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 —
restoreGoalexists; 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_goalevent. ✅ 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
transferTokenson the facade; tracked as Money-sprint SDK work. - Birthday month/day —
HouseholdMember.ageisint?;birthMonth/birthDayfields 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 toggle —
archiveGoalis built but re-surfacing archived goals in the list view is out of MVP-1 scope. - Goal image upload —
Goal.imageUrlis in the model; the upload flow and storage bucket are deferred (emoji covers MVP-1 personalisation).