diff --git a/.planning/phases/02-authentication-foundation/02-07-SUMMARY.md b/.planning/phases/02-authentication-foundation/02-07-SUMMARY.md new file mode 100644 index 0000000..561bbba --- /dev/null +++ b/.planning/phases/02-authentication-foundation/02-07-SUMMARY.md @@ -0,0 +1,189 @@ +--- +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.*