--- phase: 02-authentication-foundation plan: 07 subsystem: auth tags: [kmp, compose-multiplatform, material3, koin-viewmodel, compose-resources, auth-gate] requires: - phase: 02-authentication-foundation provides: 02-06 AuthSession StateFlow, AuthState model, authModule Koin singletons provides: - SplashScreen / LoginScreen / PostLoginPlaceholderScreen Phase 2 auth gate - LoginViewModel + LoginScreenState + PostLoginViewModel mapping AuthSession results to Compose Resources - Compose Resources strings for the seven Phase 2 auth keys - RecipeTheme Material 3 light/dark seed with primary `#3B6939` / `#A2D597` affects: [phase-03-households] tech-stack: added: - kotlinx-coroutines-test (commonTest only) for the multiplatform `runTest` runtime patterns: - "App.kt observes AuthSession.state via collectAsStateWithLifecycle and renders one of three screens; no manual navigation." - "LoginViewModel.onSignInClick() returns the launched Job so commonTest can join() deterministically without dragging in a TestDispatcher." - "ViewModels registered in authModule via org.koin.core.module.dsl.viewModel; consumed via koinViewModel()." - "All commonTest coroutine tests use kotlinx.coroutines.test.runTest so wasmJs can compile (runBlocking is JVM/Native-only)." key-files: created: - composeApp/src/commonMain/composeResources/values/strings.xml - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/SplashScreen.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModel.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModelTest.kt - .planning/phases/02-authentication-foundation/deferred-items.md modified: - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt - composeApp/build.gradle.kts - gradle/libs.versions.toml key-decisions: - "ViewModels registered in authModule (alongside AuthSession) instead of a new uiModule — keeps the single Koin module that owns AuthSession also owning its UI consumers." - "LoginViewModel.onSignInClick() returns Job rather than swallowing it so tests deterministically join without a TestDispatcher; production callers ignore the returned Job." - "AuthSession.initialize() is launched from a LaunchedEffect in App.kt rather than a Koin lifecycle hook; keeps Phase 2 startup explicit and easy to trace." - "Pre-existing ./gradlew check failures (Android JVM SecureAuthStateStoreContractTest, ios SecureAuthStateStore ktlint) are out of scope for 02-07 and tracked in deferred-items.md per scope-boundary rule." requirements-completed: [AUTH-01, AUTH-04, AUTH-05] duration: 10m completed: 2026-04-28 --- # Phase 02 Plan 07: Auth UI Gate Summary **Phase 2 auth UI gate — SplashScreen / LoginScreen / PostLoginPlaceholderScreen wired to AuthSession via koinViewModel, with externalized Polish strings and a Material 3 seed theme.** ## Performance - **Duration:** ~10 min (automated tasks) - **Started:** 2026-04-28T15:31:20Z - **Automated work completed:** 2026-04-28T15:41:31Z - **Tasks completed:** 2 of 3 (Task 3 awaits manual iOS Authentik UAT) - **Files created:** 9 - **Files modified:** 5 ## Accomplishments - Added all seven Phase 2 Compose Resources keys with the Polish scaffold copy from `02-UI-SPEC.md`. - Added `RecipeTheme` with light/dark Material 3 schemes seeded by `#3B6939` / `#A2D597` and `isSystemInDarkTheme()`. - Replaced the JetBrains template `App()` body with the auth-gate `when` over `AuthSession.state`, observing via `collectAsStateWithLifecycle` and kicking `AuthSession.initialize()` from `LaunchedEffect`. - Implemented `SplashScreen`, `LoginScreen`, and `PostLoginPlaceholderScreen` using Material 3 stdlib only — no Scaffold, no Haze, all strings via `stringResource(Res.string.*)`. - Implemented `LoginViewModel` (mapping AuthSession failures → `auth_error_*` `StringResource` keys, clearing stale errors on retry) and trivial `PostLoginViewModel.onSignOutClick()` delegating to `AuthSession.logout()`. - Registered both ViewModels in `authModule` via `org.koin.core.module.dsl.viewModel`. - Added `kotlinx-coroutines-test` to `commonTest` so the wasmJs target can compile coroutine tests (replacing JVM-only `runBlocking` with multiplatform `runTest` in both `LoginViewModelTest` and the existing `AuthSessionTest`). ## Task Commits 1. **Task 1 (RED): Compose Resources, theme seed, failing LoginViewModel tests** — `466e4c7` (test) 2. **Task 2 (GREEN): Auth screens, ViewModels, App auth gate** — `88f4898` (feat) 3. **Task 2 follow-up: switch commonTest to runTest for wasmJs compatibility** — `570652c` (fix) 4. **Task 3 (manual UAT): pending — see Awaiting User UAT below** ## Files Created/Modified ### Created - `composeApp/src/commonMain/composeResources/values/strings.xml` — Phase 2 auth strings (Polish scaffold). - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` — Material 3 seed theme. - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/SplashScreen.kt` — wordmark + progress. - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt` — wordmark + button + inline error. - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModel.kt` — `LoginScreenState` + `onSignInClick()` mapping. - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt` — welcome + logout. - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt` — `onSignOutClick()` → `AuthSession.logout()`. - `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModelTest.kt` — five tests covering cancelled/network/unknown/success and clear-error-on-retry. - `.planning/phases/02-authentication-foundation/deferred-items.md` — log of pre-existing failures. ### Modified - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` — auth-gate `when` body. - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt` — `viewModel { ... }` registrations. - `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt` — `runBlocking` → `runTest`. - `composeApp/build.gradle.kts` — `commonTest` `kotlinx-coroutines-test` dependency. - `gradle/libs.versions.toml` — added `kotlinx-coroutinesTest` library entry. ## Decisions Made See frontmatter `key-decisions`. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 — Blocking] commonTest coroutine tests must use `runTest`, not `runBlocking`** - **Found during:** Task 2 verification, when `./gradlew check` ran the wasmJs test target for the first time. - **Issue:** `kotlinx.coroutines.runBlocking` is JVM/Native-only and breaks `:composeApp:compileTestKotlinWasmJs`. The pre-existing `AuthSessionTest` (committed in Plan 02-06) used the same pattern and was never wasmJs-tested — `02-06` only ran `:composeApp:jvmTest`. Phase 02-07's verification gate is the first one to catch it. - **Fix:** Added `org.jetbrains.kotlinx:kotlinx-coroutines-test` to `commonTest`, switched both `AuthSessionTest` and the new `LoginViewModelTest` from `runBlocking` to `runTest`. - **Files modified:** `composeApp/build.gradle.kts`, `gradle/libs.versions.toml`, `AuthSessionTest.kt`, `LoginViewModelTest.kt`. - **Verification:** `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 :composeApp:compileTestKotlinWasmJs` exits 0. - **Committed in:** `570652c`. ### Out-of-scope discoveries (not fixed; logged) See `deferred-items.md`: - `SecureAuthStateStoreContractTest` (Android JVM unit) fails on `master` HEAD before any 02-07 change — Android Keystore unavailable in plain JVM unit tests; needs Robolectric or `androidTest`. - `composeApp/src/iosMain/.../SecureAuthStateStore.ios.kt:L31` ktlint `property-naming` violation pre-exists on `master`. Both originate in Plans 02-04 / 02-05 and are out of scope for this UI plan per the executor scope-boundary rule. ## Issues Encountered - `./gradlew spotlessApply` reformatted many pre-existing files unrelated to 02-07 (because the repo had pre-existing format drift). Those reformats were reverted before commit so the 02-07 commits stay scope-clean. Spotless's failure on the unrelated `SecureAuthStateStore.ios.kt` ktlint rule is logged in `deferred-items.md`. ## Known Stubs None. The auth gate is fully wired end to end; all rendered text is sourced from Compose Resources, and ViewModels delegate to the real `AuthSession` Koin singleton. The `PostLoginPlaceholderScreen` itself is a Phase 2 placeholder by design — Phase 3's `HouseholdGate` replaces it. This is documented in `02-UI-SPEC.md` and `02-CONTEXT.md` and is not a stub. ## Threat Flags None beyond the plan's threat model. The new UI surfaces only render strings and dispatch to `AuthSession`; tokens are never logged or rendered (T-02-07-01). Logout (T-02-07-02) is the only state-changing action wired in `PostLoginViewModel`. Login button explicitly mentions Authentik (T-02-07-03). Refresh failures route silently to `LoginScreen` per `02-UI-SPEC.md`'s refresh-failure UX (T-02-07-04). All copy comes from `composeResources/values/strings.xml` (T-02-07-05). ## User Setup Required **Manual iOS Authentik UAT (Task 3 — checkpoint:human-verify, blocking):** Per `02-VALIDATION.md` § Manual-Only Verifications and `docs/authentik-setup.md`: 1. Fresh install opens Splash then `LoginScreen`. 2. Tap `Zaloguj się przez Authentik` — hosted Authentik login opens and returns through `recipe://callback`. 3. App shows `Witaj, {displayName}!`. 4. Restart after access-token expiry (or short token lifetime) — app returns to authenticated screen without credentials. 5. Tap `Wyloguj się` — app returns to LoginScreen; restart does not silently authenticate. 6. `GET /api/v1/me` returns 200 with valid token; 401 without token or with wrong-audience token. Reply with `approved` to mark Phase 2 complete, or describe the failing step (with tokens redacted) so the gate can be re-opened. ## Verification ### Automated — passing - `./gradlew :composeApp:jvmTest --tests "*LoginViewModelTest*"` — PASS (5 tests). - `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 :composeApp:compileTestKotlinWasmJs` — PASS. - Acceptance grep checks: `auth_sign_in_button` Polish copy present, `0xFF3B6939` in `RecipeTheme`, `auth_error_cancelled` referenced in `LoginViewModelTest`, no `Click me!` in `App.kt`, `collectAsState` + `SplashScreen` present in `App.kt`, `auth_welcome_format` in `PostLoginPlaceholderScreen`, no raw Polish strings in any `.kt` source under `dev/ulfrx/recipe/`. ### Automated — pre-existing failures (not introduced by 02-07; tracked in deferred-items.md) - `:composeApp:testDebugUnitTest` — 2 failures in `SecureAuthStateStoreContractTest`. - `:composeApp:spotlessKotlinCheck` — 1 ktlint violation in `SecureAuthStateStore.ios.kt`. ### Manual — pending - iOS Authentik UAT (Task 3 — see User Setup Required above). ## Next Phase Readiness Phase 3 (households) can now extend `AuthState.Authenticated.householdId` and replace `PostLoginPlaceholderScreen` with `HouseholdGate` without touching `AuthSession` or the auth-gate `when` (it already handles the `is Authenticated` branch). ## Self-Check: PASSED - All listed created/modified files exist on disk. - Commits `466e4c7`, `88f4898`, `570652c` exist in `git log`. - Acceptance grep checks all pass (run inline above). - `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 :composeApp:compileTestKotlinWasmJs` exits 0. - Pre-existing failures unrelated to 02-07 are documented in `deferred-items.md` (verified via `git stash` reproduction on `master` HEAD). --- *Phase: 02-authentication-foundation* *Status: Tasks 1+2 complete; Task 3 (manual iOS Authentik UAT) awaiting user verification.*