Skip to main content

Branded Auth + Onboarding First Impression — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace the bare Rewhaven auth/onboarding forms with a branded, dusk-atmosphere first impression (intro carousel → welcome → "B"-treatment forms → setup wizard) matching the marketing site, lush-but-calm and reduced-motion-respecting.

Architecture: Inside/outside Flutter (auto_route guards, bloc, repositories over the client_sdk facade). New screens live under app/lib/inside/routes/anon/ (+ a post-auth setup route). A reusable dusk backdrop (DsAuroraSky) goes in design_system; a shared AuthScaffold (dusk hero band + clean form slot) wraps the existing forms so they barely change. Atmosphere is composed from the already-ported DsRooftops/DsConstellation/DsVignette. One new bloc (SetupBloc).

Tech Stack: Flutter 3.44 / Dart 3.9 (FVM), flutter_bloc, auto_route, json_serializable (bloc state), the design_system package, the gadfly flow_test harness, Maestro.

Global Constraints

  • Use fvm flutter / fvm dart for every command.
  • Bloc/cubit STATE is @JsonSerializable() + Equatable + a Status enum + explicit toJson/fromJson (+ part 'state.g.dart') — the DevTools serialization contract. Mirror app/lib/inside/blocs/today_chores/state.dart.
  • Maestro resolves id: against Semantics(identifier: '...'), NOT Flutter Keys. Put a Semantics(identifier:) on every control Maestro taps; keep Keys for find.byKey in widget/flow tests.
  • All animation honors reduced motion via MotionTokens.durationOrZero(context, ...) / MediaQuery.disableAnimationsOf(context) — one code path, static when reduced. client_sdk stays PURE DART (no atmosphere/UI there).
  • Codegen uses lean_builder; a scoped build_runner build --build-filter can clobber sibling generated files — after codegen, git diff and restore any unintended .g.dart/.gr.dart from HEAD (verify byte-identical).
  • Brand copy verbatim from the spec / rewhaven-marketsite src/brand.ts; brand strings live in i18n, not class/file names.
  • Flow-test screenshots are gitignored; goldens run only under --dart-define=createScreenshots=true. Run flow tests from app/.
  • After any code change: cd rewhaven && fvm flutter analyze → 0/0; fvm dart format; relevant tests green. Commit per task. Don't push (the human pushes). Run graphify update . after a phase (don't commit graphify-out).

File structure

design_system (reusable brand):

  • Create packages/design_system/lib/src/graphics/ds_aurora_sky.dart — the dusk backdrop: vertical night→horizon gradient + 2 blurred aurora blobs (warm/cool) + N slowly-drifting twinkling stars. Animated; static under reduced motion. Exported from the barrel.
  • Add a DsAuroraSky Widgetbook story + golden under gallery/ + test/golden/.

app — anon shell + screens:

  • Create app/lib/inside/routes/anon/widgets/auth_hero_band.dartDsAuroraSky (band-clipped) + DsRooftops at the bottom + a Bricolage headline (lead + glow word + subtitle).
  • Create app/lib/inside/routes/anon/widgets/auth_scaffold.dartAuthHeroBand on top + a clean solid panel hosting the form child.
  • Modify app/lib/inside/routes/anon/sign_in/page.dart, sign_up/page.dart, forgot_password/page.dart — wrap their existing form bodies in AuthScaffold.
  • Create app/lib/inside/routes/anon/welcome/page.dartWelcomeScreen (full-dusk hub).
  • Create app/lib/inside/routes/anon/intro/page.dart (+ widgets/intro_slide.dart) — OnboardingCarousel.
  • Create app/lib/inside/routes/authenticated/setup/page.dart (+ widgets/wizard_scaffold.dart, steps/*.dart) — SetupWizard.

app — state + routing + prefs:

  • Create app/lib/inside/blocs/setup/{bloc.dart,event.dart,state.dart}SetupBloc.
  • Modify app/lib/inside/routes/router.dart + guards — add intro/welcome/setup routes, the seenIntro + needsSetup gates; regenerate router.gr.dart.
  • Modify app/lib/outside/effect_providers/shared_prefs/effect.dart (+ fake) — seenIntro get/set.
  • Modify app/.maestro/ — an onboarding e2e flow.

Phase 1 — Dusk backdrop + hero band + restyle the forms

Task 1: DsAuroraSky (design_system reusable dusk backdrop)

Files:

  • Create: packages/design_system/lib/src/graphics/ds_aurora_sky.dart
  • Modify: packages/design_system/lib/design_system.dart (export)
  • Modify: packages/design_system/gallery/lib/main.dart (Graphics story)
  • Test: packages/design_system/test/golden/graphics_golden_test.dart (add a static golden)

Interfaces:

  • Produces: class DsAuroraSky extends StatefulWidget { const DsAuroraSky({this.animate = true, this.starCount = 5, super.key}); } — paints the night→horizon gradient + a warm + a cool aurora blob + starCount twinkling stars; uses theme.colors.auroraWarm/auroraCool/star and a night gradient from surfaceSunken→a warm horizon. Animated drift+twinkle; static (first frame) under reduced motion like DsConstellation.

  • Step 1: Write the failing golden test

In packages/design_system/test/golden/graphics_golden_test.dart, add (mirror the existing DsConstellation golden test in this file — same helpers, MediaQuery(disableAnimations: true) wrapper, tag golden):

testWidgets('DsAuroraSky golden (static, dark)', (tester) async {
await pumpGolden(tester, const SizedBox(width: 320, height: 360, child: DsAuroraSky(animate: false)));
await screenMatchesGolden(tester, 'ds_aurora_sky');
}, tags: 'golden');
  • Step 2: Run it to verify it fails

Run: cd rewhaven/packages/design_system && MSYS_NO_PATHCONV=1 fvm flutter test test/golden/graphics_golden_test.dart -t golden Expected: FAIL — DsAuroraSky undefined.

  • Step 3: Implement DsAuroraSky

Create ds_aurora_sky.dart. Structure (mirror DsConstellation's controller + reduced-motion handling): a StatefulWidget + SingleTickerProviderStateMixin; didChangeDependencies starts _controller.repeat() unless !animate || MediaQuery.disableAnimationsOf(context). Build = DecoratedBox with a vertical LinearGradient (colors.surfaceSunken → a dusk mix toward colors.roof/a warm horizon), with two positioned blurred circles (ImageFiltered/BackdropFilter-free: a Container with BoxDecoration(boxShadow) or a CustomPaint radial) tinted colors.auroraWarm/auroraCool, plus a CustomPaint star layer driven by the controller (copy the star twinkle math from DsConstellation, positions pseudo-random by index). Add the class doc + export 'src/graphics/ds_aurora_sky.dart'; to the barrel.

  • Step 4: Regenerate the golden + verify pass

Run: cd rewhaven/packages/design_system && MSYS_NO_PATHCONV=1 fvm flutter test test/golden/graphics_golden_test.dart -t golden --update-goldens then without --update-goldens. Expected: PASS. Open test/golden/goldens/ds_aurora_sky.png — a dusk gradient + two soft aurora glows + a few stars, no tofu.

  • Step 5: Add the gallery story + analyze + format + commit

Add a DsAuroraSky use-case to the Graphics grouping in gallery/lib/main.dart (a _scrollStage/_stage with const DsAuroraSky() at ~320×360). Then: Run: cd rewhaven && fvm flutter analyze (0/0) · fvm dart format packages/design_system · cd packages/design_system/gallery && MSYS_NO_PATHCONV=1 fvm flutter build web --release (succeeds).

git add packages/design_system/lib/src/graphics/ds_aurora_sky.dart packages/design_system/lib/design_system.dart packages/design_system/gallery/lib/main.dart packages/design_system/test/golden/
git commit -m "feat(ds): DsAuroraSky dusk backdrop (aurora + drifting stars, reduced-motion static)"

Task 2: AuthHeroBand + AuthScaffold

Files:

  • Create: app/lib/inside/routes/anon/widgets/auth_hero_band.dart
  • Create: app/lib/inside/routes/anon/widgets/auth_scaffold.dart
  • Test: app/test/widget/auth_scaffold_test.dart

Interfaces:

  • Consumes: DsAuroraSky, DsRooftops, DsTheme, theme.typography.display/colors.glow.

  • Produces:

    • class AuthHeroBand extends StatelessWidget { const AuthHeroBand({required this.lead, this.glowWord, required this.subtitle, this.height = 0.36, super.key}); } — a SizedBox (height × screen-height fraction) clipping DsAuroraSky with DsRooftops along its bottom edge + a centered Bricolage headline: lead with an optional glowWord colored colors.glow, and a muted subtitle.
    • class AuthScaffold extends StatelessWidget { const AuthScaffold({required this.band, required this.child, super.key}); } — a Scaffold with body = Column: the band on top, then an Expanded/SingleChildScrollView on colors.surface hosting child (the form), padded theme.spacing.s4.
  • Step 1: Write the failing widget test

app/test/widget/auth_scaffold_test.dart:

testWidgets('AuthScaffold shows the band headline + the form child', (tester) async {
await tester.pumpWidget(MaterialApp(
theme: DsTheme.dark.toThemeData(),
home: const AuthScaffold(
band: AuthHeroBand(lead: 'Welcome', glowWord: 'home', subtitle: 'Sign in to your household'),
child: Text('FORM-SLOT'),
),
));
await tester.pump();
expect(find.text('home'), findsOneWidget); // glow word rendered
expect(find.text('Sign in to your household'), findsOneWidget);
expect(find.text('FORM-SLOT'), findsOneWidget); // child hosted
});
  • Step 2: Run to verify it fails

Run: cd rewhaven/app && fvm flutter test test/widget/auth_scaffold_test.dart Expected: FAIL — AuthScaffold/AuthHeroBand undefined.

  • Step 3: Implement AuthHeroBand then AuthScaffold

auth_hero_band.dart: build = SizedBox(height: MediaQuery.sizeOf(context).height * height, child: Stack(children: [ Positioned.fill(child: DsAuroraSky()), Positioned(bottom:0,left:0,right:0, child: DsRooftops(height: 56)), Positioned.fill(child: Center(child: Column(mainAxisSize: min, children: [ headline, SizedBox(s2), Text(subtitle, muted) ]))) ])). The headline = Text.rich with lead in theme.typography.display.copyWith(color: ink) and $glowWord in the same style color: colors.glow (only if glowWord != null). Wrap the DsAuroraSky/DsRooftops in ExcludeSemantics (decorative). auth_scaffold.dart: as the interface.

  • Step 4: Run to verify it passes

Run: cd rewhaven/app && fvm flutter test test/widget/auth_scaffold_test.dart Expected: PASS.

  • Step 5: analyze + format + commit
cd rewhaven && fvm flutter analyze # 0/0
fvm dart format app/lib/inside/routes/anon/widgets app/test/widget/auth_scaffold_test.dart
git add app/lib/inside/routes/anon/widgets/ app/test/widget/auth_scaffold_test.dart
git commit -m "feat(app): AuthHeroBand + AuthScaffold (dusk hero band over a clean form panel)"

Task 3: Wrap sign-in / sign-up / forgot in AuthScaffold

Files:

  • Modify: app/lib/inside/routes/anon/sign_in/page.dart, sign_up/page.dart, forgot_password/page.dart
  • Test: app/test/flows/sign_in_test.dart, sign_up_test.dart, forgot_password_test.dart (these already exist — keep green; add a band-headline assertion)

Interfaces:

  • Consumes: AuthScaffold, AuthHeroBand.

  • Step 1: Update the existing flow tests to assert the band

In sign_in_test.dart add expect(find.text('home'), findsWidgets); (the band's glow word) to the initial-page screenshot step's expectations. Same idea for sign_up_test.dart (band word "household") and forgot_password_test.dart (band word "password"). Run them first to confirm they FAIL (the band isn't there yet): Run: cd rewhaven/app && fvm flutter test test/flows/sign_in_test.dart Expected: FAIL — "home" not found.

  • Step 2: Wrap each page body in AuthScaffold

In each page's build, replace the outer Scaffold(body: ...) with AuthScaffold(band: AuthHeroBand(lead: ..., glowWord: ..., subtitle: ...), child: <the existing form Column>). Sign in → lead "Welcome" glow "home" subtitle "Sign in to your household". Sign up → lead "Create your" glow "household" subtitle "Start your family's calm". Forgot → lead "Reset your" glow "password" subtitle "We'll send a reset link". Keep ALL existing fields/keys/Semantics(identifier:)/blocs/AutofillGroup/finishAutofillContext unchanged — only the wrapping changes.

  • Step 3: Run all three flow tests + verify pass

Run: cd rewhaven/app && fvm flutter test test/flows/sign_in_test.dart test/flows/sign_up_test.dart test/flows/forgot_password_test.dart Expected: PASS (light/dark/focus trips). The field finders + expectedEvents still resolve because only the wrapper changed.

  • Step 4: Full gates + commit

Run: cd rewhaven && fvm flutter analyze (0/0) · cd app && fvm flutter test (all green) · cd rewhaven && fvm dart format . (clean).

git add app/lib/inside/routes/anon/sign_in app/lib/inside/routes/anon/sign_up app/lib/inside/routes/anon/forgot_password app/test/flows
git commit -m "feat(app): restyle sign-in/sign-up/forgot with the dusk AuthScaffold band"

Then cd rewhaven && graphify update . (don't commit graphify-out). Phase 1 ships an immediate brand lift.


Phase 2 — Welcome screen + routing

Task 4: WelcomeScreen

Files:

  • Create: app/lib/inside/routes/anon/welcome/page.dart
  • Test: app/test/flows/welcome_test.dart

Interfaces:

  • Consumes: DsAuroraSky, DsRooftops, DsButton, the router.

  • Produces: @RoutePage() class WelcomePage extends StatelessWidget — full-dusk: Stack of Positioned.fill(DsAuroraSky()), Positioned(bottom, DsRooftops()), centered Column: 🌙 + rewhaven wordmark (theme.typography.display) + "Welcome home" + a muted line, then bottom-anchored DsButton('Create your household', Semantics id 'WelcomePage.createButton')router.push(const SignUpRoute()) and a Sign in text button (Semantics id 'WelcomePage.signInButton') → router.push(const SignInRoute()).

  • Step 1: Write the failing flow test

app/test/flows/welcome_test.dart (gadfly flowTest, mirror sign_in_test.dart's structure; warp not needed — start unauthenticated). Steps: screenshot the welcome page; assert "Welcome"/"home", the wordmark, and both CTAs; tap id/byKey WelcomePage.createButton → assert the sign-up band ("household") appears.

testWidgets('welcome → create your household', (tester) async {
// pump appBuilder with MocksContainer (unauthenticated); the welcome route is the anon landing
// assert: find.text('home'), find.text('rewhaven'), find.byKey(WelcomePage.createKey)
// tap create → expect find.text('household') (sign-up band)
});
  • Step 2: Run to verify it fails

Run: cd rewhaven/app && fvm flutter test test/flows/welcome_test.dart Expected: FAIL — WelcomeRoute/WelcomePage undefined.

  • Step 3: Implement WelcomePage

Create the page per the interface. Add static const createKey = Key('WelcomePage.create'); etc., and matching Semantics(identifier: 'WelcomePage.createButton'|'signInButton').

  • Step 4: Add the route (Task 5 wires the guard) + regenerate router

Add WelcomePage to the auto_route @RoutePage set and the AppRouter routes list under the unauthenticated branch. Run codegen: Run: cd rewhaven/app && fvm dart run build_runner build --delete-conflicting-outputs --build-filter "lib/inside/routes/router.gr.dart" then git diff — confirm router.gr.dart gained WelcomeRoute and NO other generated file changed (restore from HEAD if clobbered).

  • Step 5: Run the flow test + gates + commit

Run: cd rewhaven/app && fvm flutter test test/flows/welcome_test.dart (PASS) · cd rewhaven && fvm flutter analyze (0/0) · fvm dart format ..

git add app/lib/inside/routes/anon/welcome app/lib/inside/routes/router.dart app/lib/inside/routes/router.gr.dart app/test/flows/welcome_test.dart
git commit -m "feat(app): WelcomeScreen (full-dusk hub) + route"

Task 5: Make Welcome the unauthenticated landing

Files:

  • Modify: app/lib/inside/routes/guards/unauthenticated_guard.dart (or the router's initial route)
  • Test: app/test/flows/sign_in_and_home_test.dart (real-stack — update its sign-in prologue to start from Welcome → Sign in)

Interfaces:

  • Consumes: WelcomeRoute, SignInRoute.

  • Step 1: Update the real-stack flow test prologue

In sign_in_and_home_test.dart, the unauthenticated launch now lands on Welcome; before filling the sign-in form, tap the Welcome "Sign in" button to reach the form. Run it first → expect FAIL (it lands on Welcome, not the form).

  • Step 2: Point the anon landing at Welcome

Set the unauthenticated default/redirect target to WelcomeRoute (the guard redirects unauthenticated users to Welcome; Sign-in/Sign-up are pushed from there). Keep the deep-link to SignIn working (direct navigation still allowed).

  • Step 3: Run the affected flow tests + verify

Run: cd rewhaven/app && fvm flutter test test/flows/sign_in_and_home_test passes via the Welcome→Sign-in prologue; sign_in_test/sign_up_test (which warp/pump the form route directly) stay green. Expected: PASS.

  • Step 4: gates + commit
cd rewhaven && fvm flutter analyze # 0/0
git add app/lib/inside/routes/guards app/test/flows/sign_in_and_home_test.dart
git commit -m "feat(app): Welcome is the unauthenticated landing (Sign in/up pushed from it)"

Task 6: seenIntro flag on the SharedPrefs effect

Files:

  • Modify: app/lib/outside/effect_providers/shared_prefs/effect.dart, effect_fake.dart
  • Test: app/test/unit/shared_prefs_effect_test.dart (add cases)

Interfaces:

  • Produces on SharedPrefsEffect: bool getSeenIntro() (default false) + Future<void> setSeenIntro(bool), backed by getBool/setBool with key 'app.seen_intro'.

  • Step 1: Add a failing test

In app/test/unit/shared_prefs_effect_test.dart (using SharedPrefsEffectFake):

test('seenIntro defaults false, persists true', () async {
final fake = SharedPrefsEffectFake();
expect(fake.getSeenIntro(), isFalse);
await fake.setSeenIntro(true);
expect(fake.getSeenIntro(), isTrue);
});

Run: cd rewhaven/app && fvm flutter test test/unit/shared_prefs_effect_test.dart → FAIL (getSeenIntro undefined).

  • Step 2: Implement the getter/setter on SharedPrefsEffect + mirror in SharedPrefsEffectFake (the fake delegates to its in-memory map). Const seenIntroKey = 'app.seen_intro'.

  • Step 3: Run + commit

Run: test PASS · cd rewhaven && fvm flutter analyze (0/0) · format.

git add app/lib/outside/effect_providers/shared_prefs app/test/unit/shared_prefs_effect_test.dart
git commit -m "feat(app): SharedPrefsEffect.seenIntro flag"

Task 7: OnboardingCarousel (3 intro slides)

Files:

  • Create: app/lib/inside/routes/anon/intro/page.dart, app/lib/inside/routes/anon/intro/widgets/intro_slide.dart
  • Modify: app/lib/inside/routes/router.dart (+ regen router.gr.dart)
  • Test: app/test/flows/intro_test.dart

Interfaces:

  • Consumes: DsVignette (+ DsVignetteKind), DsButton, DsAuroraSky, SharedPrefsEffect.setSeenIntro, WelcomeRoute.

  • Produces: @RoutePage() class IntroPage extends StatefulWidget — full-dusk PageView of 3 IntroSlides, page dots, a "Skip" (Semantics id 'IntroPage.skip') top-right, and on the last slide a "Get started" DsButton (id 'IntroPage.getStarted'). Both Skip + Get-started call setSeenIntro(true) then router.replace(const WelcomeRoute()). IntroSlide({required kind, required lead, glowWord, required subtitle}) = DsVignette(kind) scene + the Bricolage headline + the muted line (the spec's three beats verbatim).

  • Step 1: Write the failing flow test

app/test/flows/intro_test.dart: screenshot slide 1 (assert "evening battle" glow word + the vignette); tap IntroPage.skip → assert it lands on Welcome ("Welcome"/"home") AND the prefs fake getSeenIntro() is now true. Run → FAIL (undefined).

  • Step 2: Implement IntroSlide then IntroPage per the interface, with the three beats from the spec (rooftops/"evening battle", constellation/"scoreboard", coins/"private"). Page dots reflect _controller.page. Decorative graphics wrapped in ExcludeSemantics.

  • Step 3: Add the route + regen (build_runner ... --build-filter router.gr.dart; git diff clean except IntroRoute).

  • Step 4: Run the flow test + gates + commit

Run: cd rewhaven/app && fvm flutter test test/flows/intro_test.dart (PASS) · analyze 0/0 · format.

git add app/lib/inside/routes/anon/intro app/lib/inside/routes/router.dart app/lib/inside/routes/router.gr.dart app/test/flows/intro_test.dart
git commit -m "feat(app): OnboardingCarousel (3 brand beats) + Skip/Get-started → Welcome"

Task 8: Gate the intro (first-time only)

Files:

  • Modify: app/lib/inside/routes/guards/unauthenticated_guard.dart (or an IntroGuard)
  • Test: app/test/flows/intro_test.dart (add: seenIntro=true → skips intro, lands on Welcome)

Interfaces:

  • Consumes: SharedPrefsEffect.getSeenIntro, IntroRoute, WelcomeRoute.

  • Step 1: Add the failing gate test — with the prefs fake seeded seenIntro=true, the unauthenticated landing should be Welcome (not Intro); with false, Intro. Run → FAIL.

  • Step 2: Implement the gate — the unauthenticated entry resolves to IntroRoute when !getSeenIntro(), else WelcomeRoute. (Read the prefs effect synchronously via the provider already wired in the tree.)

  • Step 3: Run + gates + commit

cd rewhaven && fvm flutter analyze # 0/0
git add app/lib/inside/routes/guards app/test/flows/intro_test.dart
git commit -m "feat(app): show the intro carousel only on first launch (seenIntro gate)"

Then graphify update ..


Phase 4 — Setup wizard + SetupBloc + post-signup routing

Task 9: SetupBloc (state/event/bloc)

Files:

  • Create: app/lib/inside/blocs/setup/state.dart, event.dart, bloc.dart
  • Test: app/test/unit/setup_bloc_test.dart

Interfaces:

  • Consumes: HouseholdRepository (or a SetupRepository exposing createHousehold/setHouseholdName + addMember) — VERIFY the facade method names in client_sdk (createHousehold, addMember) and reuse the existing HouseholdRepository if it already wraps them; else add the thin methods.

  • Produces:

    • enum SetupStatus { naming, addingKid, submitting, done, failure }
    • @JsonSerializable() class SetupState extends Equatable with status, householdName, householdEmoji, kidName, kidColorIndex, kidEmoji, errorMessage + toJson/fromJson + part 'state.g.dart' (mirror today_chores/state.dart).
    • Events: SetupHouseholdNamed(name, emoji), SetupKidSubmitted(name, colorIndex, emoji), SetupKidSkipped().
    • class SetupBloc extends Bloc<SetupEvent, SetupState> (AppBloc if that base exists) — SetupHouseholdNamed → createHousehold/setHouseholdName, emit addingKid; SetupKidSubmitted → addMember(child), emit submittingdone; SetupKidSkipped → emit done. Errors → failure with message.
  • Step 1: Write the failing unit test (use bloc_test or manual; mock the repository):

blocTest<SetupBloc, SetupState>('name → add kid → done',
build: () => SetupBloc(householdRepository: mockRepo),
act: (b) => b..add(SetupHouseholdNamed('The Riveras','🏡'))..add(SetupKidSubmitted('Luna',0,'🦊')),
expect: () => [
isA<SetupState>().having((s)=>s.status, 'status', SetupStatus.addingKid),
isA<SetupState>().having((s)=>s.status, 'status', SetupStatus.submitting),
isA<SetupState>().having((s)=>s.status, 'status', SetupStatus.done),
]);
// + a 'skip kid → done' case and a 'createHousehold throws → failure' case

Run → FAIL (undefined).

  • Step 2: Implement state/event/bloc per the interface; run codegen for setup/state.g.dart (--build-filter "lib/inside/blocs/setup/state.g.dart"; git diff clean otherwise). Verify the generated toJson produces a JSON object (DevTools contract) by reading state.g.dart.

  • Step 3: Run the unit test + verify pass. Run: cd rewhaven/app && fvm flutter test test/unit/setup_bloc_test.dart → PASS.

  • Step 4: gates + commit

cd rewhaven && fvm flutter analyze # 0/0
git add app/lib/inside/blocs/setup app/test/unit/setup_bloc_test.dart
git commit -m "feat(app): SetupBloc (name household → add kid → done)"

Task 10: WizardScaffold + 3 steps + SetupPage

Files:

  • Create: app/lib/inside/routes/authenticated/setup/page.dart, widgets/wizard_scaffold.dart, steps/name_step.dart, steps/kid_step.dart, steps/done_step.dart
  • Modify: app/lib/inside/routes/router.dart (+ regen)
  • Test: app/test/flows/setup_test.dart

Interfaces:

  • Consumes: SetupBloc, DsTextField/identity molecules, DsButton, DsAvatar/kidColor, HomeRoute.

  • Produces: @RoutePage() class SetupPage extends StatelessWidget implements AutoRouteWrapper — provides SetupBloc in wrappedRoute; WizardScaffold(progress, child, cta) renders a progress bar (steps) + the active step body + the CTA; a BlocBuilder<SetupBloc,SetupState> swaps Name → Kid → Done by status, and on done's "Go to Today" → router.replace(const HomeRoute()). Steps carry Semantics(identifier: 'SetupPage.nameField'|'continueButton'|'kidNameField'|'addKidButton'|'skipKid'|'goToToday') + Keys. The "name" step's CTA dispatches SetupHouseholdNamed; the "kid" step SetupKidSubmitted/SetupKidSkipped. Atmosphere: a small DsAuroraSky-tinted top accent (NOT a full band) — the calm handoff.

  • Step 1: Write the failing flow test

app/test/flows/setup_test.dart: pump the wizard (authenticated, seeded with NO household, or arrange the mocked repo). Step through: enter household name → Continue → enter kid name + pick a colour → Add → assert "You're home" / done → tap "Go to Today" → assert Home renders. Assert the expectedEvents include the SetupHouseholdNamed/SetupKidSubmitted bloc events. Run → FAIL.

  • Step 2: Implement WizardScaffold, the 3 steps, and SetupPage per the interface. Mirror HomePage's AutoRouteWrapper/wrappedRoute bloc-provision pattern.

  • Step 3: Add the route + regen (--build-filter router.gr.dart; git diff clean except SetupRoute).

  • Step 4: Run the flow test + gates + commit

Run: cd rewhaven/app && fvm flutter test test/flows/setup_test.dart (PASS) · analyze 0/0 · format.

git add app/lib/inside/routes/authenticated/setup app/lib/inside/routes/router.dart app/lib/inside/routes/router.gr.dart app/test/flows/setup_test.dart
git commit -m "feat(app): SetupWizard (name → add kid → you're home → Today)"

Task 11: Route a fresh account into setup (needsSetup gate)

Files:

  • Modify: app/lib/inside/routes/guards/authenticated_guard.dart (or the post-auth redirect)
  • Test: app/test/flows/sign_up_test.dart (the success path now lands on Setup, then Today)

Interfaces:

  • Consumes: HouseholdRepository.getHousehold (null = needs setup), SetupRoute, HomeRoute.

  • Step 1: Update sign_up_test success — after Create account, an account with NO household lands on Setup (assert "Name your home"); a signed-in account WITH a household lands on Home. Run → FAIL (currently goes straight to Home).

  • Step 2: Implement the gate — the authenticated entry resolves to SetupRoute when getHousehold() == null, else HomeRoute. (For dev/in-memory, the demo seed already has a household → existing users skip setup; a brand-new sign-up has none → setup.)

  • Step 3: Run the flow tests + verifysign_up_test (→ Setup → Today), today_home_chores_test/sign_in_test (seeded household → straight to Home) stay green.

  • Step 4: gates + commit + graphify

cd rewhaven && fvm flutter analyze # 0/0
git add app/lib/inside/routes/guards app/test/flows/sign_up_test.dart
git commit -m "feat(app): route a new account (no household) into the setup wizard"

Then graphify update ..

Task 12: Maestro onboarding e2e

Files:

  • Create: app/.maestro/onboarding.yaml
  • Modify: app/.maestro/README.md

Interfaces:

  • Consumes: the Semantics(identifier:) ids on Get-started/Create-household/Sign-in and the wizard CTAs; the demo seed.

  • Step 1: Author the flowappId: com.eldrforge.rewhaven; launch (clearState true so it shows the intro) → assert an intro headline → tap IntroPage.getStarted → assert Welcome → tap WelcomePage.createButton → fill sign-up fields → Create account → (since demo seed has a household, OR seed a no-household account) → drive the wizard → assert Home. Match the seeded render; note authored-not-run unless an emulator is attached.

  • Step 2: Document + commit

Update .maestro/README.md with the onboarding flow + the run command. (If an emulator is attached, run maestro --device <id> test .maestro/onboarding.yaml and capture the result; otherwise note it's authored.)

git add app/.maestro/
git commit -m "test(app): Maestro onboarding e2e (intro → welcome → sign-up → setup → Today)"

Self-review

  • Spec coverage: intro carousel (T7/T8), welcome (T4/T5), "B" forms (T2/T3), setup wizard (T9–T11), DsAuroraSky/AuthHeroBand components (T1/T2), seenIntro flag (T6), motion-reduced-motion (T1 backdrop, inherited by all), a11y/Semantics ids (each UI task), testing — flow tests per flow + SetupBloc unit + Maestro (T12). Phasing matches the spec's order. ✓
  • Placeholders: none — each task has the interface, the test, and the implementation shape with exact ids/keys. Widget-body composition references the named DS atoms + the mockups (the spec's screens), which is the established pattern; the load-bearing code (bloc state, prefs, routing gates, test assertions) is concrete.
  • Type consistency: DsAuroraSky({animate, starCount}), AuthHeroBand({lead, glowWord, subtitle, height}), AuthScaffold({band, child}), SetupStatus/SetupState/SetupBloc events, getSeenIntro/setSeenIntro are used consistently across tasks.
  • Open points deferred deliberately: AuthHeroBand lives in the app (it carries auth copy) while the reusable backdrop DsAuroraSky is in the DS; createHousehold vs setHouseholdName is resolved in T9 by reading the facade. These are flagged, not blocking.