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 dartfor every command. - Bloc/cubit STATE is
@JsonSerializable()+Equatable+ aStatusenum + explicittoJson/fromJson(+part 'state.g.dart') — the DevTools serialization contract. Mirrorapp/lib/inside/blocs/today_chores/state.dart. - Maestro resolves
id:againstSemantics(identifier: '...'), NOT FlutterKeys. Put aSemantics(identifier:)on every control Maestro taps; keepKeys forfind.byKeyin widget/flow tests. - All animation honors reduced motion via
MotionTokens.durationOrZero(context, ...)/MediaQuery.disableAnimationsOf(context)— one code path, static when reduced.client_sdkstays PURE DART (no atmosphere/UI there). - Codegen uses
lean_builder; a scopedbuild_runner build --build-filtercan clobber sibling generated files — after codegen,git diffand restore any unintended.g.dart/.gr.dartfrom HEAD (verify byte-identical). - Brand copy verbatim from the spec /
rewhaven-marketsitesrc/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 fromapp/. - 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). Rungraphify 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
DsAuroraSkyWidgetbook story + golden undergallery/+test/golden/.
app — anon shell + screens:
- Create
app/lib/inside/routes/anon/widgets/auth_hero_band.dart—DsAuroraSky(band-clipped) +DsRooftopsat the bottom + a Bricolage headline (lead + glow word + subtitle). - Create
app/lib/inside/routes/anon/widgets/auth_scaffold.dart—AuthHeroBandon 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 inAuthScaffold. - Create
app/lib/inside/routes/anon/welcome/page.dart—WelcomeScreen(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, theseenIntro+needsSetupgates; regeneraterouter.gr.dart. - Modify
app/lib/outside/effect_providers/shared_prefs/effect.dart(+ fake) —seenIntroget/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 +starCounttwinkling stars; usestheme.colors.auroraWarm/auroraCool/starand a night gradient fromsurfaceSunken→a warm horizon. Animated drift+twinkle; static (first frame) under reduced motion likeDsConstellation. -
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}); }— aSizedBox(height× screen-height fraction) clippingDsAuroraSkywithDsRooftopsalong its bottom edge + a centered Bricolage headline:leadwith an optionalglowWordcoloredcolors.glow, and a mutedsubtitle.class AuthScaffold extends StatelessWidget { const AuthScaffold({required this.band, required this.child, super.key}); }— aScaffoldwithbody=Column: thebandon top, then anExpanded/SingleChildScrollViewoncolors.surfacehostingchild(the form), paddedtheme.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
AuthHeroBandthenAuthScaffold
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:StackofPositioned.fill(DsAuroraSky()),Positioned(bottom, DsRooftops()), centeredColumn: 🌙 +rewhavenwordmark (theme.typography.display) + "Welcome home" + a muted line, then bottom-anchoredDsButton('Create your household', Semantics id 'WelcomePage.createButton')→router.push(const SignUpRoute())and aSign intext 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)"
Phase 3 — Intro carousel + seenIntro flag
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()(defaultfalse) +Future<void> setSeenIntro(bool), backed bygetBool/setBoolwith 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 inSharedPrefsEffectFake(the fake delegates to its in-memory map). ConstseenIntroKey = '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(+ regenrouter.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-duskPageViewof 3IntroSlides, 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 callsetSeenIntro(true)thenrouter.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
IntroSlidethenIntroPageper the interface, with the three beats from the spec (rooftops/"evening battle", constellation/"scoreboard", coins/"private"). Page dots reflect_controller.page. Decorative graphics wrapped inExcludeSemantics. -
Step 3: Add the route + regen (
build_runner ... --build-filter router.gr.dart;git diffclean exceptIntroRoute). -
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 anIntroGuard) - 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); withfalse, Intro. Run → FAIL. -
Step 2: Implement the gate — the unauthenticated entry resolves to
IntroRoutewhen!getSeenIntro(), elseWelcomeRoute. (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 aSetupRepositoryexposingcreateHousehold/setHouseholdName+addMember) — VERIFY the facade method names inclient_sdk(createHousehold,addMember) and reuse the existingHouseholdRepositoryif it already wraps them; else add the thin methods. -
Produces:
enum SetupStatus { naming, addingKid, submitting, done, failure }@JsonSerializable() class SetupState extends Equatablewithstatus,householdName,householdEmoji,kidName,kidColorIndex,kidEmoji,errorMessage+toJson/fromJson+part 'state.g.dart'(mirrortoday_chores/state.dart).- Events:
SetupHouseholdNamed(name, emoji),SetupKidSubmitted(name, colorIndex, emoji),SetupKidSkipped(). class SetupBloc extends Bloc<SetupEvent, SetupState>(AppBlocif that base exists) —SetupHouseholdNamed→ createHousehold/setHouseholdName, emitaddingKid;SetupKidSubmitted→ addMember(child), emitsubmitting→done;SetupKidSkipped→ emitdone. Errors →failurewith message.
-
Step 1: Write the failing unit test (use
bloc_testor 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 diffclean otherwise). Verify the generatedtoJsonproduces a JSON object (DevTools contract) by readingstate.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— providesSetupBlocinwrappedRoute;WizardScaffold(progress, child, cta)renders a progress bar (steps) + the active step body + the CTA; aBlocBuilder<SetupBloc,SetupState>swaps Name → Kid → Done bystatus, and ondone's "Go to Today" →router.replace(const HomeRoute()). Steps carrySemantics(identifier: 'SetupPage.nameField'|'continueButton'|'kidNameField'|'addKidButton'|'skipKid'|'goToToday')+Keys. The "name" step's CTA dispatchesSetupHouseholdNamed; the "kid" stepSetupKidSubmitted/SetupKidSkipped. Atmosphere: a smallDsAuroraSky-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, andSetupPageper the interface. MirrorHomePage'sAutoRouteWrapper/wrappedRoutebloc-provision pattern. -
Step 3: Add the route + regen (
--build-filter router.gr.dart;git diffclean exceptSetupRoute). -
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_testsuccess — 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
SetupRoutewhengetHousehold() == null, elseHomeRoute. (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 + verify —
sign_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 flow —
appId: com.eldrforge.rewhaven; launch (clearState true so it shows the intro) → assert an intro headline → tapIntroPage.getStarted→ assert Welcome → tapWelcomePage.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/AuthHeroBandcomponents (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/SetupBlocevents,getSeenIntro/setSeenIntroare used consistently across tasks. - Open points deferred deliberately:
AuthHeroBandlives in the app (it carries auth copy) while the reusable backdropDsAuroraSkyis in the DS;createHouseholdvssetHouseholdNameis resolved in T9 by reading the facade. These are flagged, not blocking.