--- phase: 02.1 plan: 08 subsystem: app-shell-final-integration tags: [koin, di, navigation, glass, app-entry, integration] requires: - 02.1-02-SUMMARY (RecipeTheme + LocalRecipe* providers) - 02.1-03-SUMMARY (GlassBackend / LocalGlassBackend / resolveGlassBackend) - 02.1-04-SUMMARY (RootNavHost skeleton + per-tab graphs) - 02.1-05-SUMMARY (AppShell composable) - 02.1-06-SUMMARY (Recipes/Pantry SearchViewModels) - 02.1-07-SUMMARY (Tab screens + tab ViewModels) provides: - shellModule (Koin) — registers 4 tab VMs + 2 search VMs + ShellViewModel + GlassBackend single - resolveRootRoute(AuthState, hasCurrentUser) — pure routing helper for V-04 unit testing - RootRoute enum (Splash / Login / Shell) - LocalGlassBackend wired through RecipeTheme - Authenticated users now land in AppShell (UI-09 closure) affects: - 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/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 tech-stack: added: [] patterns: - Pure routing helper extracted for unit testing (RootRoute enum + resolveRootRoute) - Per-tab koinViewModel(viewModelStoreOwner = parent) scoping (RESEARCH § Pattern 2) - GlassBackend resolved at composition root and provided via CompositionLocal key-files: created: - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt modified: - 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/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 decisions: - Routing logic extracted to a pure resolveRootRoute helper so V-04 can unit-test the auth gate without instrumenting Compose composition. - PostLoginPlaceholderScreen and PostLoginViewModel source files preserved (logout-bridge per CONTEXT line 101 / Open Questions Q3 RESOLVED) — only the imports + call site removed from App.kt. - GlassBackend default = Liquid (iOS+Android primary path, CONTEXT D-16). metrics: duration: ~25 min completed: 2026-05-08 requirements: [UI-09] --- # Phase 02.1 Plan 08: Final Integration Summary Wire the seven shell ViewModels and the GlassBackend resolver into a Koin shellModule, extend appModule.includes, provide LocalGlassBackend through RecipeTheme, replace the four TabHomePlaceholder stubs in RootNavHost with the real Tab*Screen composables, and swap App.kt's Authenticated branch from PostLoginPlaceholderScreen to AppShell — closing UI-09. ## What landed ### Task 1 — ShellModule + AppModule + RecipeTheme glass provider — `9714765` - New `di/ShellModule.kt` registers: - `single { resolveGlassBackend(get(), isDebugBuild, default = GlassBackend.Liquid) }` - `viewModel()` - 4 tab VMs (`Planner` / `Recipes` / `Pantry` / `Shopping`) - 2 search VMs (`RecipesSearchViewModel` / `PantrySearchViewModel`) - `di/AppModule.kt` extended: `includes(authModule, userModule, shellModule)` - `ui/theme/RecipeTheme.kt` adds one new `provides` entry — `LocalGlassBackend provides koinInject()` — at the same level as the other Recipe locals. The `MaterialTheme(...)` wrapper is preserved unchanged so legacy auth screens (Login / PostLoginPlaceholder / Splash) keep resolving `MaterialTheme.colorScheme.*` (Open Question Q3 RESOLVED). - Settings binding: registered in `auth/IosAuthModule.kt` and `auth/AndroidAuthModule.kt` (Phase 2 wiring for SecureAuthStateStore) — reused by shellModule, no commonMain Settings binding was needed. ### Task 2 — RootNavHost wires real tab screens — `20e840e` - All four `TabHomePlaceholder(...)` calls replaced with `koinViewModel<*ViewModel>(viewModelStoreOwner = parent)` lookups followed by the real `*Screen(viewModel = vm)` calls. - `private fun TabHomePlaceholder(...)` deleted; placeholder imports (`BasicText`, `Box`) removed. - All four `TODO(02.1-08)` markers cleared. - Each tab's ViewModelStoreOwner remains the parent graph's `NavBackStackEntry`, so tab VMs survive across home-detail navigations within the graph (RESEARCH § Pattern 2). ### Task 3 — App.kt routes Authenticated to AppShell — `2639244` - New top-level `enum class RootRoute { Splash, Login, Shell }` and `internal fun resolveRootRoute(authState, hasCurrentUser): RootRoute`. - `App()` body now switches on `resolveRootRoute(authState, currentUser != null)` with three branches: Splash / Login / Shell. Authenticated + user goes to `AppShell()`; Authenticated + null user still holds on `SplashScreen()`. - `LaunchedEffect(authSession) { initialize() }` and the `RecipeTheme { ... }` wrapper preserved verbatim. - `PostLoginPlaceholderScreen.kt` and `PostLoginViewModel.kt` source files remain on disk (CONTEXT line 101 / Open Question Q3 RESOLVED — logout-bridge possibility). Only their imports and the call site in App.kt are removed. ### Task 4 — AppShellGateTest backed by real assertions (V-04) — `26392df` - `@Ignore` removed; five assertions cover all `AuthState × hasCurrentUser` combinations: 1. `Authenticated + user → Shell` (V-04 anchor) 2. `Authenticated + null user → Splash` (two-layer gate) 3. `Unauthenticated → Login` 4. `Loading → Splash` 5. `Loading + stale user → Splash` (defensive) - Tests run through the pure `resolveRootRoute` helper, sidestepping the immature CMP iOS Compose UI testing surface (VALIDATION.md line 27). - All Wave-0 `@Ignore` stubs across the phase are now backed by real assertions: `grep -r '@Ignore' composeApp/src/commonTest/` returns 0. ### Task 5 — spotless formatting — `a6f0d46` - Spotless reformatted plan-08 files (App.kt, RootNavHost.kt, RecipeTheme.kt) — multi-line function signature for `resolveRootRoute`, multi-line `remember` blocks. Only changes to plan-08 files committed; pre-existing spotless violations in unrelated files (LokksmithOidcSupport, OidcClient, AuthSession, etc.) left out of scope per Rule SCOPE BOUNDARY — those failures predate this plan and require their own cleanup pass. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 — Blocking import]** Initial ShellModule.kt used `org.koin.core.module.dsl.viewModel` which expects a `definition` lambda; the no-arg `viewModel()` form lives in `org.koin.plugin.module.dsl.viewModel` (matching `auth/AuthModule.kt`). - Files modified: `di/ShellModule.kt` - Resolved before any commit; rolled into Task 1. **2. [Rule 3 — Blocking lint]** Spotless reformatted plan-08 files (multi-line function param lists, multi-line `remember` blocks). The wider repo has 38 pre-existing spotless violations in unrelated files; per scope boundary, only the in-scope formatting was committed (`a6f0d46`). The pre-existing violations were confirmed to predate this plan via `git stash` + `spotlessCheck` before the plan's edits. ## RecipeTheme.kt edit (final form) The single in-scope change was adding the `LocalGlassBackend provides glassBackend` entry alongside the existing four `LocalRecipe*` entries: ```kotlin @Composable public fun RecipeTheme(content: @Composable () -> Unit) { val dark = isSystemInDarkTheme() val recipeColors = if (dark) DarkRecipeColors else LightRecipeColors val materialColors = if (dark) LegacyMaterialDarkColors else LegacyMaterialLightColors val glassBackend = koinInject() MaterialTheme(colorScheme = materialColors) { androidx.compose.runtime.CompositionLocalProvider( LocalRecipeColors provides recipeColors, LocalRecipeTypography provides DefaultRecipeTypography, LocalRecipeSpacing provides DefaultRecipeSpacing, LocalRecipeShapes provides DefaultRecipeShapes, LocalRecipeGlass provides DefaultRecipeGlass, LocalGlassBackend provides glassBackend, content = content, ) } } ``` The `MaterialTheme(colorScheme = materialColors)` wrapper is unchanged (Open Question Q3 RESOLVED — legacy auth screens still depend on it). ## Settings registration check `com.russhwolf.settings.Settings` is bound as a `single` in: - `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/IosAuthModule.kt:25` - `composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/AndroidAuthModule.kt:19` Phase 2 introduced this for `SecureAuthStateStore`. shellModule reuses the same binding — no commonMain `single` was required. ## PostLoginPlaceholderScreen / PostLoginViewModel preservation Both source files remain on disk: ``` composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt ``` They are no longer reachable from `App.kt` — kept as a logout-bridge possibility per CONTEXT line 101 / Open Question Q3 (RESOLVED). A future phase may revive or repurpose them. ## Manual smoke (V-08 / V-09 / V-10 / V-11) Manual iOS-simulator smoke deferred — no simulator in this autonomous run. Static checks performed: - `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` → exits 0 - `./gradlew :composeApp:compileDebugKotlinAndroid -q` → exits 0 - `./gradlew :composeApp:iosSimulatorArm64Test --tests "...AppShellGateTest"` → tests pass - `grep -r '@Ignore' composeApp/src/commonTest/` → 0 results `./gradlew :composeApp:check` is RED only because of pre-existing spotless violations in 38 unrelated files (predates this plan; confirmed via stash). ## Self-Check: PASSED - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/ShellModule.kt — FOUND - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt — FOUND (modified) - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt — FOUND (modified) - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/RootNavHost.kt — FOUND (modified) - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt — FOUND (modified) - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt — FOUND (un-ignored) - Commit 9714765 — FOUND (Task 1) - Commit 20e840e — FOUND (Task 2) - Commit 2639244 — FOUND (Task 3) - Commit 26392df — FOUND (Task 4) - Commit a6f0d46 — FOUND (style)