android: add logout flow with full local session cleanup
Some checks are pending
CI / test (push) Has started running
Some checks are pending
CI / test (push) Has started running
This commit is contained in:
@@ -20,4 +20,8 @@ class ActiveChatTracker @Inject constructor() {
|
||||
_activeChatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_activeChatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.daemonlord.messenger.data.auth.repository
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import ru.daemonlord.messenger.data.chat.local.db.MessengerDatabase
|
||||
import ru.daemonlord.messenger.di.IoDispatcher
|
||||
import ru.daemonlord.messenger.domain.auth.repository.SessionCleanupRepository
|
||||
import ru.daemonlord.messenger.domain.notifications.repository.NotificationSettingsRepository
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DefaultSessionCleanupRepository @Inject constructor(
|
||||
private val database: MessengerDatabase,
|
||||
private val notificationSettingsRepository: NotificationSettingsRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : SessionCleanupRepository {
|
||||
|
||||
override suspend fun clearLocalSessionData() = withContext(ioDispatcher) {
|
||||
database.clearAllTables()
|
||||
notificationSettingsRepository.clearChatOverrides()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,17 +66,25 @@ class DataStoreNotificationSettingsRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearChatOverrides() {
|
||||
dataStore.edit { preferences ->
|
||||
val keysToRemove = preferences.asMap().keys
|
||||
.filter { key -> key.name.startsWith(CHAT_OVERRIDE_PREFIX) }
|
||||
keysToRemove.forEach { key -> preferences.remove(key) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Preferences.chatOverride(chatId: Long): ChatNotificationOverride {
|
||||
return this[chatOverrideKey(chatId)]
|
||||
?.let { runCatching { ChatNotificationOverride.valueOf(it) }.getOrNull() }
|
||||
?: ChatNotificationOverride.DEFAULT
|
||||
}
|
||||
|
||||
private fun chatOverrideKey(chatId: Long) = stringPreferencesKey("notification_chat_override_$chatId")
|
||||
private fun chatOverrideKey(chatId: Long) = stringPreferencesKey("$CHAT_OVERRIDE_PREFIX$chatId")
|
||||
|
||||
private companion object {
|
||||
const val CHAT_OVERRIDE_PREFIX = "notification_chat_override_"
|
||||
val GLOBAL_ENABLED_KEY = booleanPreferencesKey("notification_global_enabled")
|
||||
val PREVIEW_ENABLED_KEY = booleanPreferencesKey("notification_preview_enabled")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import ru.daemonlord.messenger.data.auth.repository.NetworkAuthRepository
|
||||
import ru.daemonlord.messenger.data.auth.repository.DefaultSessionCleanupRepository
|
||||
import ru.daemonlord.messenger.data.chat.repository.NetworkChatRepository
|
||||
import ru.daemonlord.messenger.data.media.repository.NetworkMediaRepository
|
||||
import ru.daemonlord.messenger.data.message.repository.NetworkMessageRepository
|
||||
import ru.daemonlord.messenger.data.notifications.repository.DataStoreNotificationSettingsRepository
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.auth.repository.SessionCleanupRepository
|
||||
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
|
||||
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
||||
import ru.daemonlord.messenger.domain.message.repository.MessageRepository
|
||||
@@ -26,6 +28,12 @@ abstract class RepositoryModule {
|
||||
repository: NetworkAuthRepository,
|
||||
): AuthRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindSessionCleanupRepository(
|
||||
repository: DefaultSessionCleanupRepository,
|
||||
): SessionCleanupRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindChatRepository(
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.daemonlord.messenger.domain.auth.repository
|
||||
|
||||
interface SessionCleanupRepository {
|
||||
suspend fun clearLocalSessionData()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package ru.daemonlord.messenger.domain.auth.usecase
|
||||
|
||||
import ru.daemonlord.messenger.core.notifications.ActiveChatTracker
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.auth.repository.SessionCleanupRepository
|
||||
import ru.daemonlord.messenger.domain.realtime.RealtimeManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class LogoutUseCase @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val sessionCleanupRepository: SessionCleanupRepository,
|
||||
private val realtimeManager: RealtimeManager,
|
||||
private val activeChatTracker: ActiveChatTracker,
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
realtimeManager.disconnect()
|
||||
activeChatTracker.clear()
|
||||
authRepository.logout()
|
||||
sessionCleanupRepository.clearLocalSessionData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@ interface NotificationSettingsRepository {
|
||||
suspend fun getChatOverride(chatId: Long): ChatNotificationOverride
|
||||
suspend fun setChatOverride(chatId: Long, mode: ChatNotificationOverride)
|
||||
suspend fun clearChatOverride(chatId: Long)
|
||||
suspend fun clearChatOverrides()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.daemonlord.messenger.domain.auth.usecase.LoginUseCase
|
||||
import ru.daemonlord.messenger.domain.auth.usecase.LogoutUseCase
|
||||
import ru.daemonlord.messenger.domain.auth.usecase.RestoreSessionUseCase
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
@@ -18,6 +19,7 @@ import javax.inject.Inject
|
||||
class AuthViewModel @Inject constructor(
|
||||
private val restoreSessionUseCase: RestoreSessionUseCase,
|
||||
private val loginUseCase: LoginUseCase,
|
||||
private val logoutUseCase: LogoutUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(AuthUiState())
|
||||
@@ -68,6 +70,22 @@ class AuthViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||
runCatching { logoutUseCase() }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
email = "",
|
||||
password = "",
|
||||
isLoading = false,
|
||||
isAuthenticated = false,
|
||||
errorMessage = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreSession() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isCheckingSession = true) }
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -50,6 +51,7 @@ import java.time.format.DateTimeFormatter
|
||||
@Composable
|
||||
fun ChatListRoute(
|
||||
onOpenChat: (Long) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
inviteToken: String?,
|
||||
onInviteTokenConsumed: () -> Unit,
|
||||
viewModel: ChatListViewModel = hiltViewModel(),
|
||||
@@ -72,6 +74,7 @@ fun ChatListRoute(
|
||||
onFilterSelected = viewModel::onFilterSelected,
|
||||
onSearchChanged = viewModel::onSearchChanged,
|
||||
onRefresh = viewModel::onPullToRefresh,
|
||||
onLogout = onLogout,
|
||||
onOpenChat = onOpenChat,
|
||||
)
|
||||
}
|
||||
@@ -84,6 +87,7 @@ fun ChatListScreen(
|
||||
onFilterSelected: (ChatListFilter) -> Unit,
|
||||
onSearchChanged: (String) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onOpenChat: (Long) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
@@ -115,6 +119,16 @@ fun ChatListScreen(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onLogout) {
|
||||
Text("Logout")
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -125,6 +125,7 @@ fun MessengerNavHost(
|
||||
ChatListRoute(
|
||||
inviteToken = inviteToken,
|
||||
onInviteTokenConsumed = onInviteTokenConsumed,
|
||||
onLogout = viewModel::logout,
|
||||
onOpenChat = { chatId ->
|
||||
navController.navigate("${Routes.Chat}/$chatId")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user