docs(02-07): record auth UI gate progress, awaiting iOS Authentik UAT
Tasks 1+2 (auto) complete: Compose Resources strings, RecipeTheme seed, SplashScreen/LoginScreen/PostLoginPlaceholderScreen, LoginViewModel + PostLoginViewModel registered in authModule, App.kt auth gate via collectAsStateWithLifecycle. Task 3 (checkpoint:human-verify) requires manual iOS Authentik UAT per docs/authentik-setup.md before Phase 2 can be marked complete.
This commit is contained in:
189
.planning/phases/02-authentication-foundation/02-07-SUMMARY.md
Normal file
189
.planning/phases/02-authentication-foundation/02-07-SUMMARY.md
Normal file
@@ -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<T>()."
|
||||||
|
- "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.*
|
||||||
Reference in New Issue
Block a user