diff --git a/.idea/misc.xml b/.idea/misc.xml
index ddba1308a6b3133e32d83fa7b8e5174bc5e67922..5f04613c84047e3fcff7491e61f09c4ebe5c1245 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="EntryPointsManager">
     <list size="1">
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8d39981a7c3b1ffd92f89342c925a310b2e41e0a..4543eaba43dad18c2e393a40ebfac72d719dc824 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -43,12 +43,24 @@ android {
             buildConfigField("String", "I11_KAFFEEKASSE_DEBUG_DEVICEID", "null")
             buildConfigField("String", "I11_KAFFEEKASSE_DEBUG_APIKEY", "null")
         }
-        debug {
+        create("debug-preview") {
             applicationIdSuffix = ".debug"
+            signingConfig = signingConfigs.getByName("debug")
             proguardFiles(
+                getDefaultProguardFile("proguard-android-optimize.txt"),
                 "proguard-rules.pro"
             )
+            buildConfigField("String", "I11_PORTAL_DEBUG_USERNAME", "null")
+            buildConfigField("String", "I11_PORTAL_DEBUG_PASSWORD", "null")
+            buildConfigField("String", "I11_KAFFEEKASSE_DEBUG_DEVICEID", "null")
+            buildConfigField("String", "I11_KAFFEEKASSE_DEBUG_APIKEY", "null")
+        }
+        debug {
+            applicationIdSuffix = ".debug"
             signingConfig = signingConfigs.getByName("debug")
+            proguardFiles(
+                "proguard-rules.pro"
+            )
             gradleLocalProperties(rootDir).let { properties ->
                 var username = properties["i11.portal.debug.username"]
                 var password = properties["i11.portal.debug.password"]
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/AppModule.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/AppModule.kt
index a185fa55f968d591439f8b96e32bd5fdead74d35..c3ad6c1810afd9b73e073c4c3789ab986478b8e9 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/AppModule.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/AppModule.kt
@@ -4,6 +4,7 @@ import androidx.compose.runtime.mutableStateMapOf
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Cart
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Item
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.MutableCart
+import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 
 
 sealed interface AppModule {
@@ -43,6 +44,10 @@ class KaffeekasseModule : AppModule {
         override operator fun contains(item: Item) = item in content
         override val itemCount get() = content.values.sum()
     }
+
+    companion object {
+        val RepositoryProvider.kaffeekasseCart get() = modules.require<KaffeekasseModule>().cart
+    }
 }
 
 class HiwiTrackerModule : AppModule {
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/MainActivity.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/MainActivity.kt
index 68481caa2d9167ded5321b0e3bbce47b2a24d270..a396c9fcad5337d9f34f152505f4358c8fc8b3b8 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/MainActivity.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/MainActivity.kt
@@ -1,14 +1,22 @@
 package net.novagamestudios.kaffeekasse
 
+import android.annotation.SuppressLint
+import android.os.Build
 import android.os.Bundle
+import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
 import net.novagamestudios.common_utils.LocalLogger
 import net.novagamestudios.common_utils.Logger
 import net.novagamestudios.common_utils.error
 import net.novagamestudios.kaffeekasse.ui.App
-import net.novagamestudios.kaffeekasse.ui.theme.KaffeekasseTheme
 
 class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -21,6 +29,37 @@ class MainActivity : ComponentActivity() {
                 App()
             }
         }
+        lifecycleScope.launch {
+            App.instance.settingsRepository.values
+                .map { it.fullscreen }
+                .distinctUntilChanged()
+                .collect { fullscreen ->
+                    if (fullscreen) {
+                        hideSystemUI()
+                    } else {
+                        showSystemUI()
+                    }
+                }
+        }
+
+        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+        windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+    }
+
+    @SuppressLint("WrongConstant")
+    private fun hideSystemUI() {
+        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            windowInsetsController.hide(WindowInsets.Type.systemBars())
+        }
+    }
+
+    @SuppressLint("WrongConstant")
+    private fun showSystemUI() {
+        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            windowInsetsController.show(WindowInsets.Type.systemBars())
+        }
     }
 
     private companion object : Logger {
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/api/kaffeekasse/model/BasicUserInfo.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/api/kaffeekasse/model/BasicUserInfo.kt
index ce8efcd77dacc21c1a7ed1f9bd4dba36f0a7ae79..e9f493409678e07f2658348672317aba57bebc75 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/api/kaffeekasse/model/BasicUserInfo.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/api/kaffeekasse/model/BasicUserInfo.kt
@@ -15,4 +15,6 @@ data class BasicUserInfo(
 ) : APIPurchaseAccount {
     val firstName by lazy { name.split(", ")[1] }
     val lastName by lazy { name.split(", ")[0] }
+
+    val mayHavePin get() = noPinSet == null || !noPinSet
 }
\ No newline at end of file
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/UserAuthCredentials.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/UserAuthCredentials.kt
new file mode 100644
index 0000000000000000000000000000000000000000..028c611667043607c0e4d76a1853d23451eb6195
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/UserAuthCredentials.kt
@@ -0,0 +1,11 @@
+package net.novagamestudios.kaffeekasse.model.kaffeekasse
+
+data class UserAuthCredentials(
+    val pin: String? = null,
+    val rwthId: String? = null,
+    val key: String? = null
+) {
+    companion object {
+        val Empty = UserAuthCredentials()
+    }
+}
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt
index 46512c356bc05fd626ee64ecb74b9426b437187c..a44768c517814df3e5711974541cd247981d7b42 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt
@@ -1,20 +1,50 @@
 package net.novagamestudios.kaffeekasse.model.session
 
 
-data class Session(
-    private val commonUser: User?,
-    val device: Device?
-) : java.io.Serializable {
-    val realUser get() = commonUser?.takeIf { it.isRealUser }
-
-    val isRealUserLoggedIn get() = realUser != null
-    val isKaffeekasseLoggedIn get() = commonUser?.takeIf { it.isKaffeekasse } != null
-    val isDeviceLoggedIn get() = device != null
+sealed interface Session : java.io.Serializable {
+    data object Empty : Session
+    interface WithDevice : Session {
+        val device: Device
+    }
+    interface WithRealUser : Session {
+        val realUser: User
+    }
+    interface WithDeviceAndUser : Session, WithDevice, WithRealUser
 
     companion object {
-        val Empty = Session(null, null)
-
-        private val User.isKaffeekasse get() = user == "kaffeekasse"
-        private val User.isRealUser get() = !isKaffeekasse
+        operator fun invoke(
+            device: Device?,
+            user: User?
+        ): Session {
+            val realUser = user?.takeIf { it.isRealUser }
+            return when {
+                device != null && realUser != null -> SessionWithDeviceAndRealUser(device, realUser)
+                device != null -> SessionWithDevice(device)
+                realUser != null -> SessionWithRealUser(realUser)
+                else -> Empty
+            }
+        }
     }
 }
+
+
+val Session.deviceOrNull: Device? get() = (this as? Session.WithDevice)?.device
+val Session.realUserOrNull: User? get() = (this as? Session.WithRealUser)?.realUser
+
+
+private val User.isKaffeekasse get() = user == "kaffeekasse"
+private val User.isRealUser get() = !isKaffeekasse
+
+
+private data class SessionWithDevice(
+    override val device: Device
+) : Session.WithDevice
+
+private data class SessionWithRealUser(
+    override val realUser: User
+) : Session.WithRealUser
+
+private data class SessionWithDeviceAndRealUser(
+    override val device: Device,
+    override val realUser: User
+) : Session.WithDeviceAndUser
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt
index c5b2411f8d2edb1b117ec83a4502c99ce0efd87d..6c50325bdf09cfef1d04ca1c3971ede55d22e101 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt
@@ -8,12 +8,15 @@ import net.novagamestudios.common_utils.Logger
 import net.novagamestudios.common_utils.debug
 import net.novagamestudios.common_utils.info
 import net.novagamestudios.common_utils.toastShort
+import net.novagamestudios.common_utils.verbose
 import net.novagamestudios.common_utils.warn
 import net.novagamestudios.kaffeekasse.api.kaffeekasse.KaffeekasseAPI
 import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.BasicUserInfo
 import net.novagamestudios.kaffeekasse.model.credentials.DeviceCredentials
 import net.novagamestudios.kaffeekasse.model.credentials.Login
 import net.novagamestudios.kaffeekasse.model.credentials.isValid
+import net.novagamestudios.kaffeekasse.model.kaffeekasse.UserAuthCredentials
+import net.novagamestudios.kaffeekasse.model.session.Session
 import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
 import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository
 import net.novagamestudios.kaffeekasse.util.mapState
@@ -31,11 +34,14 @@ class LoginRepository(
     val isPerformingAction = _isPerformingAction.asStateFlow()
 
     private suspend fun <R> action(block: suspend () -> R) = mutex.withReentrantLock {
+        val prev = _isPerformingAction.value
+        verbose { "Locking action: ${!prev}" }
         _isPerformingAction.value = true
         try {
             block()
         } finally {
-            _isPerformingAction.value = false
+            verbose { "Unlocking action: ${!prev}" }
+            _isPerformingAction.value = prev
         }
     }
 
@@ -66,8 +72,7 @@ class LoginRepository(
     }
 
     suspend fun tryAutoLogin(activityContext: Context): Login? = action {
-        if (portal.session.value.isRealUserLoggedIn) return@action null
-        if (portal.session.value.isDeviceLoggedIn) return@action null
+        if (portal.session.value !is Session.Empty) return@action null
         if (!autoLoginAttemptAvailable) return@action null
 
         debug { "Auto login enabled: ${autoLogin.value}" }
@@ -118,20 +123,37 @@ class LoginRepository(
 
 
 
-    suspend fun login(user: BasicUserInfo): String? = action {
+    suspend fun login(user: BasicUserInfo, auth: UserAuthCredentials = UserAuthCredentials.Empty): LoginResult = action {
         try {
-            when (portal.loginUser(user.id)) {
-                KaffeekasseAPI.UserLoginResult.Failure.PrivateDevice -> "Private device"
-                is KaffeekasseAPI.UserLoginResult.Failure.UnknownError -> "Unknown error"
-                KaffeekasseAPI.UserLoginResult.Failure.UserAuthenticationFailure -> "User authentication failure"
-                else -> null
+            when (portal.loginUser(
+                user.id,
+                pin = auth.pin,
+                rwthId = auth.rwthId,
+                key = auth.key
+            )) {
+                KaffeekasseAPI.UserLoginResult.Failure.PrivateDevice -> LoginResult.Failure("Private device")
+                is KaffeekasseAPI.UserLoginResult.Failure.UnknownError -> LoginResult.Failure("Unknown error")
+                KaffeekasseAPI.UserLoginResult.Failure.UserAuthenticationFailure -> LoginResult.Failure("User authentication failure")
+                is KaffeekasseAPI.UserLoginResult.LoggedIn -> LoginResult.Success(portal.session.value)
             }
         } catch (e: Exception) {
             warn(e) { "Failed to login user" }
-            e.message ?: e::class.simpleName ?: "Unknown error"
+            LoginResult.Failure(e.message ?: e::class.simpleName ?: "Unknown error")
         }
     }
 
+    sealed interface LoginResult {
+        data class Success(val session: Session) : LoginResult
+        data class Failure(val error: String) : LoginResult
+    }
+
+
+    suspend fun logoutUser() = action {
+        portal.logoutUser()
+    }
+
+
+
     private suspend fun performDeviceLogin(credentials: DeviceCredentials): Boolean = action {
         info { "Logging in device: ${credentials.deviceId}" }
         val success = kaffeekasse.loginDevice(credentials)
@@ -140,7 +162,7 @@ class LoginRepository(
     }
 
     suspend fun tryAutoLoginDevice(): Unit = action {
-        if (portal.session.value.isRealUserLoggedIn || portal.session.value.isDeviceLoggedIn) return@action
+        if (portal.session.value !is Session.Empty) return@action
         info { "Trying to auto login device" }
         val credentials = credentials.deviceCredentials()
             ?.takeIf { it.isValid }
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/RepositoryProvider.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/RepositoryProvider.kt
index f2a8fa43ce14eb22107bebf3af5962187f6645d2..7f35ced67789863be530d5c15d70fb20ba958f7d 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/RepositoryProvider.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/RepositoryProvider.kt
@@ -25,4 +25,4 @@ interface RepositoryProvider {
     // Active app modules
     val modules: AppModules
 
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt
index 821d6685d1e493cccce722fd3700740b0851b48c..ff57c0a6a01d8be268e7865114da220a25463295 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt
@@ -20,6 +20,7 @@ import java.io.File
 @Serializable
 data class Settings(
     val themeMode: ThemeMode = ThemeMode.Dark,
+    val fullscreen: Boolean = false,
     val autoLogin: Boolean = false,
     val deviceCredentials: DeviceCredentials? = null,
     val developerMode: Boolean = false,
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/SettingsRepository.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/SettingsRepository.kt
index 888f93621d2c5f877c66862c8a6f6d1a9973565d..b586c6cad1827c6407dda022f7d7d502c4ec02b9 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/SettingsRepository.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/SettingsRepository.kt
@@ -2,6 +2,7 @@ package net.novagamestudios.kaffeekasse.repositories
 
 import kotlinx.coroutines.flow.StateFlow
 import net.novagamestudios.common_utils.compose.state.MutableDataStoreState
+import net.novagamestudios.kaffeekasse.model.session.realUserOrNull
 import net.novagamestudios.kaffeekasse.util.mapState
 
 class SettingsRepository(
@@ -10,7 +11,7 @@ class SettingsRepository(
 ) : MutableSettingsStore by settingsStore {
     val userSettings: StateFlow<MutableDataStoreState<UserSettings>?> by lazy {
         repositoryProvider.portalRepository.session.mapState { session ->
-            val key = session.realUser?.user ?: return@mapState null
+            val key = session.realUserOrNull?.user ?: return@mapState null
             UserSettingsStore(key, settingsStore)
         }
     }
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/HiwiTrackerRepository.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/HiwiTrackerRepository.kt
index 21f151c64aee0fce26238c5657b57da29d68dd9d..0d8d366e60aa433874a042305903f3e79073ed76 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/HiwiTrackerRepository.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/HiwiTrackerRepository.kt
@@ -32,7 +32,7 @@ class HiwiTrackerRepository(
         }
     }
 
-    override suspend fun markUserDataDirty() {
+    override fun markUserDataDirty() {
         dataByMonth.clear()
     }
 
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/KaffeekasseRepository.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/KaffeekasseRepository.kt
index c7bf0c196f3d8491e4d6252868d0f94fddecff8a..0cd93d672e1d32d4733cad0a534e863a14642728 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/KaffeekasseRepository.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/KaffeekasseRepository.kt
@@ -4,16 +4,15 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import net.novagamestudios.common_utils.Logger
-import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.APIPurchaseAccount
 import net.novagamestudios.kaffeekasse.api.kaffeekasse.KaffeekasseAPI
 import net.novagamestudios.kaffeekasse.api.kaffeekasse.KaffeekasseScraper
+import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.APIPurchaseAccount
 import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.ExtendedUserInfo
 import net.novagamestudios.kaffeekasse.data.category
 import net.novagamestudios.kaffeekasse.data.cleanName
 import net.novagamestudios.kaffeekasse.data.cleanProductName
 import net.novagamestudios.kaffeekasse.data.cleanVariantName
 import net.novagamestudios.kaffeekasse.data.drawableResource
-import net.novagamestudios.kaffeekasse.model.session.Device
 import net.novagamestudios.kaffeekasse.model.credentials.DeviceCredentials
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Account
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Cart
@@ -26,6 +25,10 @@ import net.novagamestudios.kaffeekasse.model.kaffeekasse.ManualBillDetails
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Stock
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Transaction
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.isEmpty
+import net.novagamestudios.kaffeekasse.model.session.Device
+import net.novagamestudios.kaffeekasse.model.session.Session
+import net.novagamestudios.kaffeekasse.model.session.User
+import net.novagamestudios.kaffeekasse.util.richdata.KeyedMultiDataSource
 import net.novagamestudios.kaffeekasse.util.richdata.RichData
 import net.novagamestudios.kaffeekasse.util.richdata.RichDataSource
 import net.novagamestudios.kaffeekasse.util.richdata.RichDataSource.Companion.RichDataSource
@@ -56,15 +59,14 @@ class KaffeekasseRepository(
     }
 
     suspend fun logoutDevice() {
-        if (session.value.device == null) return
+        if (session.value !is Session.WithDevice) return
         mutableCurrentDevice.value = null
         client.logoutAll()
     }
 
 
-
-    val account: RichDataSource<Account> = RichDataSource {
-        requireLoggedInUser()
+    val account: KeyedMultiDataSource<User, Account> = KeyedMultiDataSource { user ->
+        requireLoggedIn(user)
         api.loggedInUser().mapToRichDataState {
             Account(
                 firstName = balance.myBalance.firstName,
@@ -76,17 +78,31 @@ class KaffeekasseRepository(
         }
     }
 
+    val transactions: KeyedMultiDataSource<User, List<Transaction>> = KeyedMultiDataSource { user ->
+        requireLoggedIn(user)
+        scraper.transactions().let {
+            RichData.Data(it)
+        }
+    }
+
+    val manualBillAccounts: KeyedMultiDataSource<User, List<ManualBillDetails.PurchaseAccount>> = KeyedMultiDataSource { user ->
+        requireLoggedIn(user)
+        scraper.manualBillDetails().accounts.let {
+            RichData.Data(it)
+        }
+    }
+
+
+
     val stock: RichDataSource<Stock> = RichDataSource {
-        requireLoggedInUser()
+        requireAnyLoggedInUser()
         progress(0f / 3f)
 
         val baseItemGroups = scraper.manualBillDetails().itemGroups
         progress(1f / 3f)
 
-        if (!session.value.isDeviceLoggedIn) return@RichDataSource RichData.Data(
-            StockImpl(
-            itemGroups = baseItemGroups
-        )
+        if (session.value !is Session.WithDevice) return@RichDataSource RichData.Data(
+            StockImpl(itemGroups = baseItemGroups)
         )
 
         val itemGroupsById: Map<Int?, MutableItemGroup> = baseItemGroups.map {
@@ -141,13 +157,6 @@ class KaffeekasseRepository(
         }
     }
 
-    val transactions: RichDataSource<List<Transaction>> = RichDataSource {
-        requireLoggedInUser()
-        scraper.transactions().let {
-            RichData.Data(it)
-        }
-    }
-
 
 
     val basicUserInfoList = RichDataSource {
@@ -156,15 +165,6 @@ class KaffeekasseRepository(
     }
 
     private val extendedUserInfoById = mutableMapOf<Int, RichDataSource<ExtendedUserInfo>>()
-
-    val manualBillAccounts = RichDataSource {
-        requireLoggedInUser()
-        scraper.manualBillDetails().accounts.let {
-            RichData.Data(it)
-        }
-    }
-
-
     fun getExtendedUserInfo(userId: Int): RichDataSource<ExtendedUserInfo> = extendedUserInfoById.getOrPut(userId) {
         RichDataSource {
             requireLoggedInDevice()
@@ -173,16 +173,16 @@ class KaffeekasseRepository(
     }
 
 
-    suspend fun purchase(cart: Cart, account: ManualBillDetails.PurchaseAccount) {
+    suspend fun purchase(asUser: User, cart: Cart, account: ManualBillDetails.PurchaseAccount) {
         if (cart.isEmpty()) return
-        requireLoggedInUser()
+        requireLoggedIn(asUser)
         scraper.submitCart(account, cart)
         markBalanceDataDirty()
     }
 
-    suspend fun purchase(cart: Cart, targetAccount: APIPurchaseAccount? = null) {
+    suspend fun purchase(asUser: User, cart: Cart, targetAccount: APIPurchaseAccount? = null) {
         if (cart.isEmpty()) return
-        requireLoggedInUser()
+        requireLoggedIn(asUser)
         requireLoggedInDevice()
         cart.forEach { (item, count) ->
             val result = api.purchase(
@@ -196,15 +196,14 @@ class KaffeekasseRepository(
     }
 
 
-    private suspend fun markBalanceDataDirty() {
+    private fun markBalanceDataDirty() {
         account.markDirty()
         transactions.markDirty()
     }
 
-    override suspend fun markUserDataDirty() {
-        account.markDirty()
-        stock.markDirty()
-        transactions.markDirty()
+    override fun markUserDataDirty() {
+        account.clear()
+        transactions.clear()
         extendedUserInfoById.values.forEach { it.markDirty() }
     }
 
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt
index 3eec06ea5ea0cb773e64b80d1e32d9fde34a56a7..9358c16b0432d731f37443e06481f7817ee07ec2 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt
@@ -11,6 +11,7 @@ import net.novagamestudios.kaffeekasse.api.kaffeekasse.KaffeekasseAPI
 import net.novagamestudios.kaffeekasse.model.credentials.Login
 import net.novagamestudios.kaffeekasse.model.session.Session
 import net.novagamestudios.kaffeekasse.model.session.User
+import net.novagamestudios.kaffeekasse.model.session.realUserOrNull
 
 
 class PortalRepository(
@@ -30,13 +31,13 @@ class PortalRepository(
         kaffeekasse.currentDevice
     ) { session, device ->
         Session(
-            commonUser = if (session.isLoggedIn) User(session.username, session.displayName) else null,
-            device = device
+            device = device,
+            user = if (session.isLoggedIn) User(session.username, session.displayName) else null
         )
     }.stateIn(coroutineScope, SharingStarted.Eagerly, Session.Empty)
 
     suspend fun loginUser(login: Login) {
-        if (session.value.isRealUserLoggedIn) throw IllegalStateException("User already logged in")
+        if (session.value is Session.WithRealUser) throw IllegalStateException("User already logged in")
         client.login(login)
         markUserDataDirty()
     }
@@ -47,8 +48,8 @@ class PortalRepository(
         rwthId: String? = null,
         key: String? = null
     ): KaffeekasseAPI.UserLoginResult {
-        if (session.value.isRealUserLoggedIn) throw IllegalStateException("User already logged in")
-        if (!session.value.isDeviceLoggedIn) throw IllegalStateException("No permissions to login user")
+        if (session.value is Session.WithRealUser) throw IllegalStateException("User already logged in")
+        if (session.value !is Session.WithDevice) throw IllegalStateException("No permissions to login user")
         val result = kaffeekasse.api.performUserLogin(userId, pin, rwthId, key)
         info { "User login result: $result" }
         markUserDataDirty()
@@ -56,13 +57,13 @@ class PortalRepository(
     }
 
     suspend fun logoutUser() {
-        requireLoggedInUser()
-        if (session.value.isDeviceLoggedIn) kaffeekasse.api.logoutUser()
+        requireAnyLoggedInUser()
+        if (session.value is Session.WithDevice) kaffeekasse.api.logoutUser()
         else client.logoutAll()
         markUserDataDirty()
     }
 
-    override suspend fun markUserDataDirty() {
+    override fun markUserDataDirty() {
         modules.forEach { if (it != this) it.markUserDataDirty() }
     }
 }
@@ -70,8 +71,9 @@ class PortalRepository(
 abstract class PortalRepositoryModule internal constructor() {
     internal lateinit var portal: PortalRepository
     protected open val session get() = portal.session
-    protected fun requireLoggedInUser() = require(session.value.isRealUserLoggedIn) { "Logged in user required" }
-    protected fun requireLoggedInDevice() = require(session.value.isDeviceLoggedIn) { "Logged in device required" }
-    abstract suspend fun markUserDataDirty()
+    protected fun requireAnyLoggedInUser() = require(session.value is Session.WithRealUser) { "Logged in user required" }
+    protected fun requireLoggedIn(user: User) = require(session.value.realUserOrNull == user) { "User \"${user.user}\" not logged in" }
+    protected fun requireLoggedInDevice() = require(session.value is Session.WithDevice) { "Logged in device required" }
+    abstract fun markUserDataDirty()
 }
 
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/releases/GitLabReleases.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/releases/GitLabReleases.kt
index 970b65fda7a276ab743f56c0cfe3741f1e46e5f5..73b30eac6dc5eba8e7e09ea3a1eb6575b1fb0bfc 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/releases/GitLabReleases.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/releases/GitLabReleases.kt
@@ -35,90 +35,3 @@ class GitLabReleases(
 }
 
 
-/*
-
-    {
-        "name": "1.0.0",
-        "tag_name": "v1.0.0",
-        "description": null,
-        "created_at": "2024-03-03T22:38:29.368+01:00",
-        "released_at": "2024-03-03T22:38:29.368+01:00",
-        "upcoming_release": false,
-        "author": {
-            "id": 13529,
-            "username": "jonas.broeckmann",
-            "name": "Jonas Broeckmann",
-            "state": "active",
-            "locked": false,
-            "avatar_url": "https://git.rwth-aachen.de/uploads/-/system/user/avatar/13529/avatar.png",
-            "web_url": "https://git.rwth-aachen.de/jonas.broeckmann"
-        },
-        "commit": {
-            "id": "0f3ed979a3aa506e5fbbe37ac2b25929dcea5718",
-            "short_id": "0f3ed979",
-            "created_at": "2024-03-03T22:32:51.000+01:00",
-            "parent_ids": [
-                "4a566100b8648a1a452966e9bdb97c6ae37b47f1"
-            ],
-            "title": "CI use package registry",
-            "message": "CI use package registry\n",
-            "author_name": "JojoIV",
-            "author_email": "jonas.broeckmann@gmx.de",
-            "authored_date": "2024-03-03T22:32:51.000+01:00",
-            "committer_name": "JojoIV",
-            "committer_email": "jonas.broeckmann@gmx.de",
-            "committed_date": "2024-03-03T22:32:51.000+01:00",
-            "trailers": {},
-            "extended_trailers": {},
-            "web_url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/commit/0f3ed979a3aa506e5fbbe37ac2b25929dcea5718"
-        },
-        "commit_path": "/jonas.broeckmann/kaffeekasse/-/commit/0f3ed979a3aa506e5fbbe37ac2b25929dcea5718",
-        "tag_path": "/jonas.broeckmann/kaffeekasse/-/tags/v1.0.0",
-        "assets": {
-            "count": 5,
-            "sources": [
-                {
-                    "format": "zip",
-                    "url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/archive/v1.0.0/kaffeekasse-v1.0.0.zip"
-                },
-                {
-                    "format": "tar.gz",
-                    "url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/archive/v1.0.0/kaffeekasse-v1.0.0.tar.gz"
-                },
-                {
-                    "format": "tar.bz2",
-                    "url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/archive/v1.0.0/kaffeekasse-v1.0.0.tar.bz2"
-                },
-                {
-                    "format": "tar",
-                    "url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/archive/v1.0.0/kaffeekasse-v1.0.0.tar"
-                }
-            ],
-            "links": [
-                {
-                    "id": 802,
-                    "name": "kaffeekasse-1.0.0.apk",
-                    "url": "https://git.rwth-aachen.de/api/v4/projects/95637/packages/generic/kaffeekasse/1.0.0/kaffeekasse-1.0.0.apk",
-                    "direct_asset_url": "https://git.rwth-aachen.de/api/v4/projects/95637/packages/generic/kaffeekasse/1.0.0/kaffeekasse-1.0.0.apk",
-                    "link_type": "other"
-                }
-            ]
-        },
-        "evidences": [
-            {
-                "sha": "e93469e2ef6de10312003fd88eca1f021342d1c20eed",
-                "filepath": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/releases/v1.0.0/evidences/5410.json",
-                "collected_at": "2024-03-03T22:38:29.729+01:00"
-            }
-        ],
-        "_links": {
-            "closed_issues_url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/issues?release_tag=v1.0.0&scope=all&state=closed",
-            "closed_merge_requests_url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/merge_requests?release_tag=v1.0.0&scope=all&state=closed",
-            "merged_merge_requests_url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/merge_requests?release_tag=v1.0.0&scope=all&state=merged",
-            "opened_issues_url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/issues?release_tag=v1.0.0&scope=all&state=opened",
-            "opened_merge_requests_url": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/merge_requests?release_tag=v1.0.0&scope=all&state=opened",
-            "self": "https://git.rwth-aachen.de/jonas.broeckmann/kaffeekasse/-/releases/v1.0.0"
-        }
-    }
- */
-
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/App.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/App.kt
index f33c6cbf9a773fd3f69653e8890886392120478c..13747a9ada3cfa14953e70e9b0dd70ce361691b8 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/App.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/App.kt
@@ -2,21 +2,33 @@ package net.novagamestudios.kaffeekasse.ui
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CloseFullscreen
+import androidx.compose.material.icons.filled.OpenInFull
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import cafe.adriel.voyager.core.model.ScreenModel
+import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.navigator.Navigator
 import net.novagamestudios.common_utils.Logger
-import net.novagamestudios.common_utils.ProvideLogger
+import net.novagamestudios.common_utils.LoggerForFun
+import net.novagamestudios.common_utils.compose.components.BoxCenter
+import net.novagamestudios.common_utils.debug
 import net.novagamestudios.common_utils.verbose
 import net.novagamestudios.kaffeekasse.App
+import net.novagamestudios.kaffeekasse.model.session.Session
 import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository
 import net.novagamestudios.kaffeekasse.ui.navigation.AppModulesScreen
 import net.novagamestudios.kaffeekasse.ui.navigation.AppScreenTransition
 import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
 import net.novagamestudios.kaffeekasse.ui.theme.KaffeekasseTheme
+import net.novagamestudios.kaffeekasse.ui.util.requireWithKey
 import net.novagamestudios.kaffeekasse.util.collectAsStateHere
 
 
@@ -43,29 +55,78 @@ fun App(
     verbose { "recompose app" }
     Navigator(
         LoginNavigation,
-        key = "app-navigator"
+        key = NavigatorKey
     ) { navigator ->
-        AppScreenTransition(
-            navigator = navigator,
-            Modifier
-                .background(MaterialTheme.colorScheme.background)
-                .fillMaxSize(),
-        )
-        autoNavigateLogin(navigator, vm)
+        if (navigator.lastItem matches vm.session) {
+            AppScreenTransition(
+                navigator = navigator,
+                Modifier
+                    .background(MaterialTheme.colorScheme.background)
+                    .fillMaxSize(),
+            )
+        } else {
+            autoNavigateLogin(navigator, vm)
+            Surface {
+                BoxCenter(Modifier.fillMaxSize()) {
+                    CircularProgressIndicator()
+                }
+            }
+        }
     }
     UpdateDialogs()
 }
 
+private infix fun Screen.matches(session: Session) = when (this) {
+    is LoginNavigation -> session !is Session.WithRealUser
+    is AppModulesScreen -> session is Session.WithRealUser
+    else -> false
+}
+
+private const val NavigatorKey = "app-navigator"
 
 @Composable
 private fun autoNavigateLogin(
     navigator: Navigator,
     vm: AppViewModel
 ) {
-    if (vm.session.isRealUserLoggedIn) {
-        navigator.replaceAll(AppModulesScreen(vm.session))
-    } else {
-        navigator.replaceAll(LoginNavigation)
+    navigator.handleSession(vm.session)
+}
+
+
+fun Navigator.handleSession(session: Session) {
+    val logger = LoggerForFun()
+    val appNavigator = requireWithKey(NavigatorKey)
+    when (appNavigator.lastItem) {
+        is LoginNavigation -> {
+            if (session is Session.WithRealUser) {
+                logger.debug { "Auto-navigate to AppModulesScreen because $session" }
+                appNavigator.replaceAll(AppModulesScreen(session))
+            }
+        }
+        is AppModulesScreen -> {
+            if (session !is Session.WithRealUser) {
+                logger.debug { "Auto-navigate to LoginNavigation because $session" }
+                appNavigator.replaceAll(LoginNavigation)
+            }
+        }
+    }
+}
+
+
+@Composable
+fun FullscreenIconButton(
+    modifier: Modifier = Modifier
+) {
+    val settings = App.settings()
+    IconButton(
+        onClick = { settings.tryUpdate { it.copy(fullscreen = !it.fullscreen) } },
+        modifier
+    ) {
+        if (!settings.value.fullscreen) {
+            Icon(Icons.Default.OpenInFull, "Vollbild")
+        } else {
+            Icon(Icons.Default.CloseFullscreen, "Vollbild verlassen")
+        }
     }
 }
 
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/AppModules.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/AppModules.kt
index fc87a227be28d48bb8e12722893d3ee1c991e243..cf7f33295ff285cebfe7ea18b99f195ad96e81b1 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/AppModules.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/AppModules.kt
@@ -32,8 +32,8 @@ import net.novagamestudios.kaffeekasse.AppModule
 import net.novagamestudios.kaffeekasse.AppModules
 import net.novagamestudios.kaffeekasse.HiwiTrackerModule
 import net.novagamestudios.kaffeekasse.KaffeekasseModule
-import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.model.session.Session
+import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.repositories.SettingsRepository
 import net.novagamestudios.kaffeekasse.ui.navigation.AppModulesScreen
 import net.novagamestudios.kaffeekasse.ui.navigation.HiwiTrackerNavigation
@@ -43,7 +43,7 @@ import net.novagamestudios.kaffeekasse.util.collectAsStateHere
 
 
 class AppModulesViewModel(
-    val session: Session,
+    val session: Session.WithRealUser,
     private val settingsRepository: SettingsRepository,
     val allModules: AppModules
 ) : ScreenModel {
@@ -52,7 +52,7 @@ class AppModulesViewModel(
     private val userSettings by settingsRepository.userSettings.collectAsStateHere()
     val modules: AppModules get() = when {
         settingsRepository.value.developerMode -> allModules
-        session.isDeviceLoggedIn -> AppModules(allModules.filterIsInstance<KaffeekasseModule>())
+        session is Session.WithDevice -> AppModules(allModules.filterIsInstance<KaffeekasseModule>())
         else -> allModules
     }
     private val initialModule get() = modules
@@ -60,8 +60,7 @@ class AppModulesViewModel(
         ?: modules.first()
 
 
-    val AppModule.moduleTab: ModuleTab
-        get() = when (this) {
+    val AppModule.moduleTab: ModuleTab get() = when (this) {
         is KaffeekasseModule -> KaffeekasseNavigation.Tab(session)
         is HiwiTrackerModule -> HiwiTrackerNavigation.Tab(session)
     }
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt
index 3d307bf4f066ef8d20c1d3467ed2e9f520f8468f..7b399e7012d8db08fc747b9fa8d9ed5b104e43d3 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt
@@ -44,21 +44,21 @@ import net.novagamestudios.kaffeekasse.ui.util.PullToRefreshBox
 import net.novagamestudios.kaffeekasse.util.richdata.collectAsRichStateHere
 
 class AccountViewModel private constructor(
-    val session: Session,
+    val session: Session.WithRealUser,
     private val kaffeekasse: KaffeekasseRepository
 ) : ScreenModel, Logger {
-    private val accountState = kaffeekasse.account.collectAsRichStateHere()
+    private val accountState = kaffeekasse.account[session.realUser].collectAsRichStateHere()
     val account get() = accountState.dataOrNull
     val errors get() = accountState.errorOrNull?.messages
     val isLoading get() = accountState.isLoading
 
     fun refresh() {
         screenModelScope.launch {
-            kaffeekasse.account.refresh()
+            accountState.refresh()
         }
     }
     suspend fun refreshIfNeeded() {
-        kaffeekasse.account.ensureCleanData()
+        accountState.ensureCleanData()
     }
 
     companion object {
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/KaffeekasseModule.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/KaffeekasseModule.kt
index 1d7873c9b0d71fd0b7d4cafd69e9d0a70e7c55a4..9d500a064c914ad2291e56dcb149bcf97f5916cb 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/KaffeekasseModule.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/KaffeekasseModule.kt
@@ -11,26 +11,25 @@ import androidx.compose.runtime.Composable
 import cafe.adriel.voyager.core.model.ScreenModel
 import cafe.adriel.voyager.navigator.Navigator
 import net.novagamestudios.common_utils.Logger
-import net.novagamestudios.kaffeekasse.KaffeekasseModule
+import net.novagamestudios.kaffeekasse.KaffeekasseModule.Companion.kaffeekasseCart
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.MutableCart
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.isNotEmpty
 import net.novagamestudios.kaffeekasse.model.session.Session
 import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.ui.AppModuleSelection
+import net.novagamestudios.kaffeekasse.ui.kaffeekasse.ManualBillViewModel.Companion.backNavigationHandler
 import net.novagamestudios.kaffeekasse.ui.navigation.AppSubpageTitle
-import net.novagamestudios.kaffeekasse.ui.util.BackNavigationHandler
 import net.novagamestudios.kaffeekasse.ui.navigation.KaffeekasseNavigation
-import net.novagamestudios.kaffeekasse.ui.kaffeekasse.ManualBillViewModel.Companion.backNavigationHandler
+import net.novagamestudios.kaffeekasse.ui.util.BackNavigationHandler
 
 
 class KaffeekasseModuleViewModel private constructor(
-    session: Session,
+    session: Session.WithRealUser,
     val cart: MutableCart
 ) : ScreenModel, Logger {
-    val accountName = if (session.isDeviceLoggedIn) {
-        session.realUser?.displayName ?: "Unknown User"
-    } else {
-        null
+    val accountName = when (session) {
+        is Session.WithDevice -> session.realUser.displayName ?: "Unknown User"
+        else -> null
     }
 
 
@@ -38,7 +37,7 @@ class KaffeekasseModuleViewModel private constructor(
         context (RepositoryProvider)
         fun create(screen: KaffeekasseNavigation.Tab) = KaffeekasseModuleViewModel(
             session = screen.session,
-            cart = modules.require<KaffeekasseModule>().cart
+            cart = kaffeekasseCart
         )
     }
 }
@@ -84,7 +83,7 @@ fun KaffeekasseTopBarActions(
     navigator: Navigator
 ) {
     when (val screen = navigator.lastItem) {
-        is KaffeekasseNavigation.ManualBillScreen -> ManualBillTopBarActions(screen.vm(), screen)
+        is KaffeekasseNavigation.ManualBillScreen -> ManualBillTopBarActions(screen.vm())
         is KaffeekasseNavigation.AccountScreen -> AccountTopBarActions(screen.vm())
         is KaffeekasseNavigation.TransactionsScreen -> TransactionsTopBarActions(screen.vm())
     }
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt
index 00dd3bda61b4e2bc26c500c0019c3b1b4cd0e4e6..ee56746a0761034db282431cbe321f2b38685d53 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt
@@ -36,7 +36,6 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.unit.dp
 import cafe.adriel.voyager.core.model.ScreenModel
 import cafe.adriel.voyager.core.model.screenModelScope
-import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
@@ -44,29 +43,30 @@ import kotlinx.coroutines.launch
 import net.novagamestudios.common_utils.Logger
 import net.novagamestudios.common_utils.compose.tabIndicatorOffset
 import net.novagamestudios.common_utils.verbose
-import net.novagamestudios.kaffeekasse.KaffeekasseModule
-import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
+import net.novagamestudios.kaffeekasse.KaffeekasseModule.Companion.kaffeekasseCart
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.MutableCart
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.isNotEmpty
 import net.novagamestudios.kaffeekasse.model.session.Session
+import net.novagamestudios.kaffeekasse.model.session.deviceOrNull
+import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
-import net.novagamestudios.kaffeekasse.ui.util.BackNavigationHandler
-import net.novagamestudios.kaffeekasse.ui.navigation.KaffeekasseNavigation
-import net.novagamestudios.kaffeekasse.ui.login.LogoutTopBarAction
-import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.APICheckoutViewModel
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CategorizedItems
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CategorizedItemsViewModel
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.Checkout
+import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CheckoutViewModel
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CustomItems
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CustomItemsViewModel
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.FailureRetryScreen
-import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.ScraperCheckoutViewModel
+import net.novagamestudios.kaffeekasse.ui.login.LogoutTopBarAction
+import net.novagamestudios.kaffeekasse.ui.navigation.KaffeekasseNavigation
+import net.novagamestudios.kaffeekasse.ui.util.BackNavigationHandler
 import net.novagamestudios.kaffeekasse.util.richdata.collectAsRichStateHere
+import net.novagamestudios.kaffeekasse.util.richdata.isLoadingOrNone
 
 
 class ManualBillViewModel private constructor(
     repositoryProvider: RepositoryProvider,
-    val session: Session,
+    val session: Session.WithRealUser,
     private val kaffeekasse: KaffeekasseRepository,
     val cart: MutableCart
 ) : ScreenModel, Logger {
@@ -90,25 +90,20 @@ class ManualBillViewModel private constructor(
     }
 
     val customItemsViewModel by lazy {
-        with(repositoryProvider) { CustomItemsViewModel.create(screenModelScope) }
+        with(repositoryProvider) {
+            CustomItemsViewModel.create(
+                user = session.realUser,
+                computationScope = screenModelScope
+            )
+        }
     }
 
     val checkoutViewModel by derivedStateOf {
-        val onSubmitted = { categorizedItemsByGroup.forEach { it.reset() } }
-        if (session.isDeviceLoggedIn) {
-            APICheckoutViewModel(
-                session,
-                screenModelScope,
-                kaffeekasse,
-                cart,
-                onSubmitted
-            )
-        } else {
-            ScraperCheckoutViewModel(
-                screenModelScope,
-                kaffeekasse,
-                cart,
-                onSubmitted
+        with(repositoryProvider) {
+            CheckoutViewModel.create(
+                session = session,
+                coroutineScope = screenModelScope,
+                onSubmitted = { categorizedItemsByGroup.forEach { it.reset() } }
             )
         }
     }
@@ -134,8 +129,9 @@ class ManualBillViewModel private constructor(
         kaffeekasse.stock.ensureCleanData()
     }
 
+    @Suppress("unused")
     suspend fun autoScrollToDeviceItemGroup() {
-        val initialItemGroupIndex = session.device?.itemTypeId
+        val initialItemGroupIndex = session.deviceOrNull?.itemTypeId
             ?.let { id -> itemGroups.indexOfFirst { it.id == id }.takeIf { it >= 0 } }
         if (initialItemGroupIndex != null) {
             currentGroupIndex = initialItemGroupIndex
@@ -149,7 +145,7 @@ class ManualBillViewModel private constructor(
             repositoryProvider = this@RepositoryProvider,
             session = screen.session,
             kaffeekasse = kaffeekasseRepository,
-            cart = modules.require<KaffeekasseModule>().cart
+            cart = kaffeekasseCart
         )
 
         @Composable
@@ -288,8 +284,7 @@ private fun TabbedItemGroups(
 
 @Composable
 fun ManualBillTopBarActions(
-    vm: ManualBillViewModel,
-    screen: Screen
+    vm: ManualBillViewModel
 ) {
     val navigator = LocalNavigator.currentOrThrow
     IconButton(onClick = {
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Transactions.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Transactions.kt
index e644627fe9ddbe55dd99abd25467ab131b4eccc3..ec098acc48aac651e54f94a88c6658052191bff0 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Transactions.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Transactions.kt
@@ -59,11 +59,11 @@ import java.time.LocalDate
 import java.time.format.DateTimeFormatter
 
 class TransactionsViewModel private constructor(
-    @Suppress("UNUSED_PARAMETER") session: Session,
+    private val session: Session.WithRealUser,
     private val kaffeekasse: KaffeekasseRepository
 ) : ScreenModel, Logger {
 
-    private val transactionsState = kaffeekasse.transactions.collectAsRichStateHere()
+    private val transactionsState = kaffeekasse.transactions[session.realUser].collectAsRichStateHere()
     val transactions get() = transactionsState.dataOrNull
     val errors get() = transactionsState.errorOrNull?.messages
     val isLoading get() = transactionsState.isLoading
@@ -81,11 +81,11 @@ class TransactionsViewModel private constructor(
 
     fun fetchTransactions() {
         screenModelScope.launch {
-            kaffeekasse.transactions.refresh()
+            transactionsState.refresh()
         }
     }
     suspend fun refreshIfNeeded() {
-        kaffeekasse.transactions.ensureCleanData()
+        transactionsState.ensureCleanData()
     }
     fun openInBrowser(context: Context) {
         context.openInBrowser(kaffeekasse.scraper.transactionsUrl)
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/Checkout.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/Checkout.kt
index f7f61dd898add272c64f0859acb9371c0604ea53..b9af47722dc9b40a7aa31f99954f9a86b1d1700b 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/Checkout.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/Checkout.kt
@@ -54,6 +54,7 @@ import net.novagamestudios.common_utils.compose.maskedCircleIcon
 import net.novagamestudios.common_utils.compose.state.ReentrantActionState
 import net.novagamestudios.common_utils.toastLong
 import net.novagamestudios.common_utils.warn
+import net.novagamestudios.kaffeekasse.KaffeekasseModule.Companion.kaffeekasseCart
 import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.APIPurchaseAccount
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Cart
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.ManualBillDetails
@@ -62,11 +63,12 @@ import net.novagamestudios.kaffeekasse.model.kaffeekasse.PurchaseAccount
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.isEmpty
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.isNotEmpty
 import net.novagamestudios.kaffeekasse.model.session.Session
+import net.novagamestudios.kaffeekasse.model.session.User
+import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.cards.CategoryIcon
 import net.novagamestudios.kaffeekasse.util.richdata.RichDataFlow
 import net.novagamestudios.kaffeekasse.util.richdata.RichDataState
-import net.novagamestudios.kaffeekasse.util.richdata.asRichDataFlow
 import net.novagamestudios.kaffeekasse.util.richdata.collectAsRichStateIn
 import net.novagamestudios.kaffeekasse.util.richdata.combineRich
 import net.novagamestudios.kaffeekasse.util.richdata.dataOrNull
@@ -127,9 +129,35 @@ abstract class CheckoutViewModel(
             }
         }
     }
+
+    companion object {
+        context (RepositoryProvider)
+        fun create(
+            session: Session.WithRealUser,
+            coroutineScope: CoroutineScope,
+            onSubmitted: suspend () -> Unit
+        ): CheckoutViewModel = if (session is Session.WithDevice) {
+            APICheckoutViewModel(
+                session.realUser,
+                coroutineScope,
+                kaffeekasseRepository,
+                kaffeekasseCart,
+                onSubmitted
+            )
+        } else {
+            ScraperCheckoutViewModel(
+                session.realUser,
+                coroutineScope,
+                kaffeekasseRepository,
+                kaffeekasseCart,
+                onSubmitted
+            )
+        }
+    }
 }
 
-class ScraperCheckoutViewModel(
+private class ScraperCheckoutViewModel(
+    private val user: User,
     coroutineScope: CoroutineScope,
     kaffeekasse: KaffeekasseRepository,
     cart: MutableCart,
@@ -141,22 +169,26 @@ class ScraperCheckoutViewModel(
     onSubmitted
 ) {
 
-    private val purchaseAccounts: RichDataFlow<List<PurchaseAccount>> = kaffeekasse.manualBillAccounts
+    private val purchaseAccounts = kaffeekasse.manualBillAccounts[user]
 
     override val accountSelectionState = purchaseAccounts.mapRich { accounts ->
         AccountSelectionState(accounts)
     }.collectAsRichStateIn(coroutineScope)
 
     override suspend fun refreshPurchaseAccountsIfNeeded() {
-        kaffeekasse.manualBillAccounts.ensureCleanData()
+        purchaseAccounts.ensureCleanData()
     }
     override suspend fun performSubmitCart(purchaseAccount: PurchaseAccount) {
-        kaffeekasse.purchase(cart, purchaseAccount as ManualBillDetails.PurchaseAccount)
+        kaffeekasse.purchase(
+            asUser = user,
+            cart = cart,
+            account = purchaseAccount as ManualBillDetails.PurchaseAccount
+        )
     }
 }
 
-class APICheckoutViewModel(
-    session: Session,
+private class APICheckoutViewModel(
+    private val user: User,
     coroutineScope: CoroutineScope,
     kaffeekasse: KaffeekasseRepository,
     cart: MutableCart,
@@ -164,7 +196,7 @@ class APICheckoutViewModel(
 ) : CheckoutViewModel(coroutineScope, kaffeekasse, cart, onSubmitted) {
 
     private val selfUserBasic = kaffeekasse.basicUserInfoList.mapRich { userList ->
-        val selfDisplayName = session.realUser?.displayName ?: return@mapRich null
+        val selfDisplayName = user.displayName ?: return@mapRich null
         userList.firstOrNull { "${it.firstName} ${it.lastName}" == selfDisplayName }
     }.stateIn(coroutineScope)
 
@@ -198,7 +230,13 @@ class APICheckoutViewModel(
         }
     }
     override suspend fun performSubmitCart(purchaseAccount: PurchaseAccount) {
-        kaffeekasse.purchase(cart, purchaseAccount.takeUnless { it.id == selfUserBasic.value.dataOrNull?.id } as? APIPurchaseAccount)
+        kaffeekasse.purchase(
+            asUser = user,
+            cart = cart,
+            targetAccount = purchaseAccount.takeUnless {
+                it.id == selfUserBasic.value.dataOrNull?.id
+            } as? APIPurchaseAccount
+        )
     }
 }
 
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/CustomItems.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/CustomItems.kt
index bbe38dd8b249adb616e50647a003ce33145c02fb..2ffadf561e2583c31fee8a30077a479eb9213836 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/CustomItems.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/CustomItems.kt
@@ -36,10 +36,11 @@ import net.novagamestudios.common_utils.compose.DashedShape
 import net.novagamestudios.common_utils.compose.components.ColumnCenter
 import net.novagamestudios.common_utils.compose.state.ReentrantActionState
 import net.novagamestudios.common_utils.verbose
-import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Item
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.MutableCart
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Transaction
+import net.novagamestudios.kaffeekasse.model.session.User
+import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.repositories.SettingsRepository
 import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
 import net.novagamestudios.kaffeekasse.ui.theme.disabled
@@ -52,6 +53,7 @@ import net.novagamestudios.kaffeekasse.util.richdata.stateIn
 import java.time.LocalDateTime
 
 class CustomItemsViewModel private constructor(
+    private val user: User,
     private val computationScope: CoroutineScope,
     settingsRepository: SettingsRepository,
     private val kaffeekasse: KaffeekasseRepository
@@ -82,13 +84,19 @@ class CustomItemsViewModel private constructor(
         displayed.take(SuggestedItemsCount)
     }
 
+    private val userTransactions = kaffeekasse.transactions[user]
+    private val userTransactionsState = userTransactions.collectAsRichStateIn(computationScope)
+
     private val suggestionsMutex = ReentrantActionState()
-    val isLoadingSuggestions by suggestionsMutex
+    val isLoadingSuggestions by derivedStateOf {
+        suggestionsMutex.value || userTransactionsState.isLoading
+    }
+
 
     init {
         verbose { "CustomItemsViewModel initialized" }
         computationScope.launch {
-            kaffeekasse.transactions
+            userTransactions
                 .map { it.dataOrNull }
                 .filterNotNull()
                 .collectLatest { transactions ->
@@ -108,7 +116,7 @@ class CustomItemsViewModel private constructor(
 
     fun fetch() {
         computationScope.launch {
-            kaffeekasse.transactions.ensureCleanData()
+            userTransactions.ensureCleanData()
         }
     }
 
@@ -170,7 +178,11 @@ class CustomItemsViewModel private constructor(
 
 
         context (RepositoryProvider)
-        fun create(computationScope: CoroutineScope) = CustomItemsViewModel(
+        fun create(
+            user: User,
+            computationScope: CoroutineScope
+        ) = CustomItemsViewModel(
+            user = user,
             computationScope = computationScope,
             settingsRepository = settingsRepository,
             kaffeekasse = kaffeekasseRepository
@@ -213,10 +225,20 @@ fun CustomItems(
         )
         if (vm.isLoadingSuggestions) {
             CircularProgressIndicator(Modifier.padding(vertical = 64.dp))
-        } else ItemCardGrid(
-            items = vm.displayedSuggestedItems,
-            cart = cart,
-            spacerForFAB = true
-        )
+        } else if (vm.displayedSuggestedItems.isEmpty()) {
+            Text(
+                "Keine Vorschläge",
+                Modifier.padding(horizontal = 16.dp, vertical = 64.dp),
+                color = LocalContentColor.current.disabled(),
+                textAlign = TextAlign.Center,
+                style = MaterialTheme.typography.labelLarge
+            )
+        } else {
+            ItemCardGrid(
+                items = vm.displayedSuggestedItems,
+                cart = cart,
+                spacerForFAB = true
+            )
+        }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/cards/Item.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/cards/Item.kt
index 28dbaeecbca62c4e42d5ad9bb48547145443b750..4b93751b3e85a5f751efea3f738524d7c8779778 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/cards/Item.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/cards/Item.kt
@@ -33,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.ProvideTextStyle
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
@@ -61,6 +62,7 @@ import net.novagamestudios.kaffeekasse.app
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Item
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.KnownItem
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Transaction
+import net.novagamestudios.kaffeekasse.model.session.Session
 import net.novagamestudios.kaffeekasse.util.richdata.collectAsRichState
 
 @Composable
@@ -143,20 +145,30 @@ private fun ItemInformation(
         )
     }
     Spacer(Modifier.weight(1f))
-    val transactionsState = app().kaffeekasseRepository.transactions.collectAsRichState()
-    val lastUnitPrice by remember { derivedStateOf { transactionsState.dataOrNull?.findLastUnitPrice(item) } }
-    (item.price ?: lastUnitPrice ?: item.estimatedPrice)?.let {
-        val highlighted = it != item.estimatedPrice && App.developerMode
-        Text(
-            remember(it) {
-                listOfNotNull(
-                    if (item.price != null) null else "≈",
-                    String.format("%.2f€", it)
-                ).joinToString(" ")
-            },
-            color = if (highlighted) Color.Yellow else  Color.Unspecified,
-            style = MaterialTheme.typography.bodyMedium
-        )
+
+    // Very bad code
+    val app = app()
+    val session = app.portalRepository.session.collectAsState()
+    if (session is Session.WithRealUser) {
+        val transactionsState = app.kaffeekasseRepository.transactions[session.realUser].collectAsRichState()
+        val lastUnitPrice by remember {
+            derivedStateOf {
+                transactionsState.dataOrNull?.findLastUnitPrice(item)
+            }
+        }
+        (item.price ?: lastUnitPrice ?: item.estimatedPrice)?.let {
+            val highlighted = it != item.estimatedPrice && App.developerMode
+            Text(
+                remember(it) {
+                    listOfNotNull(
+                        if (item.price != null) null else "≈",
+                        String.format("%.2f€", it)
+                    ).joinToString(" ")
+                },
+                color = if (highlighted) Color.Yellow else Color.Unspecified,
+                style = MaterialTheme.typography.bodyMedium
+            )
+        }
     }
 }
 
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt
index 3a6376b5f39f69b13fe3c9ca447d78dc68aaf38e..e152c1539f903da64151ffbdaee01814f0de6120 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt
@@ -1,5 +1,6 @@
 package net.novagamestudios.kaffeekasse.ui.login
 
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.IntrinsicSize
@@ -7,6 +8,7 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
@@ -28,6 +30,7 @@ import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -36,6 +39,7 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.dp
@@ -45,16 +49,19 @@ import cafe.adriel.voyager.navigator.Navigator
 import kotlinx.coroutines.launch
 import net.novagamestudios.common_utils.Logger
 import net.novagamestudios.common_utils.compose.components.CircularLoadingBox
+import net.novagamestudios.kaffeekasse.R
 import net.novagamestudios.kaffeekasse.app
 import net.novagamestudios.kaffeekasse.data.cleanName
 import net.novagamestudios.kaffeekasse.model.credentials.DeviceCredentials
 import net.novagamestudios.kaffeekasse.model.credentials.isValid
 import net.novagamestudios.kaffeekasse.model.session.Device
 import net.novagamestudios.kaffeekasse.model.session.Session
+import net.novagamestudios.kaffeekasse.model.session.deviceOrNull
 import net.novagamestudios.kaffeekasse.repositories.LoginRepository
 import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository
 import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction
+import net.novagamestudios.kaffeekasse.ui.FullscreenIconButton
 import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
 import net.novagamestudios.kaffeekasse.ui.util.rememberSerializableState
 import net.novagamestudios.kaffeekasse.util.collectAsStateHere
@@ -85,7 +92,6 @@ class LoginViewModel private constructor(
         }
     }
 
-
     companion object {
         context (RepositoryProvider)
         fun create(@Suppress("UNUSED_PARAMETER") screen: LoginNavigation.Companion) = LoginViewModel(
@@ -102,13 +108,13 @@ fun LoginAdditional(
     vm: LoginViewModel,
     navigator: Navigator
 ) {
-    val device = vm.session.device
+    val session = vm.session
     when (navigator.lastItem) {
         is LoginNavigation.FormScreen -> {
-            if (device != null) navigator.replace(LoginNavigation.UserSelectionScreen(device))
+            if (session is Session.WithDevice) navigator.replace(LoginNavigation.UserSelectionScreen(session.device))
         }
         is LoginNavigation.UserSelectionScreen -> {
-            if (device == null) navigator.replace(LoginNavigation.FormScreen)
+            if (session !is Session.WithDevice) navigator.replace(LoginNavigation.FormScreen)
         }
     }
     if (vm.showLoginDeviceDialog) LoginDeviceDialog(vm)
@@ -200,8 +206,17 @@ private fun LoginDeviceForm(
 fun LoginTopBarTitle(
     vm: LoginViewModel
 ) {
-    vm.session.device?.let {
-        DeviceInfo(it, Modifier.padding(16.dp))
+    vm.session.deviceOrNull?.let {
+        Row(
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Image(
+                painterResource(R.drawable.logo_edited),
+                "Kaffeekasse",
+                Modifier.size(32.dp)
+            )
+            DeviceInfo(it, Modifier.padding(16.dp))
+        }
     }
 }
 
@@ -210,31 +225,39 @@ context (RowScope)
 fun LoginTopBarActions(
     vm: LoginViewModel
 ) {
-    if (!vm.session.isRealUserLoggedIn) {
-        if (vm.session.isDeviceLoggedIn) {
-            var confirmLogout by remember { mutableStateOf(false) }
-            IconButton(
-                onClick = { confirmLogout = true },
-                enabled = !vm.isLoading
-            ) {
-                Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen")
+    val session = vm.session
+    if (session !is Session.WithRealUser) {
+        when (session) {
+            is Session.WithDevice -> {
+                var confirmLogout by remember { mutableStateOf(false) }
+                IconButton(
+                    onClick = { confirmLogout = true },
+                    enabled = !vm.isLoading
+                ) {
+                    Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen")
+                }
+                if (confirmLogout) ConfirmLogoutDeviceDialog(
+                    onDismiss = { confirmLogout = false },
+                    onConfirm = { vm.logoutDevice() }
+                )
             }
-            if (confirmLogout) ConfirmLogoutDeviceDialog(
-                onDismiss = { confirmLogout = false },
-                onConfirm = { vm.logoutDevice() }
-            )
-        } else {
-            IconButton(
-                onClick = { vm.showLoginDeviceDialog = true },
-                enabled = !vm.isLoading
-            ) {
-                Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen")
+            else -> {
+                IconButton(
+                    onClick = { vm.showLoginDeviceDialog = true },
+                    enabled = !vm.isLoading
+                ) {
+                    Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen")
+                }
             }
         }
     }
     AppInfoTopBarAction()
+    FullscreenIconButton()
 }
 
+
+
+
 @Composable
 private fun DeviceInfo(
     device: Device,
@@ -286,11 +309,19 @@ private fun ConfirmLogoutDeviceDialog(
 @Composable
 fun LogoutTopBarAction(session: Session) {
     val app = app()
-    IconButton(
-        onClick = { app.launch { app.portalRepository.logoutUser() } },
-        enabled = session.isRealUserLoggedIn || session.isDeviceLoggedIn
-    ) {
-        Icon(Icons.AutoMirrored.Filled.Logout, "Ausloggen")
+    val loginRepository = app.loginRepository
+    val isLoading by loginRepository.isPerformingAction.collectAsState()
+    CircularLoadingBox(loading = isLoading) {
+        IconButton(
+            onClick = { app.launch { loginRepository.logoutUser() } },
+            enabled = session !is Session.Empty
+        ) {
+            Icon(
+                Icons.AutoMirrored.Filled.Logout,
+                contentDescription = "Ausloggen",
+                tint = MaterialTheme.colorScheme.primary
+            )
+        }
     }
 }
 
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt
index 8a312d98eefd50a573da479fcde4b21e7d47559e..dec7ea0e338d8930f59c5a4f595b1cf010bf59cc 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt
@@ -122,7 +122,6 @@ fun LoginForm(
         verticalArrangement = Arrangement.spacedBy(8.dp),
         horizontalAlignment = Alignment.CenterHorizontally
     ) {
-        val activityContext = LocalContext.current
 
         Icon(
             Icons.Rounded.Coffee,
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt
index a4f3081482bd3de25a0c905281acdca578f87030..e1117c1b3e2bbe1ba573ce71ff468cd90a8d46aa 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt
@@ -1,29 +1,50 @@
 package net.novagamestudios.kaffeekasse.ui.login
 
+import android.content.res.Configuration
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridState
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.Password
 import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.DividerDefaults
 import androidx.compose.material3.DockedSearchBar
+import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.ProvideTextStyle
 import androidx.compose.material3.SearchBarDefaults
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
 import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -31,6 +52,7 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
@@ -39,68 +61,109 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
 import cafe.adriel.voyager.core.model.ScreenModel
 import cafe.adriel.voyager.core.model.screenModelScope
+import cafe.adriel.voyager.navigator.Navigator
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
-import net.novagamestudios.common_utils.compose.components.BoxCenter
+import net.novagamestudios.common_utils.Logger
 import net.novagamestudios.common_utils.compose.components.RowCenter
+import net.novagamestudios.common_utils.compose.state.rememberDerivedStateOf
 import net.novagamestudios.common_utils.toastShort
 import net.novagamestudios.common_utils.warn
 import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.BasicUserInfo
+import net.novagamestudios.kaffeekasse.model.kaffeekasse.UserAuthCredentials
 import net.novagamestudios.kaffeekasse.repositories.LoginRepository
 import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
 import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
-import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
+import net.novagamestudios.kaffeekasse.ui.handleSession
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.FailureRetryScreen
-import net.novagamestudios.kaffeekasse.ui.util.PullToRefreshBox
+import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
+import net.novagamestudios.kaffeekasse.ui.theme.disabled
 import net.novagamestudios.kaffeekasse.ui.util.RichDataContent
 import net.novagamestudios.kaffeekasse.ui.util.rememberPullToRefreshState
 import net.novagamestudios.kaffeekasse.util.richdata.asRichDataFlow
 import net.novagamestudios.kaffeekasse.util.richdata.combineRich
 import net.novagamestudios.kaffeekasse.util.richdata.dataOrNull
-import net.novagamestudios.kaffeekasse.util.richdata.derivedRichDataSource
 import net.novagamestudios.kaffeekasse.util.richdata.stateIn
+import net.novagamestudios.kaffeekasse.util.richdata.withFunctions
 
 
 class UserSelectionViewModel private constructor(
     private val loginRepository: LoginRepository,
     kaffeekasse: KaffeekasseRepository
-) : ScreenModel {
+) : ScreenModel, Logger {
 
     val users = kaffeekasse.basicUserInfoList
 
     var searchQuery by mutableStateOf("")
     
-    val filteredUsers = derivedRichDataSource(
-        users,
-        combineRich(
+    val filteredUsers = run {
+        val data = combineRich(
             snapshotFlow { searchQuery.lowercase() }.asRichDataFlow(),
             users,
             loadDuringTransform = true
         ) { query, users ->
-            val sorted = users.asSequence().sortedBy { it.lastName }
+            val sorted = users.asSequence().sortedBy { it.lastName.uppercase() }
             if (query.isBlank()) return@combineRich sorted.toList()
             sorted.filter { query in "${it.firstName} ${it.lastName}".lowercase() }.toList()
         }
             .flowOn(Dispatchers.IO)
             .stateIn(screenModelScope)
-    )
+        data withFunctions users
+    }
 
     var error by mutableStateOf<String?>(null)
 
-    fun selectUser(user: BasicUserInfo) {
-        // TODO handle pin
+    var userAuthDialog by mutableStateOf<UserAuthDialogState?>(null)
+
+    data class UserAuthDialogState(
+        val user: BasicUserInfo,
+        val pin: String = ""
+        // TODO
+    ) {
+        val auth get() = UserAuthCredentials(pin = pin.takeIf { it.isNotBlank() })
+    }
+
+    fun loginUser(
+        user: BasicUserInfo,
+        auth: UserAuthCredentials = UserAuthCredentials.Empty,
+        navigator: Navigator? = null
+    ) {
+        if (user.mayHavePin && auth.pin == null) {
+            userAuthDialog = UserAuthDialogState(user)
+            return
+        }
         screenModelScope.launch {
-            error = loginRepository.login(user)
+            when (val result = loginRepository.login(user, auth)) {
+                is LoginRepository.LoginResult.Success -> {
+                    userAuthDialog = null
+                    navigator?.handleSession(result.session)
+                }
+                is LoginRepository.LoginResult.Failure -> {
+                    error = result.error
+                }
+            }
         }
     }
 
+    fun loginUser(dialog: UserAuthDialogState) {
+        loginUser(dialog.user, dialog.auth)
+    }
 
     companion object {
         context (RepositoryProvider)
@@ -138,6 +201,37 @@ fun UserSelection(
             .nestedScroll(pullToRefreshState.nestedScrollConnection),
         contentAlignment = Alignment.TopCenter
     ) {
+        val focusRequester = remember { FocusRequester() }
+        // TODO
+        DockedSearchBar(
+            query = vm.searchQuery,
+            onQueryChange = { vm.searchQuery = it },
+            onSearch = {
+                vm.filteredUsers.value
+                    .dataOrNull
+                    ?.singleOrNull()
+                    ?.let { vm.loginUser(it) }
+            },
+            active = false,
+            onActiveChange = { },
+            Modifier
+                .padding(8.dp)
+                .align(Alignment.TopCenter)
+                .focusRequester(focusRequester),
+            placeholder = { Text("Search users") },
+            leadingIcon = { Icon(Icons.Default.Search, "Search") },
+            trailingIcon = {
+                if (vm.searchQuery.isNotEmpty()) IconButton(
+                    onClick = { vm.searchQuery = "" }
+                ) {
+                    Icon(Icons.Default.Close, "Clear search")
+                }
+            },
+            content = { }
+        )
+
+        val searchBarOffset = SearchBarDefaults.InputFieldHeight
+
         RichDataContent(
             data = vm.filteredUsers,
             errorContent = {
@@ -150,68 +244,264 @@ fun UserSelection(
                     modifier = Modifier.fillMaxSize()
                 )
             },
-            loadingContent = { LinearProgressIndicator(
-                Modifier
-                    .padding(top = 8.dp)
-                    .align(Alignment.TopCenter)
-                    .fillMaxWidth()
-            ) },
-            dataContent = { users ->
-                val focusRequester = remember { FocusRequester() }
-                // TODO
-                DockedSearchBar(
-                    query = vm.searchQuery,
-                    onQueryChange = { vm.searchQuery = it },
-                    onSearch = {
-                        vm.filteredUsers.value
-                            .dataOrNull
-                            ?.singleOrNull()
-                            ?.let { vm.selectUser(it) }
-                    },
-                    active = false,
-                    onActiveChange = { },
+            loadingContent = {
+                LinearProgressIndicator(
                     Modifier
-                        .padding(8.dp)
-                        .align(Alignment.TopCenter)
-                        .focusRequester(focusRequester),
-                    placeholder = { Text("Search users") },
-                    leadingIcon = { Icon(Icons.Default.Search, "Search") },
-                    trailingIcon = {
-                        if (vm.searchQuery.isNotEmpty()) IconButton(
-                            onClick = { vm.searchQuery = "" }
-                        ) {
-                            Icon(Icons.Default.Close, "Clear search")
+                        .padding(top = 8.dp)
+                        .align(Alignment.BottomCenter)
+                        .fillMaxWidth()
+                )
+            },
+            dataContent = { users ->
+
+                val gridState = rememberLazyGridState()
+                val coroutineScope = rememberCoroutineScope()
+
+                val currentStartChars by rememberDerivedStateOf {
+                    val start = gridState.layoutInfo.viewportStartOffset + gridState.layoutInfo.beforeContentPadding
+                    val end = gridState.layoutInfo.viewportEndOffset - gridState.layoutInfo.afterContentPadding
+                    gridState.layoutInfo.visibleItemsInfo
+                        .asSequence()
+                        .filter {
+                            val mid = it.offset.y + (it.size.height / 2)
+                            mid in start..<end
                         }
+                        .mapNotNull { it.contentType as? Int }
+                        .mapNotNullTo(mutableSetOf()) { users.getOrNull(it)?.charOrNull }
+                }
+                val userIndexAndCharIndexByChar: Map<Char, Pair<Int, Int>> by rememberDerivedStateOf {
+                    users.asSequence()
+                        .withIndex()
+                        .mapNotNull { (userIndex, user) ->
+                            user.charOrNull?.let { it to userIndex }
+                        }
+                        .distinctBy { (char, _) -> char }
+                        .withIndex()
+                        .associate { (charIndex, pair) ->
+                            val (char, userIndex) = pair
+                            char to Pair(userIndex, charIndex)
+                        }
+                }
+                val chars = remember { ('A'..'Z').toList() }
+
+                val onSelectIndex: (Int) -> Unit = block@{ index ->
+                    val (userIndex, charIndex) = (userIndexAndCharIndexByChar[chars[index]] ?: return@block)
+                    val sectionIndex = userIndex + charIndex
+                    coroutineScope.launch {
+                        gridState.scrollToItem(sectionIndex)
                     }
-                ) { }
-
-                LazyVerticalGrid(
-                    columns = GridCells.Adaptive(minSize = 300.dp),
-                    Modifier.fillMaxSize(),
-                    contentPadding = PaddingValues(
-                        start = 8.dp,
-                        top = 12.dp + SearchBarDefaults.InputFieldHeight,
-                        end = 8.dp,
-                        bottom = 12.dp
-                    ),
-                    horizontalArrangement = Arrangement.Center
-                ) {
-                    items(users) { user ->
-                        UserItem(
-                            user = user,
-                            onClick = { vm.selectUser(user) },
-                            Modifier
-                                .padding(6.dp)
-                                .widthIn(max = 300.dp)
+                }
+
+                val itemContent: @Composable (Int) -> Unit = { i ->
+                    val char = chars[i]
+                    AlphabetSelectionChar(
+                        char,
+                        highlighted = char in currentStartChars,
+                        enabled = char in userIndexAndCharIndexByChar
+                    )
+                }
+
+                @Composable
+                fun Grid(modifier: Modifier = Modifier) = UserGrid(
+                    gridState = gridState,
+                    users = users,
+                    onClick = { vm.loginUser(it) },
+                    topOffset = searchBarOffset,
+                    modifier
+                )
+
+
+                when {
+                    users.isEmpty() -> Text(
+                        "Niemand hat diesen Namen 🤔",
+                        Modifier.align(Alignment.Center)
+                    )
+                    LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE -> Column {
+                        Grid(Modifier.weight(1f))
+                        HorizontalDivider()
+                        HorizontalSelectionBar(
+                            itemCount = chars.size,
+                            onSelect = onSelectIndex,
+                            itemContent = itemContent
                         )
                     }
+                    else -> Row {
+                        VerticalSelectionBar(
+                            itemCount = chars.size,
+                            onSelect = onSelectIndex,
+                            Modifier.padding(top = searchBarOffset),
+                            itemContent = itemContent
+                        )
+                        Grid(Modifier.weight(1f))
+                    }
                 }
             }
         )
         PullToRefreshContainer(pullToRefreshState, Modifier.zIndex(2f))
     }
+    vm.userAuthDialog?.let { state ->
+        UserAuthDialog(
+            state,
+            onChange = { vm.userAuthDialog = it },
+            onDismiss = { vm.userAuthDialog = null },
+            onSubmit = { vm.loginUser(state) }
+        )
+    }
+}
+
+
+@Composable
+fun VerticalSelectionBar(
+    itemCount: Int,
+    onSelect: (Int) -> Unit,
+    modifier: Modifier = Modifier,
+    itemContent: @Composable (Int) -> Unit
+) {
+    fun selectAtRatio(ratio: Float) = onSelect((ratio * itemCount).toInt())
+    Column(
+        modifier
+            .fillMaxHeight()
+            .pointerInput(Unit) {
+                detectTapGestures { offset ->
+                    selectAtRatio(offset.y / size.height)
+                }
+            }
+            .pointerInput(Unit) {
+                detectVerticalDragGestures { change, _ ->
+                    change.consume()
+                    selectAtRatio(change.position.y / size.height)
+                }
+            }
+            .padding(4.dp),
+        verticalArrangement = Arrangement.SpaceAround,
+        horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+        for (item in 0 until itemCount) key(item) {
+            itemContent(item)
+        }
+    }
+}
+
+
+@Composable
+fun HorizontalSelectionBar(
+    itemCount: Int,
+    onSelect: (Int) -> Unit,
+    modifier: Modifier = Modifier,
+    itemContent: @Composable (Int) -> Unit
+) {
+    fun selectAtRatio(ratio: Float) = onSelect((ratio * itemCount).toInt())
+    Row(
+        modifier
+            .fillMaxWidth()
+            .pointerInput(Unit) {
+                detectTapGestures { offset ->
+                    selectAtRatio(offset.x / size.width)
+                }
+            }
+            .pointerInput(Unit) {
+                detectVerticalDragGestures { change, _ ->
+                    selectAtRatio(change.position.x / size.width)
+                }
+            }
+            .padding(4.dp),
+        horizontalArrangement = Arrangement.SpaceAround,
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        for (item in 0 until itemCount) key(item) {
+            itemContent(item)
+        }
+    }
+}
+
+
+@Composable
+fun AlphabetSelectionChar(
+    char: Char,
+    modifier: Modifier = Modifier,
+    highlighted: Boolean = false,
+    enabled: Boolean = true,
+) {
+    val scale by animateFloatAsState(
+        if (highlighted) 1f else 0.7f,
+        label = "CharScaleAnimation"
+    )
+    Text(
+        "$char",
+        modifier.graphicsLayer {
+            scaleX = scale
+            scaleY = scale
+        },
+        color = when {
+            enabled && highlighted -> MaterialTheme.colorScheme.primary
+            enabled -> LocalContentColor.current
+            else -> LocalContentColor.current.disabled()
+        },
+        fontSize = 24.sp,
+        fontWeight = if (highlighted) FontWeight.Bold else FontWeight.Normal
+    )
+}
+
+@Composable
+private fun UserGrid(
+    gridState: LazyGridState,
+    users: List<BasicUserInfo>,
+    onClick: (BasicUserInfo) -> Unit,
+    topOffset: Dp,
+    modifier: Modifier = Modifier
+) = LazyVerticalGrid(
+    columns = GridCells.Adaptive(minSize = 250.dp),
+    modifier,
+    state = gridState,
+    contentPadding = PaddingValues(
+        start = 8.dp,
+        top = 12.dp + topOffset,
+        end = 8.dp,
+        bottom = 12.dp
+    ),
+    horizontalArrangement = Arrangement.Center
+) {
+    var prevChar: Char? = null
+    users.forEachIndexed { i, user ->
+        val char = user.charOrNull
+        if (prevChar != char) {
+            item(
+                key = char,
+                span = { GridItemSpan(maxLineSpan) },
+                contentType = char
+            ) {
+                Row(
+                    Modifier.padding(6.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    Text(
+                        "$char",
+                        Modifier.padding(horizontal = 6.dp),
+                        color = DividerDefaults.color,
+                        fontSize = 24.sp,
+                        fontWeight = FontWeight.Bold
+                    )
+                    HorizontalDivider(thickness = 1.dp)
+                }
+            }
+        }
+        prevChar = char
+        item(
+            key = i,
+            contentType = i
+        ) {
+            UserItem(
+                user = user,
+                onClick = { onClick(user) },
+                Modifier
+                    .padding(6.dp)
+                    .widthIn(max = 300.dp)
+            )
+        }
+    }
 }
 
+
 @Composable
 private fun UserItem(
     user: BasicUserInfo,
@@ -222,8 +512,16 @@ private fun UserItem(
         onClick = onClick,
         modifier
     ) {
-        RowCenter(Modifier.padding(14.dp)) {
-            Text("${user.lastName}, ${user.firstName}")
+        RowCenter(
+            Modifier
+                .height(56.dp)
+                .padding(horizontal = 16.dp)
+        ) {
+            ProvideTextStyle(MaterialTheme.typography.titleMedium) {
+                Text("${user.lastName},")
+                Spacer(Modifier.width(8.dp))
+                Text(user.firstName)
+            }
             Spacer(Modifier.weight(1f))
             if (user.noPinSet == false) {
                 Icon(Icons.Default.Lock, "No PIN set")
@@ -233,3 +531,51 @@ private fun UserItem(
 }
 
 
+@Composable
+private fun UserAuthDialog(
+    state: UserSelectionViewModel.UserAuthDialogState,
+    onChange: (UserSelectionViewModel.UserAuthDialogState) -> Unit,
+    onDismiss: () -> Unit,
+    onSubmit: () -> Unit,
+    modifier: Modifier = Modifier
+) = AlertDialog(
+    onDismissRequest = { onDismiss() },
+    confirmButton = {
+        Button(
+            onClick = onSubmit,
+            enabled = state.pin.isNotBlank()
+        ) { Text("Einloggen") }
+    },
+    modifier,
+    dismissButton = {
+        TextButton(onClick = onDismiss) { Text("Abbrechen") }
+    },
+    icon = { Icon(Icons.Default.Password, "PIN") },
+    title = { Text("${state.user.firstName} ${state.user.lastName}") },
+    text = {
+        val focusRequester = remember { FocusRequester() }
+        OutlinedTextField(
+            state.pin,
+            onValueChange = { onChange(state.copy(pin = it)) },
+            Modifier.focusRequester(focusRequester),
+            label = { Text("PIN") },
+            leadingIcon = { Icon(Icons.Default.Lock, "PIN") },
+            visualTransformation = PasswordVisualTransformation(),
+            keyboardOptions = KeyboardOptions(
+                keyboardType = KeyboardType.Number,
+                imeAction = ImeAction.Done
+            ),
+            keyboardActions = KeyboardActions(
+                onDone = { onSubmit() }
+            ),
+            singleLine = true
+        )
+        LaunchedEffect(Unit) {
+            focusRequester.requestFocus()
+        }
+    }
+)
+
+
+private val BasicUserInfo.charOrNull get() = lastName.firstOrNull()?.uppercaseChar()
+
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt
index 0b0fb7fd61c503da82674767bdda6cb0c19621b2..fbd280e34a772b38740561ba3b9dd527cc4fe19d 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt
@@ -1,7 +1,5 @@
 package net.novagamestudios.kaffeekasse.ui.navigation
 
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedContent
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.CircularProgressIndicator
@@ -18,13 +16,14 @@ import net.novagamestudios.common_utils.Logger
 import net.novagamestudios.common_utils.compose.components.BoxCenter
 import net.novagamestudios.kaffeekasse.model.session.Device
 import net.novagamestudios.kaffeekasse.model.session.Session
+import net.novagamestudios.kaffeekasse.model.session.deviceOrNull
 import net.novagamestudios.kaffeekasse.rememberScreenModelWithApp
 import net.novagamestudios.kaffeekasse.ui.AppModulesViewModel
 import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.HiwiTrackerModuleViewModel
-import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.Overview
-import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.OverviewViewModel
 import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.HiwiTrackerTopBarActions
 import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.HiwiTrackerTopBarTitle
+import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.Overview
+import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.OverviewViewModel
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.Account
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.AccountViewModel
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.DynamicManualBill
@@ -47,14 +46,12 @@ import net.novagamestudios.kaffeekasse.ui.login.UserSelectionViewModel
 import net.novagamestudios.kaffeekasse.util.nearestScreen
 
 
-
-
 sealed interface LoginNavigation {
-    companion object : Screen {
+    companion object : LoginNavigation, Screen {
         override val key = LoginNavigation::class.simpleName!!
         @Composable fun vm() = rememberScreenModelWithApp { LoginViewModel.create(this@Companion) }
         private val initialScreen: Screen
-            @Composable get() = vm().session.device
+            @Composable get() = vm().session.deviceOrNull
                 ?.let { UserSelectionScreen(it) }
                 ?: FormScreen
         @Composable override fun Content() = AppScaffoldNavigator(
@@ -97,12 +94,12 @@ sealed interface LoginNavigation {
 
 
 sealed interface ModuleTab : Tab {
-    val session: Session
+    val session: Session.WithRealUser
 }
 
 
 data class AppModulesScreen(
-    val session: Session
+    val session: Session.WithRealUser
 ) : Screen {
     override val key = AppModulesScreen::class.simpleName!!
     @Composable fun vm() = rememberScreenModelWithApp { AppModulesViewModel.create(this@AppModulesScreen) }
@@ -137,7 +134,7 @@ data class AppModulesScreen(
 
 sealed interface KaffeekasseNavigation {
     data class Tab(
-        override val session: Session
+        override val session: Session.WithRealUser
     ) : ModuleTab, KaffeekasseNavigation {
         override val key = KaffeekasseNavigation::class.simpleName!!
         @Composable fun vm() = rememberScreenModelWithApp { KaffeekasseModuleViewModel.create(this@Tab) }
@@ -152,21 +149,21 @@ sealed interface KaffeekasseNavigation {
         override val options @Composable get() = remember { TabOptions(1u, "Kaffeekasse") }
     }
     data class ManualBillScreen(
-        val session: Session
+        val session: Session.WithRealUser
     ) : KaffeekasseNavigation, Screen, Logger {
         override val key = ManualBillScreen::class.simpleName!!
         @Composable fun vm() = rememberScreenModelWithApp { ManualBillViewModel.create(this@ManualBillScreen) }
         @Composable override fun Content() = DynamicManualBill(vm())
     }
     data class AccountScreen(
-        val session: Session
+        val session: Session.WithRealUser
     ) : KaffeekasseNavigation, Screen {
         override val key = AccountScreen::class.simpleName!!
         @Composable fun vm() = rememberScreenModelWithApp { AccountViewModel.create(this@AccountScreen) }
         @Composable override fun Content() = Account(vm())
     }
     data class TransactionsScreen(
-        val session: Session
+        val session: Session.WithRealUser
     ) : KaffeekasseNavigation, Screen {
         override val key = TransactionsScreen::class.simpleName!!
         @Composable fun vm() = rememberScreenModelWithApp { TransactionsViewModel.create(this@TransactionsScreen) }
@@ -177,7 +174,7 @@ sealed interface KaffeekasseNavigation {
 
 sealed interface HiwiTrackerNavigation {
     data class Tab(
-        override val session: Session
+        override val session: Session.WithRealUser
     ) : ModuleTab, HiwiTrackerNavigation {
         override val key = HiwiTrackerNavigation::class.simpleName!!
         @Composable fun vm() = rememberScreenModelWithApp { HiwiTrackerModuleViewModel.create(this@Tab) }
@@ -190,7 +187,7 @@ sealed interface HiwiTrackerNavigation {
         override val options @Composable get() = remember { TabOptions(2u, "Hiwi Tracker") }
     }
     data class OverviewScreen(
-        val session: Session
+        val session: Session.WithRealUser
     ) : HiwiTrackerNavigation, Screen {
         override val key = OverviewScreen::class.simpleName!!
         @Composable fun vm() = rememberScreenModelWithApp { OverviewViewModel.create(this@OverviewScreen) }
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/Helpers.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/Helpers.kt
index a5e09589e82ac94f3c0b81b41be8283abb0ee9b3..3228432299b39b6318d0d4235b18c41e5d2cbd73 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/Helpers.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/Helpers.kt
@@ -1,20 +1,26 @@
 package net.novagamestudios.kaffeekasse.ui.util
 
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateValueAsState
+import androidx.compose.animation.core.spring
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
 import androidx.compose.runtime.remember
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.ColorMatrix
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
 import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import kotlinx.coroutines.sync.Mutex
-import net.novagamestudios.common_utils.compose.state.ReentrantActionState
 import net.novagamestudios.common_utils.debug
 import net.novagamestudios.common_utils.toastShort
-import net.novagamestudios.kaffeekasse.util.withReentrantLock
 
 
 @Composable
@@ -44,14 +50,6 @@ private fun ColorMatrix.setToTint(tint: Color) {
 
 
 
-suspend fun <R> ReentrantActionState.trueWhileWithMutex(mutex: Mutex, block: suspend () -> R): R = trueWhile {
-    mutex.withReentrantLock {
-        block()
-    }
-}
-
-
-
 @OptIn(InternalVoyagerApi::class)
 @Composable
 fun debugNavigation() {
@@ -71,3 +69,36 @@ fun debugNavigation() {
 }
 
 
+@Composable
+fun animateTextUnitAsState(
+    targetValue: TextUnit,
+    animationSpec: AnimationSpec<TextUnit> = spring(),
+    label: String = "TextUnitAnimation",
+    finishedListener: ((TextUnit) -> Unit)? = null
+): State<TextUnit> {
+    val density = LocalDensity.current
+    return animateValueAsState(
+        targetValue = targetValue,
+        typeConverter = remember(density) {
+            val dpConverter = Dp.VectorConverter
+            with(density) {
+                TwoWayConverter(
+                    convertToVector = { dpConverter.convertToVector( it.toDp()) },
+                    convertFromVector = { dpConverter.convertFromVector(it).toSp() }
+                )
+            }
+        },
+        animationSpec = animationSpec,
+        label = label,
+        finishedListener = finishedListener
+    )
+}
+
+
+@OptIn(InternalVoyagerApi::class)
+fun Navigator.requireWithKey(key: String): Navigator {
+    if (this.key == key) return this
+    val parent = parent ?: error("Navigator with key '$key' not found")
+    return parent.requireWithKey(key)
+}
+
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/KeyedMultiDataSource.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/KeyedMultiDataSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..95a3fb891cd8a4674b9bd79a3b0df883d5a1cdfd
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/KeyedMultiDataSource.kt
@@ -0,0 +1,33 @@
+package net.novagamestudios.kaffeekasse.util.richdata
+
+import kotlinx.coroutines.CoroutineScope
+
+class KeyedMultiDataSource<K, T : Any>(
+    private val coroutineScope: CoroutineScope,
+    private val producer: suspend RichDataCollector<T>.(K) -> RichData<T>,
+) {
+    private val dataByKey = mutableMapOf<K, RichDataSource<T>>()
+
+    operator fun get(key: K): RichDataSource<T> = dataByKey.getOrPut(key) {
+        with(coroutineScope) {
+            RichDataSource.RichDataSource {
+                producer(key)
+            }
+        }
+    }
+
+    fun clear() {
+        dataByKey.clear()
+    }
+
+    fun markDirty() {
+        dataByKey.values.forEach { it.markDirty() }
+    }
+
+    companion object {
+        context (CoroutineScope)
+        operator fun <K, T : Any> invoke(
+            producer: suspend RichDataCollector<T>.(K) -> RichData<T>
+        ) = KeyedMultiDataSource(this@CoroutineScope, producer)
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataFunctions.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataFunctions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c06161256fe40b7432deecbaf8bb61da9003e249
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataFunctions.kt
@@ -0,0 +1,50 @@
+package net.novagamestudios.kaffeekasse.util.richdata
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.supervisorScope
+
+interface RichDataFunctions<in T : Any> {
+    fun markDirty()
+    suspend fun ensureCleanData()
+    suspend fun refresh()
+
+    companion object {
+        @Suppress("unused")
+        val NOOP: RichDataFunctions<Any> = object : RichDataFunctions<Any> {
+            override fun markDirty() = Unit
+            override suspend fun ensureCleanData() = Unit
+            override suspend fun refresh() = Unit
+        }
+    }
+}
+
+operator fun <T : Any> RichDataFunctions<T>.plus(
+    other: RichDataFunctions<T>
+): RichDataFunctions<T> = object : RichDataFunctions<T> {
+    override fun markDirty() {
+        this@plus.markDirty()
+        other.markDirty()
+    }
+    override suspend fun ensureCleanData(): Unit = supervisorScope {
+        launch { this@plus.ensureCleanData() }
+        launch { other.ensureCleanData() }
+    }
+    override suspend fun refresh(): Unit = supervisorScope {
+        launch { this@plus.refresh() }
+        launch { other.refresh() }
+    }
+}
+
+
+
+infix fun <T : Any> RichDataStateFlow<out T>.withFunctions(
+    functions: RichDataFunctions<T>
+): RichDataSource<T> = object : RichDataSource<T>,
+    RichDataStateFlow<T> by this,
+    RichDataFunctions<T> by functions { }
+
+infix fun <T : Any> RichDataState<T>.withFunctions(
+    functions: RichDataFunctions<T>
+): RichDataStateWithFunctions<T> = object : RichDataStateWithFunctions<T>,
+    RichDataState<T> by this,
+    RichDataFunctions<T> by functions { }
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataSource.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataSource.kt
index 523756d53af76b102aed99e41832098e573162ad..4374a937e6089b9cfa50b8c0463ffee3917ee8c5 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataSource.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataSource.kt
@@ -4,37 +4,15 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.channelFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
 import net.novagamestudios.common_utils.compose.components.Progress
 import kotlin.coroutines.suspendCoroutine
 
-typealias RichDataFactory<T> = suspend FlowCollector<RichData<T>>.() -> RichData<T>
+typealias RichDataCollector<T> = FlowCollector<RichData<T>>
+typealias RichDataFactory<T> = suspend RichDataCollector<T>.() -> RichData<T>
 
-private typealias Updater<T> = suspend FlowCollector<RichData<T>>.(RichData<T>) -> Unit
-
-
-interface RichDataFunctions<in T : Any> {
-    suspend fun markDirty()
-    suspend fun ensureCleanData()
-    suspend fun refresh()
-
-    companion object {
-        // no operation
-        val NOOP: RichDataFunctions<Any> = object : RichDataFunctions<Any> {
-            override suspend fun markDirty() = Unit
-            override suspend fun ensureCleanData() = Unit
-            override suspend fun refresh() = Unit
-        }
-    }
-}
 
 interface RichDataSource<T : Any> : RichDataFunctions<T>, RichDataStateFlow<T> {
     companion object {
@@ -45,6 +23,9 @@ interface RichDataSource<T : Any> : RichDataFunctions<T>, RichDataStateFlow<T> {
     }
 }
 
+
+private typealias Updater<T> = suspend RichDataCollector<T>.(RichData<T>) -> Unit
+
 private class RichDataProducer<T : Any>(
     coroutineScope: CoroutineScope,
     producer: RichDataFactory<T>
@@ -58,7 +39,7 @@ private class RichDataProducer<T : Any>(
 
     private val outFlow: StateFlow<RichData<T>> = channelFlow {
         var current: RichData<T> = RichData.None()
-        val collector = FlowCollector<RichData<T>> {
+        val collector = RichDataCollector<T> {
             current = it
             this.send(it)
         }
@@ -74,7 +55,7 @@ private class RichDataProducer<T : Any>(
     }.stateIn(coroutineScope, SharingStarted.Lazily)
 
     override val replayCache get() = outFlow.replayCache
-    override suspend fun collect(collector: FlowCollector<RichData<T>>) = outFlow.collect(collector)
+    override suspend fun collect(collector: RichDataCollector<T>) = outFlow.collect(collector)
     override val value: RichData<T> get() = outFlow.value
 
     init {
@@ -91,10 +72,13 @@ private class RichDataProducer<T : Any>(
         }
     }
 
-    override suspend fun markDirty() = awaitUpdater { current ->
+    override fun markDirty() = enqueueUpdater { current ->
         if (current is RichData.Data && !current.isDirty) {
             emit(current.copy(isDirty = true))
         }
+//        if (current is RichData.Data) {
+//            emit(RichData.None())
+//        }
     }
 
     override suspend fun ensureCleanData() = awaitUpdater { current ->
@@ -111,46 +95,13 @@ private class RichDataProducer<T : Any>(
 }
 
 
-context (FlowCollector<RichData<T>>)
+context (RichDataCollector<T>)
 suspend fun <T : Any> progress(progress: Progress) {
     emit(RichData.Loading(progress))
 }
 
-context (FlowCollector<RichData<T>>)
+context (RichDataCollector<T>)
 suspend fun <T : Any> progress(progress: Float) {
     emit(RichData.Loading(Progress.Determinate(progress)))
 }
 
-
-fun <T : Any> derivedRichDataSource(
-    functions: RichDataFunctions<T>,
-    stateFlow: RichDataStateFlow<out T>
-): RichDataSource<T> = object : RichDataSource<T>, RichDataFunctions<T> by functions, RichDataStateFlow<T> by stateFlow { }
-
-fun <T : Any> RichDataStateFlow<T>.asRichDataSource(): RichDataSource<T> = derivedRichDataSource(
-    RichDataFunctions.NOOP,
-    this
-)
-
-fun <T : Any> Flow<T>.asRichDataSourceIn(
-    coroutineScope: CoroutineScope
-): RichDataSource<T> = derivedRichDataSource(
-    RichDataFunctions.NOOP,
-    richStateIn(coroutineScope)
-)
-
-operator fun <T : Any> RichDataFunctions<T>.plus(other: RichDataFunctions<T>): RichDataFunctions<T> = object : RichDataFunctions<T> {
-    override suspend fun markDirty() {
-        this@plus.markDirty()
-        other.markDirty()
-    }
-    override suspend fun ensureCleanData() {
-        this@plus.ensureCleanData()
-        other.ensureCleanData()
-    }
-    override suspend fun refresh() {
-        this@plus.refresh()
-        other.refresh()
-    }
-}
-
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataState.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataState.kt
index 7ecceeccae7787c2dacc538a35bf2909fe795985..b825afde68d9b0da260812278ecfdb1393271d22 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataState.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/util/richdata/RichDataState.kt
@@ -13,14 +13,16 @@ import cafe.adriel.voyager.core.model.screenModelScope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
-interface RichDataState<T : Any> : State<RichData<T>> {
+interface RichDataState<out T : Any> : State<RichData<T>> {
     val isLoading: Boolean
     val isNone: Boolean
-    val isLoadingOrNone: Boolean get() = isLoading || isNone
     val dataOrNull: T?
     val errorOrNull: RichData.Error<T>?
 }
 
+interface RichDataStateWithFunctions<T : Any> : RichDataState<T>, RichDataFunctions<T>
+
+
 class MutableRichDataState<T : Any>(
     private val initial: RichData<T> = RichData.None()
 ) : MutableState<RichData<T>> by mutableStateOf(initial), RichDataState<T> {
@@ -30,7 +32,10 @@ class MutableRichDataState<T : Any>(
     override val errorOrNull by derivedStateOf { value.errorOrNull }
 }
 
+val <T : Any> RichDataState<T>.isLoadingOrNone: Boolean get() = isLoading || isNone
+
 fun <T : Any> MutableRichDataState<T>.asRichDataState(): RichDataState<T> = this
+
 fun <T : Any> RichDataFlow<T>.collectAsRichStateIn(coroutineScope: CoroutineScope): RichDataState<T> {
     val mutableState = MutableRichDataState<T>()
     coroutineScope.launch {
@@ -38,12 +43,19 @@ fun <T : Any> RichDataFlow<T>.collectAsRichStateIn(coroutineScope: CoroutineScop
     }
     return mutableState.asRichDataState()
 }
+fun <T : Any> RichDataSource<T>.collectAsRichStateIn(coroutineScope: CoroutineScope): RichDataStateWithFunctions<T> {
+    return (this as RichDataFlow<T>).collectAsRichStateIn(coroutineScope) withFunctions this
+}
 
 context (CoroutineScope)
 fun <T : Any> RichDataFlow<T>.collectAsRichStateHere() = collectAsRichStateIn(this@CoroutineScope)
+context (CoroutineScope)
+fun <T : Any> RichDataSource<T>.collectAsRichStateHere() = collectAsRichStateIn(this@CoroutineScope)
 
 context (ScreenModel)
 fun <T : Any> RichDataFlow<T>.collectAsRichStateHere() = collectAsRichStateIn(screenModelScope)
+context (ScreenModel)
+fun <T : Any> RichDataSource<T>.collectAsRichStateHere() = collectAsRichStateIn(screenModelScope)
 
 
 @Composable
@@ -58,3 +70,5 @@ fun <T : Any> RichDataFlow<T>.collectAsRichState(): MutableRichDataState<T> {
 
 
 
+
+