Files
recipe/.planning/phases/02-authentication-foundation/02-07-SUMMARY.md
2026-04-29 21:07:49 +02:00

12 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed duration completed
02-authentication-foundation 07 auth
kmp
compose-multiplatform
material3
koin-viewmodel
compose-resources
auth-gate
phase provides
02-authentication-foundation 02-06 AuthSession StateFlow, AuthState model, authModule Koin singletons
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`
phase-03-households
added patterns
kotlinx-coroutines-test (commonTest only) for the multiplatform `runTest` runtime
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).
created modified
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
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
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.
AUTH-01
AUTH-04
AUTH-05
10m 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 tests466e4c7 (test)
  2. Task 2 (GREEN): Auth screens, ViewModels, App auth gate88f4898 (feat)
  3. Task 2 follow-up: switch commonTest to runTest for wasmJs compatibility570652c (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.ktLoginScreenState + 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.ktonSignOutClick()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.ktviewModel { ... } registrations.
  • composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.ktrunBlockingrunTest.
  • composeApp/build.gradle.ktscommonTest 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.