fix(manage): use account-scoped device sync and remove mock device source

Preserve Manage device state across resume while preventing cross-account cache leaks by tracking synced user IDs and clearing stale local device data on account changes. Replace the mock device remote data source with Apollo-backed API queries/mutations using me.locations so Manage only shows devices belonging to the authenticated user.

Made-with: Cursor
This commit is contained in:
2026-04-07 18:04:42 +02:00
parent 0a79ee5e04
commit 89ce2e1afa
19 changed files with 450 additions and 145 deletions

View File

@@ -1,6 +1,5 @@
package org.db3.airmq.features.manage
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
@@ -68,7 +67,6 @@ fun ManageScreen(
viewModel: ManageViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Log.d("MANAGE_DEBUG", uiState.toString())
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner, viewModel) {
val observer = LifecycleEventObserver { _, event ->
@@ -413,6 +411,7 @@ private fun ManageScreenAuthorizedPreview() {
ManageScreenContent(
uiState = State(
isAuthorized = true,
userId = "preview",
userName = "User",
userEmail = "user@example.com",
devices = listOf(

View File

@@ -11,6 +11,7 @@ object ManageScreenContract {
data class State(
val isAuthorized: Boolean = false,
val userId: String = "",
val userName: String = "",
val userEmail: String = "",
val devicesLabel: String = "",

View File

@@ -21,8 +21,8 @@ import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem
import org.db3.airmq.features.manage.ManageScreenContract.Event
import org.db3.airmq.features.manage.ManageScreenContract.State
import org.db3.airmq.sdk.auth.AuthService
import org.db3.airmq.sdk.auth.model.User
import org.db3.airmq.sdk.device.data.remote.DeviceSubscriptionManager
import org.db3.airmq.sdk.device.domain.DeviceRepository
import org.db3.airmq.sdk.device.domain.OnlineStatus
@HiltViewModel
@@ -30,6 +30,7 @@ class ManageViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
private val authService: AuthService,
private val getMyDevicesUseCase: GetMyDevicesUseCase,
private val deviceRepository: DeviceRepository,
private val subscriptionManager: DeviceSubscriptionManager
) : ViewModel() {
@@ -47,7 +48,7 @@ class ManageViewModel @Inject constructor(
if (session?.isAuthenticated == true) {
val user = session
_uiState.update { state ->
if (state.isAuthorized) {
if (state.isAuthorized && state.userId == session.userId) {
state.copy(
devices = devices.map { device -> device.toDeviceItem(appContext) }
)
@@ -75,8 +76,20 @@ class ManageViewModel @Inject constructor(
viewModelScope.launch {
val session = authService.getUser()
if (session?.isAuthenticated == true) {
subscriptionManager.start()
_uiState.value = authorizedState(session)
deviceRepository.ensureLocalDevicesMatchAccount(session.userId)
subscriptionManager.start(session.userId)
_uiState.update { prev ->
val sameUser = prev.isAuthorized && prev.userId == session.userId
State(
isAuthorized = true,
userId = session.userId,
userName = session.displayName
?: appContext.getString(R.string.text_anonymous_user),
userEmail = session.email ?: "",
devicesLabel = "",
devices = if (sameUser) prev.devices else emptyList()
)
}
} else {
subscriptionManager.stop()
_uiState.value = anonymousState()
@@ -86,19 +99,12 @@ class ManageViewModel @Inject constructor(
private fun anonymousState(): State = State(
isAuthorized = false,
userId = "",
userName = appContext.getString(R.string.text_anonymous_user),
userEmail = appContext.getString(R.string.text_please_sign_in),
devicesLabel = appContext.getString(R.string.text_sign_in_small)
)
private fun authorizedState(user: User): State = State(
isAuthorized = true,
userName = user.displayName ?: appContext.getString(R.string.text_anonymous_user),
userEmail = user.email ?: "",
devicesLabel = "",
devices = emptyList()
)
private fun org.db3.airmq.sdk.device.domain.Device.toDeviceItem(context: Context): DeviceItem {
val statusText = when (toOnlineStatus()) {
OnlineStatus.Online -> context.getString(R.string.map_status_online)