Implement main app navigation
This commit is contained in:
@@ -0,0 +1,715 @@
|
||||
---
|
||||
phase: 02.1
|
||||
plan: 08
|
||||
type: execute
|
||||
wave: 5
|
||||
depends_on: ["02.1-05", "02.1-06", "02.1-07"]
|
||||
files_modified:
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt
|
||||
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt
|
||||
autonomous: true
|
||||
requirements: [UI-09]
|
||||
tags: [kotlin, koin, di, app-entry, navigation, glass, expect-actual, integration, multiplatform-settings]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "shellModule registers all 4 tab VMs (PlannerViewModel, RecipesViewModel, PantryViewModel, ShoppingViewModel), both Search VMs (RecipesSearchViewModel, PantrySearchViewModel), ShellViewModel, and a single<GlassBackend> { resolveGlassBackend(get<Settings>(), isDebugBuild, default) } provider"
|
||||
- "AppModule.includes(...) gains shellModule alongside authModule + userModule"
|
||||
- "App.kt's Authenticated + currentUser != null branch resolves to AppShell() instead of PostLoginPlaceholderScreen(...)"
|
||||
- "App.kt preserves the LaunchedEffect(authSession) { initialize() } block and the currentUser == null → SplashScreen() arm"
|
||||
- "PostLoginPlaceholderScreen + PostLoginViewModel are NOT deleted (logout-bridge possibility per CONTEXT line 101 / RESEARCH § Open Questions Q3)"
|
||||
- "RecipeTheme.kt provides LocalGlassBackend via CompositionLocalProvider so AppShell + chrome composables resolve the backend"
|
||||
- "RootNavHost's TabHomePlaceholder stubs (from plan 02.1-04) are replaced with the real Tab*Screen calls using koinViewModel(viewModelStoreOwner = parent) per RESEARCH § Pattern 2"
|
||||
- "V-04 anchor: AppShellGateTest replaces its @Ignore stub with a real test asserting that App's Authenticated+user routing branches to the AppShell branch (or extracted RootRouter pure function)"
|
||||
artifacts:
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt"
|
||||
provides: "Koin shellModule — 7 VMs + GlassBackend single"
|
||||
contains: "val shellModule"
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt"
|
||||
provides: "appModule extended to include shellModule"
|
||||
contains: "shellModule"
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt"
|
||||
provides: "App() composable routing Authenticated+user to AppShell()"
|
||||
contains: "AppShell()"
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt"
|
||||
provides: "RootNavHost wired to call PlannerScreen / RecipesScreen / PantryScreen / ShoppingScreen with VM scoping"
|
||||
contains: "PlannerScreen"
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt"
|
||||
provides: "RecipeTheme provides LocalGlassBackend value resolved at startup"
|
||||
contains: "LocalGlassBackend provides"
|
||||
- path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt"
|
||||
provides: "V-04 anchor — real assertion that Authenticated+user routes to AppShell branch"
|
||||
key_links:
|
||||
- from: "App.kt"
|
||||
to: "ui/screens/shell/AppShell.kt"
|
||||
via: "Authenticated branch invokes AppShell() instead of PostLoginPlaceholderScreen(...)"
|
||||
pattern: "AppShell\\(\\)"
|
||||
- from: "di/AppModule.kt"
|
||||
to: "di/ShellModule.kt"
|
||||
via: "includes(authModule, userModule, shellModule)"
|
||||
pattern: "shellModule"
|
||||
- from: "di/ShellModule.kt"
|
||||
to: "ui/components/glass/GlassBackend.kt"
|
||||
via: "single<GlassBackend> { resolveGlassBackend(get<Settings>(), isDebugBuild, default) }"
|
||||
pattern: "resolveGlassBackend"
|
||||
- from: "ui/theme/RecipeTheme.kt"
|
||||
to: "ui/components/glass/GlassBackend.kt"
|
||||
via: "CompositionLocalProvider(LocalGlassBackend provides koinInject<GlassBackend>())"
|
||||
pattern: "LocalGlassBackend"
|
||||
- from: "navigation/RootNavHost.kt"
|
||||
to: "ui/screens/{planner,recipes,pantry,shopping}/{Tab}Screen.kt"
|
||||
via: "composable<*Home>{ koinViewModel(viewModelStoreOwner = parent) → Tab*Screen(viewModel = vm) }"
|
||||
pattern: "PlannerScreen\\(viewModel ="
|
||||
---
|
||||
|
||||
<objective>
|
||||
Final integration — wire the seven shell ViewModels and the GlassBackend resolver into a Koin `shellModule`; extend `appModule.includes(...)` to pull in `shellModule`; provide `LocalGlassBackend` in `RecipeTheme` so all chrome consuming `GlassSurface` resolves the right backend; replace the four `TabHomePlaceholder` stubs in `RootNavHost.kt` (from plan 02.1-04) with calls into the real `PlannerScreen` / `RecipesScreen` / `PantryScreen` / `ShoppingScreen` (from plan 02.1-07) using `koinViewModel(viewModelStoreOwner = parent)` per RESEARCH § Pattern 2; and finally swap the `Authenticated + currentUser != null` branch in `App.kt` from `PostLoginPlaceholderScreen(...)` to `AppShell()`.
|
||||
|
||||
Replace the @Ignore'd Wave-0 stub in `AppShellGateTest.kt` (V-04) with a real assertion. The cleanest test path: extract the routing logic in `App.kt` into a pure `RootRouter` enum (Splash / Login / Shell) computed from `(authState, currentUser)` and assert the enum value directly. The `App()` composable becomes a thin wrapper that switches on the enum. This keeps the test deterministic without instrumenting Compose composition.
|
||||
|
||||
`PostLoginPlaceholderScreen.kt` and `PostLoginViewModel.kt` are NOT deleted — RESEARCH § Open Questions Q3 (now RESOLVED) and CONTEXT line 101 keep them as a logout-bridge possibility. They are simply no longer reachable from the auth-gate flow this phase. A future phase may delete them or repurpose them.
|
||||
|
||||
Per CONTEXT line 52, the auth screens (LoginScreen, PostLoginPlaceholderScreen, SplashScreen) keep their Material 3 imports as legacy. Plan 02.1-02 preserved `MaterialTheme(colorScheme = ...)` wrapping in RecipeTheme so those screens keep working.
|
||||
|
||||
Purpose: turn the shell from "exists in the codebase" to "actually rendered after sign-in". UI-09 final closure: the Authenticated user lands in the real shell, not the placeholder.
|
||||
Output: 1 new file (ShellModule.kt) + 5 modified files; 1 test un-ignored covering V-04.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-CONTEXT.md
|
||||
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md
|
||||
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-UI-SPEC.md
|
||||
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md
|
||||
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-VALIDATION.md
|
||||
@composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt
|
||||
@composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt
|
||||
@composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt
|
||||
@composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt
|
||||
@composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt
|
||||
@composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt
|
||||
|
||||
<interfaces>
|
||||
After Wave 4 (plan 02.1-05) and its prerequisites (02.1-06, 02.1-07) land, the following symbols are available:
|
||||
|
||||
From plan 02.1-05:
|
||||
- `dev.ulfrx.recipe.ui.screens.shell.AppShell` — composable taking no required params
|
||||
- `dev.ulfrx.recipe.ui.screens.shell.ShellViewModel`
|
||||
|
||||
From plan 02.1-06:
|
||||
- `dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel(searchSource: SearchSource? = null)`
|
||||
- `dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel(searchSource: SearchSource? = null)`
|
||||
|
||||
From plan 02.1-07:
|
||||
- `dev.ulfrx.recipe.ui.screens.planner.{PlannerScreen, PlannerViewModel}`
|
||||
- `dev.ulfrx.recipe.ui.screens.recipes.{RecipesScreen, RecipesViewModel}`
|
||||
- `dev.ulfrx.recipe.ui.screens.pantry.{PantryScreen, PantryViewModel}`
|
||||
- `dev.ulfrx.recipe.ui.screens.shopping.{ShoppingScreen, ShoppingViewModel}`
|
||||
|
||||
From plan 02.1-03:
|
||||
- `dev.ulfrx.recipe.ui.components.glass.{GlassBackend, LocalGlassBackend, resolveGlassBackend, isDebugBuild, DEBUG_GLASS_BACKEND_KEY}`
|
||||
|
||||
From plan 02.1-04:
|
||||
- `dev.ulfrx.recipe.navigation.{PlannerGraph, PlannerHome, RecipesGraph, RecipesHome, PantryGraph, PantryHome, ShoppingGraph, ShoppingHome}`
|
||||
|
||||
Existing analog (`auth/AuthModule.kt:9-25`) — Koin module shape:
|
||||
```kotlin
|
||||
val authModule = module {
|
||||
single<SecureAuthStateStore> { SecureAuthStateStore(get()) }
|
||||
// ...
|
||||
viewModel<LoginViewModel>()
|
||||
viewModel<PostLoginViewModel>()
|
||||
}
|
||||
```
|
||||
|
||||
Existing AppModule (`di/AppModule.kt`):
|
||||
```kotlin
|
||||
val appModule = module {
|
||||
includes(authModule, userModule)
|
||||
}
|
||||
```
|
||||
|
||||
`com.russhwolf:multiplatform-settings:1.3.0` provides `Settings` interface — already on commonMain via Phase 2 (used by SecureAuthStateStore) and registered in Koin.
|
||||
|
||||
Current App.kt structure (App.kt:43-58):
|
||||
```kotlin
|
||||
when (authState) {
|
||||
AuthState.Loading -> SplashScreen()
|
||||
AuthState.Unauthenticated -> LoginScreen(viewModel = koinViewModel<LoginViewModel>())
|
||||
AuthState.Authenticated -> {
|
||||
val user = currentUser
|
||||
if (user == null) {
|
||||
SplashScreen()
|
||||
} else {
|
||||
PostLoginPlaceholderScreen(
|
||||
user = user,
|
||||
viewModel = koinViewModel<PostLoginViewModel>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The modification: replace the `PostLoginPlaceholderScreen(...)` call (lines 53-56) with `AppShell()`. The `currentUser == null → SplashScreen()` arm stays. The `LaunchedEffect(authSession) { initialize() }` block (lines 39-41) stays untouched.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create ShellModule.kt + extend AppModule.kt + provide LocalGlassBackend in RecipeTheme</name>
|
||||
<files>
|
||||
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt,
|
||||
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt,
|
||||
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt
|
||||
</files>
|
||||
<read_first>
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt — analog Koin module shape (lines 9-25)
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt — current state (preserve includes; just append shellModule)
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt — current state from plan 02.1-02 (must preserve MaterialTheme wrapper for legacy auth screens — RESEARCH § Open Questions Q3 RESOLVED)
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackend.kt — for resolveGlassBackend signature
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § di/ShellModule (lines 268-289) + § di/AppModule (lines 293-304)
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-CONTEXT.md D-17 — debug runtime override mechanism
|
||||
</read_first>
|
||||
<action>
|
||||
Step 1 — create `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt`:
|
||||
|
||||
```kotlin
|
||||
package dev.ulfrx.recipe.di
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import dev.ulfrx.recipe.ui.components.glass.GlassBackend
|
||||
import dev.ulfrx.recipe.ui.components.glass.isDebugBuild
|
||||
import dev.ulfrx.recipe.ui.components.glass.resolveGlassBackend
|
||||
import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.shell.ShellViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koin.plugin.module.dsl.viewModel
|
||||
|
||||
/**
|
||||
* Phase 2.1 (UI-03 / UI-04 / UI-09 / UI-10) — DI module for the app-shell layer.
|
||||
*
|
||||
* Registers:
|
||||
* - 4 tab ViewModels (Planner / Recipes / Pantry / Shopping) — pure StateFlow,
|
||||
* no dependencies this phase. Phase 5+ extends each to inject repositories.
|
||||
* - 2 Search ViewModels (Recipes + Pantry) — pure StateFlow with nullable
|
||||
* `searchSource: SearchSource? = null` per RESEARCH § Pattern 4 line 410.
|
||||
* - 1 ShellViewModel — active-tab + search-open state machine.
|
||||
* - 1 GlassBackend single — resolved at composition root from
|
||||
* [resolveGlassBackend] (CONTEXT D-16 / D-17). The default backend chosen here
|
||||
* is [GlassBackend.Liquid] — the iOS+Android primary path; if Liquid fails to
|
||||
* compile for a future target, the per-target source-set actual will pick
|
||||
* [GlassBackend.Haze] or [GlassBackend.Flat] before this resolve runs.
|
||||
*/
|
||||
val shellModule =
|
||||
module {
|
||||
// Glass backend — resolved once at startup. Production builds short-circuit
|
||||
// [resolveGlassBackend] via [isDebugBuild] = false; debug builds may pick up
|
||||
// a runtime override stored in `multiplatform-settings`.
|
||||
single<GlassBackend> {
|
||||
resolveGlassBackend(
|
||||
settings = get<Settings>(),
|
||||
isDebug = isDebugBuild,
|
||||
default = GlassBackend.Liquid,
|
||||
)
|
||||
}
|
||||
|
||||
// Shell-level state machine.
|
||||
viewModel<ShellViewModel>()
|
||||
|
||||
// Tab ViewModels — empty-state-only this phase; feature phases extend them.
|
||||
viewModel<PlannerViewModel>()
|
||||
viewModel<RecipesViewModel>()
|
||||
viewModel<PantryViewModel>()
|
||||
viewModel<ShoppingViewModel>()
|
||||
|
||||
// Per-tab Search ViewModels — pure echo this phase; Phase 5 / 8 inject
|
||||
// their respective SearchSource implementations.
|
||||
viewModel<RecipesSearchViewModel>()
|
||||
viewModel<PantrySearchViewModel>()
|
||||
}
|
||||
```
|
||||
|
||||
Note on `Settings` provider: `Settings` is already registered in Koin via the
|
||||
multiplatform-settings wiring from Phase 1 / Phase 2 (used by `SecureAuthStateStore`).
|
||||
If `get<Settings>()` does not resolve (Koin can't find a Settings binding), then
|
||||
multiplatform-settings was registered scoped or under a different type. In that
|
||||
case, inspect `auth/AuthModule.kt` and the platform-specific Koin modules; either
|
||||
promote the Settings binding to a single<Settings> in commonMain shellModule, or
|
||||
reuse whatever scope SecureAuthStateStore used.
|
||||
|
||||
Step 2 — modify `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`:
|
||||
|
||||
Replace the existing `appModule` declaration with:
|
||||
```kotlin
|
||||
package dev.ulfrx.recipe.di
|
||||
|
||||
import dev.ulfrx.recipe.auth.authModule
|
||||
import dev.ulfrx.recipe.user.userModule
|
||||
import org.koin.dsl.module
|
||||
|
||||
// Phase 2 added authModule + userModule. Phase 2.1 adds shellModule (UI-03/04/09/10).
|
||||
// Phase 4 will add syncModule; Phase 5 will add catalogModule; etc.
|
||||
val appModule =
|
||||
module {
|
||||
includes(authModule, userModule, shellModule)
|
||||
}
|
||||
```
|
||||
|
||||
Step 3 — modify `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` to provide `LocalGlassBackend` to all descendants.
|
||||
|
||||
Plan 02.1-02 produced a `RecipeTheme` composable that wraps `MaterialTheme(...)` and
|
||||
provides `LocalRecipeColors` / `LocalRecipeTypography` / etc. via
|
||||
`CompositionLocalProvider`. THIS plan adds one more local: `LocalGlassBackend`,
|
||||
resolved via `koinInject<GlassBackend>()` at startup.
|
||||
|
||||
Read the current RecipeTheme.kt (post plan 02.1-02). Locate the `CompositionLocalProvider(...)` block.
|
||||
Add `LocalGlassBackend provides koinInject<GlassBackend>()` to the `provides` list.
|
||||
|
||||
Required additional imports in RecipeTheme.kt:
|
||||
```kotlin
|
||||
import dev.ulfrx.recipe.ui.components.glass.GlassBackend
|
||||
import dev.ulfrx.recipe.ui.components.glass.LocalGlassBackend
|
||||
import org.koin.compose.koinInject
|
||||
```
|
||||
|
||||
Conceptual edit (for guidance — actual line numbers depend on plan 02.1-02's output):
|
||||
|
||||
Before:
|
||||
```kotlin
|
||||
@Composable
|
||||
fun RecipeTheme(content: @Composable () -> Unit) {
|
||||
val colors = if (isSystemInDarkTheme()) DarkRecipeColors else LightRecipeColors
|
||||
MaterialTheme(colorScheme = colors.toMaterialColorScheme()) {
|
||||
CompositionLocalProvider(
|
||||
LocalRecipeColors provides colors,
|
||||
LocalRecipeTypography provides RecipeTypographyDefault,
|
||||
LocalRecipeSpacing provides RecipeSpacingDefault,
|
||||
LocalRecipeShapes provides RecipeShapesDefault,
|
||||
LocalRecipeGlass provides RecipeGlassDefault,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```kotlin
|
||||
@Composable
|
||||
fun RecipeTheme(content: @Composable () -> Unit) {
|
||||
val colors = if (isSystemInDarkTheme()) DarkRecipeColors else LightRecipeColors
|
||||
val glassBackend = koinInject<GlassBackend>()
|
||||
MaterialTheme(colorScheme = colors.toMaterialColorScheme()) {
|
||||
CompositionLocalProvider(
|
||||
LocalRecipeColors provides colors,
|
||||
LocalRecipeTypography provides RecipeTypographyDefault,
|
||||
LocalRecipeSpacing provides RecipeSpacingDefault,
|
||||
LocalRecipeShapes provides RecipeShapesDefault,
|
||||
LocalRecipeGlass provides RecipeGlassDefault,
|
||||
LocalGlassBackend provides glassBackend,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The exact symbol names (`LightRecipeColors`, `RecipeTypographyDefault`, `toMaterialColorScheme`)
|
||||
depend on what plan 02.1-02 produced. The contract that matters: `LocalGlassBackend`
|
||||
is now provided via `koinInject<GlassBackend>()` at the same level as the other Recipe locals.
|
||||
|
||||
Append-only: do not remove any existing `provides` entry. Do not change the
|
||||
`MaterialTheme(...)` wrapper (legacy auth screens still depend on it — Open Questions Q3).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'val shellModule' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt` returns 1
|
||||
- All 7 VMs registered: `grep -cE 'viewModel<(ShellViewModel|PlannerViewModel|RecipesViewModel|PantryViewModel|ShoppingViewModel|RecipesSearchViewModel|PantrySearchViewModel)>\(\)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt` returns 7
|
||||
- GlassBackend single registered via resolveGlassBackend: `grep -c 'single<GlassBackend>' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt` returns 1
|
||||
- GlassBackend single uses isDebugBuild: `grep -c 'isDebug = isDebugBuild' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt` returns 1
|
||||
- GlassBackend single defaults to Liquid: `grep -c 'default = GlassBackend.Liquid' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt` returns 1
|
||||
- AppModule extended: `grep -c 'shellModule' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt` returns at least 1
|
||||
- AppModule includes 3 modules: `grep -c 'includes(authModule, userModule, shellModule)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt` returns 1
|
||||
- RecipeTheme provides LocalGlassBackend: `grep -c 'LocalGlassBackend provides' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1
|
||||
- RecipeTheme uses koinInject<GlassBackend>: `grep -c 'koinInject<GlassBackend>' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1
|
||||
- MaterialTheme wrapper preserved (Open Questions Q3): `grep -c 'MaterialTheme' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns at least 1
|
||||
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>shellModule registers 7 VMs + 1 GlassBackend single; AppModule pulls it in; RecipeTheme provides LocalGlassBackend via koinInject so all descendants of RecipeTheme see the resolved backend.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Replace TabHomePlaceholder stubs in RootNavHost.kt with real Tab*Screen calls + per-tab VM scoping</name>
|
||||
<files>composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt</files>
|
||||
<read_first>
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt — current state from plan 02.1-04 (placeholder stubs in each tab's composable<*Home> block)
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/planner/PlannerScreen.kt — from plan 02.1-07
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesScreen.kt — from plan 02.1-07
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantryScreen.kt — from plan 02.1-07
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shopping/ShoppingScreen.kt — from plan 02.1-07
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Pattern 2 (lines 343-360) — verbatim koinViewModel(viewModelStoreOwner = parent) idiom
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § Tab screens (lines 206-238) + § App.kt (lines 99-122)
|
||||
</read_first>
|
||||
<action>
|
||||
Open `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` (state from plan 02.1-04 has 4 placeholder `TabHomePlaceholder(...)` calls).
|
||||
|
||||
Replace each `TabHomePlaceholder(name = "...", parent = parent)` call with a real
|
||||
`koinViewModel<TabViewModel>(viewModelStoreOwner = parent)` lookup followed by the
|
||||
real `Tab*Screen(viewModel = vm)` call. Then DELETE the now-unused
|
||||
`TabHomePlaceholder` private composable at the bottom of the file.
|
||||
|
||||
Required new imports:
|
||||
```kotlin
|
||||
import dev.ulfrx.recipe.ui.screens.planner.PlannerScreen
|
||||
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.recipes.RecipesScreen
|
||||
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.pantry.PantryScreen
|
||||
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
|
||||
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingScreen
|
||||
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
```
|
||||
|
||||
Imports to REMOVE:
|
||||
```kotlin
|
||||
import androidx.compose.foundation.text.BasicText // (or whatever placeholder Text was used)
|
||||
import androidx.compose.foundation.layout.Box // if no longer needed elsewhere in the file
|
||||
```
|
||||
|
||||
Resulting per-tab block (Planner shown — repeat for Recipes / Pantry / Shopping):
|
||||
```kotlin
|
||||
navigation<PlannerGraph>(startDestination = PlannerHome) {
|
||||
composable<PlannerHome> { entry ->
|
||||
val parent = remember(entry) {
|
||||
navController.getBackStackEntry(PlannerGraph)
|
||||
}
|
||||
val vm: PlannerViewModel = koinViewModel(viewModelStoreOwner = parent)
|
||||
PlannerScreen(viewModel = vm)
|
||||
}
|
||||
// future: composable<PlannerDetail>{ ... }
|
||||
}
|
||||
```
|
||||
|
||||
Same shape for the other three tabs:
|
||||
```kotlin
|
||||
navigation<RecipesGraph>(startDestination = RecipesHome) {
|
||||
composable<RecipesHome> { entry ->
|
||||
val parent = remember(entry) { navController.getBackStackEntry(RecipesGraph) }
|
||||
val vm: RecipesViewModel = koinViewModel(viewModelStoreOwner = parent)
|
||||
RecipesScreen(viewModel = vm)
|
||||
}
|
||||
}
|
||||
|
||||
navigation<PantryGraph>(startDestination = PantryHome) {
|
||||
composable<PantryHome> { entry ->
|
||||
val parent = remember(entry) { navController.getBackStackEntry(PantryGraph) }
|
||||
val vm: PantryViewModel = koinViewModel(viewModelStoreOwner = parent)
|
||||
PantryScreen(viewModel = vm)
|
||||
}
|
||||
}
|
||||
|
||||
navigation<ShoppingGraph>(startDestination = ShoppingHome) {
|
||||
composable<ShoppingHome> { entry ->
|
||||
val parent = remember(entry) { navController.getBackStackEntry(ShoppingGraph) }
|
||||
val vm: ShoppingViewModel = koinViewModel(viewModelStoreOwner = parent)
|
||||
ShoppingScreen(viewModel = vm)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
DELETE the trailing `private fun TabHomePlaceholder(...)` composable that was added
|
||||
by plan 02.1-04 — it has no remaining call sites.
|
||||
|
||||
The `// TODO(02.1-08): replace with ...` comments should also be deleted (the work
|
||||
they reference is done).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- All 4 Tab*Screen composables called: `grep -cE '(PlannerScreen|RecipesScreen|PantryScreen|ShoppingScreen)\(viewModel = vm\)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 4
|
||||
- All 4 koinViewModel calls with viewModelStoreOwner: `grep -c 'koinViewModel(viewModelStoreOwner = parent)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 4
|
||||
- All 4 getBackStackEntry calls remain: `grep -c 'getBackStackEntry' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 4
|
||||
- TabHomePlaceholder is deleted: `grep -c 'TabHomePlaceholder' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 0
|
||||
- TODO markers from plan 02.1-04 are cleared: `grep -c 'TODO(02.1-08)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 0
|
||||
- All 4 navigation<*Graph> blocks preserved: `grep -cE 'navigation<(Planner|Recipes|Pantry|Shopping)Graph>' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 4
|
||||
- startDestination = PlannerGraph preserved: `grep -c 'startDestination = PlannerGraph' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 1
|
||||
- Material 3 boundary still preserved: `grep -c 'androidx.compose.material3' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt` returns 0
|
||||
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>RootNavHost wires the four real tab screens with per-tab VM scoping per RESEARCH § Pattern 2; all placeholder code is gone; tab navigation graph is the production shape feature phases inherit.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Swap App.kt's Authenticated branch from PostLoginPlaceholderScreen to AppShell + extract testable RootRouter</name>
|
||||
<files>composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt</files>
|
||||
<read_first>
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt — current state (the Authenticated branch on lines 48-58)
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthState.kt — AuthState enum/sealed
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShell.kt — from plan 02.1-05
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § App.kt (lines 99-122) — modification contract
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-CONTEXT.md line 101 — keep PostLoginPlaceholderScreen as logout-bridge possibility
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Open Questions Q3 (RESOLVED) — auth screens stay as Material 3 legacy
|
||||
</read_first>
|
||||
<action>
|
||||
Open `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt`.
|
||||
|
||||
Step 1 — extract a pure routing helper function so the routing logic is unit-testable
|
||||
(V-04 anchor). Add at the top of the file (after imports, before `@Composable fun App()`):
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Pure routing decision for [App] — facilitates unit testing of the auth gate.
|
||||
* Maps an [AuthState] + nullable currentUser to one of three top-level branches.
|
||||
*/
|
||||
enum class RootRoute { Splash, Login, Shell }
|
||||
|
||||
/**
|
||||
* Pure helper — returned route is what [App] should render. Unit-tested in
|
||||
* AppShellGateTest (V-04).
|
||||
*/
|
||||
internal fun resolveRootRoute(authState: AuthState, hasCurrentUser: Boolean): RootRoute =
|
||||
when (authState) {
|
||||
AuthState.Loading -> RootRoute.Splash
|
||||
AuthState.Unauthenticated -> RootRoute.Login
|
||||
AuthState.Authenticated -> if (hasCurrentUser) RootRoute.Shell else RootRoute.Splash
|
||||
}
|
||||
```
|
||||
|
||||
Step 2 — modify the `App()` composable body. Replace lines 43-58 (the `when (authState) { ... }` block) with a use of `resolveRootRoute(...)`:
|
||||
|
||||
Before:
|
||||
```kotlin
|
||||
when (authState) {
|
||||
AuthState.Loading -> SplashScreen()
|
||||
AuthState.Unauthenticated -> LoginScreen(viewModel = koinViewModel<LoginViewModel>())
|
||||
AuthState.Authenticated -> {
|
||||
val user = currentUser
|
||||
if (user == null) {
|
||||
SplashScreen()
|
||||
} else {
|
||||
PostLoginPlaceholderScreen(
|
||||
user = user,
|
||||
viewModel = koinViewModel<PostLoginViewModel>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```kotlin
|
||||
when (resolveRootRoute(authState, hasCurrentUser = currentUser != null)) {
|
||||
RootRoute.Splash -> SplashScreen()
|
||||
RootRoute.Login -> LoginScreen(viewModel = koinViewModel<LoginViewModel>())
|
||||
RootRoute.Shell -> AppShell()
|
||||
}
|
||||
```
|
||||
|
||||
Step 3 — clean up imports. ADD:
|
||||
```kotlin
|
||||
import dev.ulfrx.recipe.ui.screens.shell.AppShell
|
||||
```
|
||||
|
||||
REMOVE (no longer used in the routing branch — but keep them if anything else in the
|
||||
file still references them; at the time this plan runs, the only reference site
|
||||
was the placeholder branch, so they should be safe to drop):
|
||||
```kotlin
|
||||
import dev.ulfrx.recipe.ui.screens.auth.PostLoginPlaceholderScreen
|
||||
import dev.ulfrx.recipe.ui.screens.auth.PostLoginViewModel
|
||||
```
|
||||
|
||||
HOWEVER: per CONTEXT line 101 + RESEARCH § Open Questions Q3 (RESOLVED), DO NOT
|
||||
delete the `PostLoginPlaceholderScreen.kt` and `PostLoginViewModel.kt` source files
|
||||
themselves. They remain in the codebase as a logout-bridge possibility — a future
|
||||
phase may revive them or repurpose them. Only the imports and the call site in App.kt
|
||||
are removed.
|
||||
|
||||
Step 4 — preserve the rest of the file:
|
||||
- The `@Composable @Preview fun App()` declaration
|
||||
- The `RecipeTheme { ... }` wrapper
|
||||
- The `koinInject<AuthSession>()` and `koinInject<UserRepository>()` calls
|
||||
- The `collectAsStateWithLifecycle()` observations
|
||||
- The `LaunchedEffect(authSession) { authSession.initialize() }` block — this is
|
||||
load-bearing per CONTEXT and the docstring on line 20-25.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Authenticated branch routes to AppShell: `grep -c 'RootRoute.Shell -> AppShell()' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- PostLoginPlaceholderScreen no longer called in App.kt: `grep -c 'PostLoginPlaceholderScreen' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 0
|
||||
- PostLoginViewModel no longer imported / called in App.kt: `grep -c 'PostLoginViewModel' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 0
|
||||
- Pure routing helper extracted: `grep -c 'fun resolveRootRoute' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- RootRoute enum declared: `grep -c 'enum class RootRoute' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- LaunchedEffect preserved: `grep -c 'LaunchedEffect(authSession)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- RecipeTheme wrapper preserved: `grep -c 'RecipeTheme {' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- SplashScreen still used: `grep -c 'SplashScreen()' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns at least 1
|
||||
- LoginScreen still used: `grep -c 'LoginScreen(' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- AppShell imported: `grep -c 'import dev.ulfrx.recipe.ui.screens.shell.AppShell' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- PostLoginPlaceholderScreen.kt + PostLoginViewModel.kt source files still exist on disk: `test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt && test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt`
|
||||
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>App() routes Authenticated+user to AppShell instead of PostLoginPlaceholderScreen. The pure routing helper resolveRootRoute is extracted and ready for V-04 unit testing. PostLoginPlaceholderScreen / PostLoginViewModel source files remain on disk per Open Questions Q3.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Replace @Ignore stub in AppShellGateTest.kt with real assertion that resolveRootRoute(Authenticated, hasUser=true) → Shell (V-04)</name>
|
||||
<files>composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt</files>
|
||||
<read_first>
|
||||
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt — current Wave-0 stub
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt — for resolveRootRoute helper just-added
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthState.kt — AuthState shape
|
||||
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt — kotlin.test pattern shape
|
||||
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-VALIDATION.md § Per-Task Verification Map V-04 (line 49)
|
||||
</read_first>
|
||||
<action>
|
||||
Replace the Wave-0 `@Ignore`'d body of `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` with:
|
||||
|
||||
```kotlin
|
||||
package dev.ulfrx.recipe.ui.screens.shell
|
||||
|
||||
import dev.ulfrx.recipe.RootRoute
|
||||
import dev.ulfrx.recipe.auth.AuthState
|
||||
import dev.ulfrx.recipe.resolveRootRoute
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* V-04 — UI-09 — App.kt's `Authenticated + currentUser != null` branch resolves to
|
||||
* the AppShell route, not PostLoginPlaceholderScreen.
|
||||
*
|
||||
* Tested via the pure [resolveRootRoute] helper extracted in plan 02.1-08, so the
|
||||
* routing semantics are deterministic without instrumenting a real Compose
|
||||
* composition. (The CMP iOS Compose UI testing surface is too immature this phase
|
||||
* for snapshot/UI tests on the actual `App()` composable — VALIDATION.md line 27.)
|
||||
*/
|
||||
class AppShellGateTest {
|
||||
@Test
|
||||
fun authenticatedWithUser_routesToShell_notPlaceholder() {
|
||||
val route = resolveRootRoute(
|
||||
authState = AuthState.Authenticated,
|
||||
hasCurrentUser = true,
|
||||
)
|
||||
assertEquals(RootRoute.Shell, route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authenticatedWithoutUserYet_routesToSplash() {
|
||||
// Two-layer gate per App.kt docstring lines 20-25: tokens present but
|
||||
// /me has not returned yet → hold on splash, never show empty post-login.
|
||||
val route = resolveRootRoute(
|
||||
authState = AuthState.Authenticated,
|
||||
hasCurrentUser = false,
|
||||
)
|
||||
assertEquals(RootRoute.Splash, route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unauthenticated_routesToLogin() {
|
||||
val route = resolveRootRoute(
|
||||
authState = AuthState.Unauthenticated,
|
||||
hasCurrentUser = false,
|
||||
)
|
||||
assertEquals(RootRoute.Login, route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadingAuth_routesToSplash() {
|
||||
val route = resolveRootRoute(
|
||||
authState = AuthState.Loading,
|
||||
hasCurrentUser = false,
|
||||
)
|
||||
assertEquals(RootRoute.Splash, route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadingAuthIgnoresHasCurrentUser() {
|
||||
// Defensive: while Loading, we should always splash regardless of whether
|
||||
// a stale currentUser is observable from a previous session.
|
||||
val route = resolveRootRoute(
|
||||
authState = AuthState.Loading,
|
||||
hasCurrentUser = true,
|
||||
)
|
||||
assertEquals(RootRoute.Splash, route)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Drop the `@Ignore` import and annotation. Use `kotlin.test` only.
|
||||
|
||||
Note: the imports `dev.ulfrx.recipe.RootRoute` and `dev.ulfrx.recipe.resolveRootRoute`
|
||||
target the helpers added in App.kt (top-level declarations in the `dev.ulfrx.recipe`
|
||||
package). Confirm the package matches App.kt's `package dev.ulfrx.recipe` line.
|
||||
`resolveRootRoute` should be `internal` (visible from commonTest in the same module).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:commonTest --tests "dev.ulfrx.recipe.ui.screens.shell.AppShellGateTest" -q</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c '@Ignore' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns 0
|
||||
- `grep -c '@Test' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns at least 5
|
||||
- V-04 anchor test name present: `grep -c 'authenticatedWithUser_routesToShell_notPlaceholder' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns 1
|
||||
- Two-layer gate covered: `grep -c 'authenticatedWithoutUserYet_routesToSplash' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns 1
|
||||
- Imports resolveRootRoute: `grep -c 'import dev.ulfrx.recipe.resolveRootRoute' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns 1
|
||||
- Imports RootRoute: `grep -c 'import dev.ulfrx.recipe.RootRoute' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns 1
|
||||
- `./gradlew :composeApp:commonTest --tests "dev.ulfrx.recipe.ui.screens.shell.AppShellGateTest" -q` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>AppShellGateTest contains 5 passing assertions covering all four AuthState × hasCurrentUser combinations. V-04 anchor backed by real assertions; UI-09's auth-gate-to-shell routing is deterministically tested.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- iOS K/N compile green: `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0
|
||||
- Android compile green: `./gradlew :composeApp:compileDebugKotlinAndroid -q` exits 0
|
||||
- Full commonTest green: `./gradlew :composeApp:commonTest -q` exits 0
|
||||
- Full check green: `./gradlew :composeApp:check -q` exits 0
|
||||
- iOS framework links: `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 -q` exits 0
|
||||
- All V-anchors V-01..V-07 are now covered by passing tests (no @Ignore left in any test file): `grep -rE '@Ignore' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ | wc -l` returns 0
|
||||
- App.kt routes Authenticated+user to AppShell: `grep -c 'RootRoute.Shell -> AppShell()' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` returns 1
|
||||
- AppModule pulls in shellModule: `grep -c 'shellModule' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt` returns at least 1
|
||||
- Material 3 boundary preserved across plan-08 changes: `grep -rc 'androidx.compose.material3' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt` returns 0
|
||||
- PostLoginPlaceholderScreen.kt + PostLoginViewModel.kt source files preserved on disk
|
||||
- Wave 0 ALL test stubs un-ignored across the phase
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. ShellModule.kt registers 7 ViewModels (ShellViewModel, 4 tab VMs, 2 Search VMs) and 1 GlassBackend single resolved via resolveGlassBackend(get<Settings>(), isDebugBuild, default = GlassBackend.Liquid).
|
||||
2. AppModule.kt's `includes(...)` pulls in shellModule alongside authModule + userModule.
|
||||
3. RecipeTheme.kt provides LocalGlassBackend via koinInject<GlassBackend>() at the same level as other Recipe locals; the MaterialTheme(...) wrapper is preserved (Open Questions Q3 RESOLVED — legacy auth screens keep working).
|
||||
4. RootNavHost.kt's four TabHomePlaceholder stubs are replaced with real `koinViewModel<*ViewModel>(viewModelStoreOwner = parent)` lookups followed by `*Screen(viewModel = vm)` calls (RESEARCH § Pattern 2). The placeholder helper is deleted.
|
||||
5. App.kt routes `Authenticated + currentUser != null` → `AppShell()` via the extracted pure `resolveRootRoute(...)` helper. `LaunchedEffect(authSession) { initialize() }` and `currentUser == null → SplashScreen()` arms are preserved. PostLoginPlaceholderScreen / PostLoginViewModel source files stay on disk per CONTEXT line 101.
|
||||
6. V-04 anchor: AppShellGateTest passes 5 assertions covering all AuthState × hasCurrentUser combinations.
|
||||
7. No @Ignore'd tests remain anywhere in commonTest — all Wave-0 stubs are now backed by real assertions (V-01..V-07).
|
||||
8. Full `./gradlew :composeApp:check` green.
|
||||
9. UI-09 final closure: signed-in user lands in the real shell with all four tabs accessible; default landing tab is Planner (D-03); each tab renders its anticipatory empty state (D-10/D-11); search affordance visible only on Recipes + Pantry (D-06).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-08-SUMMARY.md` per `$HOME/.claude/get-shit-done/templates/summary.md`. Record:
|
||||
- Whether `Settings` was already registered in Koin commonMain (Phase 2 wiring) or whether shellModule had to register it.
|
||||
- The final exact form of the RecipeTheme.kt edit (which `provides` line was added; preserved structure).
|
||||
- Confirmation that PostLoginPlaceholderScreen.kt and PostLoginViewModel.kt source files remain on disk (logout-bridge per Open Questions Q3 RESOLVED).
|
||||
- Manual smoke test results from V-08 / V-09 / V-10 / V-11 (iOS simulator runbook): default Planer landing, tab back-stack preservation across reselect, search affordance scoped to Recipes + Pantry, Liquid dock animation visible (or flat fallback if Liquid did not resolve on the device path).
|
||||
</output>
|
||||
Reference in New Issue
Block a user