Wire Firebase auth state into settings/logout and refresh Google sign-in config.

This updates the settings account section to show/logout based on authenticated user state, refines auth service naming, and aligns app signing/Firebase config changes needed for successful Google authentication.

Made-with: Cursor
This commit is contained in:
2026-03-01 21:40:58 +01:00
parent 91a9521f3e
commit 28ad63fb4a
10 changed files with 79 additions and 71 deletions

View File

@@ -1,4 +1,5 @@
import com.android.build.api.dsl.ApplicationExtension
import java.util.Properties
plugins {
alias(libs.plugins.android.application)

View File

@@ -14,48 +14,11 @@
},
"oauth_client": [
{
"client_id": "223884730019-0ddlndpmfesrt1vk5vdsqgeudkmoanls.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCcgdvdp8xec2dKUqlQHl9peQAyOWRVga4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "223884730019-0ddlndpmfesrt1vk5vdsqgeudkmoanls.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:223884730019:android:1345a64a13459de7562916",
"android_client_info": {
"package_name": "org.db3.airmq.debug"
}
},
"oauth_client": [
{
"client_id": "223884730019-0cobqn6haoga9n5o60lqcke9uglqihea.apps.googleusercontent.com",
"client_id": "223884730019-nrh7lbjumja79jlci98u54jjc9utc988.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "org.db3.airmq.debug",
"certificate_hash": "38547bbc27af278e820c78fc012227a40f64505f"
}
},
{
"client_id": "223884730019-p44o1v2t3g6v0kmopsa65f0umf60ksif.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "org.db3.airmq.debug",
"certificate_hash": "195356c67cabf18c9ab0a8ccfcec45c3346b6b6c"
"package_name": "org.db3.airmq",
"certificate_hash": "d9df75253752259b83100ffb1c809615b9216be2"
}
},
{

View File

@@ -392,7 +392,9 @@ private suspend fun launchGoogleSignIn(context: Context): Result<String> = runCa
val activity = context.findActivity()
val request = GetCredentialRequest.Builder()
.addCredentialOption(
GetSignInWithGoogleOption.Builder(context.getString(R.string.default_web_client_id))
GetGoogleIdOption.Builder()
.setServerClientId(context.getString(R.string.default_web_client_id))
.setFilterByAuthorizedAccounts(false)
.build()
)
.build()

View File

@@ -52,7 +52,7 @@ class ManageViewModel @Inject constructor(
private fun refreshAuthState() {
viewModelScope.launch {
val session = authService.getCurrentSession()
val session = authService.getUser()
_uiState.value = if (session?.isAuthenticated == true) {
authorizedState(session)
} else {

View File

@@ -40,7 +40,6 @@ import org.db3.airmq.R
import org.db3.airmq.features.settings.SettingsScreenContract.Action
import org.db3.airmq.features.settings.SettingsScreenContract.Event
import org.db3.airmq.features.settings.SettingsScreenContract.State
import org.db3.airmq.features.settings.SettingsScreenContract.UserMode
import org.db3.airmq.ui.theme.AirMQTheme
@Composable
@@ -100,6 +99,22 @@ private fun SettingsScreenContent(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
if (uiState.isAuthorized) {
Text(
text = stringResource(id = R.string.pref_account_header),
style = MaterialTheme.typography.labelLarge,
color = Color(0xFF607D8B),
modifier = Modifier.padding(start = headerTextStart, top = 4.dp, bottom = 4.dp)
)
PreferenceRow(
iconName = "ic_pref_logout",
iconFallbackRes = android.R.drawable.ic_lock_power_off,
title = stringResource(id = R.string.pref_logout_title),
iconSize = iconSize,
iconTextGap = iconTextGap,
onClick = { onEvent(Event.LogOutClicked) }
)
}
Text(
text = stringResource(id = R.string.pref_application_header),
style = MaterialTheme.typography.labelLarge,
@@ -141,16 +156,6 @@ private fun SettingsScreenContent(
iconTextGap = iconTextGap,
onClick = { onEvent(Event.AboutClicked) }
)
if (uiState.userMode == UserMode.AUTHORIZED) {
PreferenceRow(
iconName = "ic_pref_debug",
iconFallbackRes = android.R.drawable.ic_lock_power_off,
title = stringResource(id = R.string.pref_logout_title),
iconSize = iconSize,
iconTextGap = iconTextGap,
onClick = { onEvent(Event.LogOutClicked) }
)
}
}
}
}
@@ -252,7 +257,7 @@ private fun SettingsScreenAnonymousPreview() {
AirMQTheme {
SettingsScreenContent(
uiState = State(
userMode = UserMode.ANONYMOUS,
isAuthorized = false,
city = "Minsk",
deviceStatusNotificationsEnabled = true,
offlineDevicesVisible = false,
@@ -270,7 +275,25 @@ private fun SettingsScreenAdvancedPreview() {
AirMQTheme {
SettingsScreenContent(
uiState = State(
userMode = UserMode.ANONYMOUS,
isAuthorized = false,
city = "Minsk",
deviceStatusNotificationsEnabled = true,
offlineDevicesVisible = true,
advancedEnabled = true,
isLoading = false
),
onEvent = {}
)
}
}
@Preview(name = "Settings Advanced", showBackground = true, showSystemUi = true)
@Composable
private fun SettingsScreenAuthorizedPreview() {
AirMQTheme {
SettingsScreenContent(
uiState = State(
isAuthorized = true,
city = "Minsk",
deviceStatusNotificationsEnabled = true,
offlineDevicesVisible = true,

View File

@@ -1,14 +1,8 @@
package org.db3.airmq.features.settings
object SettingsScreenContract {
enum class UserMode {
ANONYMOUS,
AUTHORIZED
}
data class State(
val userMode: UserMode = UserMode.ANONYMOUS,
val isAuthorized: Boolean = false,
val city: String = "",
val deviceStatusNotificationsEnabled: Boolean = true,
val offlineDevicesVisible: Boolean = false,

View File

@@ -18,18 +18,16 @@ import org.db3.airmq.R
import org.db3.airmq.features.settings.SettingsScreenContract.Action
import org.db3.airmq.features.settings.SettingsScreenContract.Event
import org.db3.airmq.features.settings.SettingsScreenContract.State
import org.db3.airmq.features.settings.SettingsScreenContract.UserMode
import org.db3.airmq.sdk.auth.AuthService
import org.db3.airmq.sdk.settings.SettingsService
@HiltViewModel
class SettingsViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
private val settingsService: SettingsService
private val settingsService: SettingsService,
private val authService: AuthService
) : ViewModel() {
// Temporary migration stub: keep screen in anonymous mode.
private val forceAnonymous = true
private val _uiState = MutableStateFlow(State())
val uiState: StateFlow<State> = _uiState.asStateFlow()
@@ -45,7 +43,7 @@ class SettingsViewModel @Inject constructor(
Event.CityClicked -> _actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon)))
Event.AboutClicked -> _actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon)))
Event.DebugClicked -> _actions.tryEmit(Action.OpenDebug)
Event.LogOutClicked -> _actions.tryEmit(Action.LogOutToManage)
Event.LogOutClicked -> logOut()
is Event.DeviceStatusNotificationsChanged -> {
updateToggle(
toggle = { settingsService.setDeviceStatusNotificationsEnabled(event.enabled) },
@@ -69,12 +67,13 @@ class SettingsViewModel @Inject constructor(
private fun loadSettings() {
viewModelScope.launch(Dispatchers.IO) {
val session = authService.getUser()
val city = settingsService.getCity() ?: appContext.getString(R.string.city_minsk)
val deviceStatus = settingsService.getDeviceStatusNotificationsEnabled()
val offlineVisible = settingsService.getOfflineDevicesVisible()
val advanced = settingsService.getAdvancedEnabled()
_uiState.value = _uiState.value.copy(
userMode = if (forceAnonymous) UserMode.ANONYMOUS else UserMode.AUTHORIZED,
isAuthorized = session?.isAuthenticated == true,
city = city,
deviceStatusNotificationsEnabled = deviceStatus,
offlineDevicesVisible = offlineVisible,
@@ -103,4 +102,19 @@ class SettingsViewModel @Inject constructor(
}
}
}
private fun logOut() {
viewModelScope.launch(Dispatchers.IO) {
val result = authService.signOut()
if (result.isSuccess) {
_actions.tryEmit(Action.LogOutToManage)
} else {
_actions.tryEmit(
Action.ShowMessage(
result.exceptionOrNull()?.message ?: appContext.getString(R.string.toast_error)
)
)
}
}
}
}

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="@android:color/white"
android:pathData="M10.79,16.29c0.39,0.39 1.02,0.39 1.41,0l3.59,-3.59c0.39,-0.39 0.39,-1.02 0,-1.41L12.2,7.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L12.67,11H4c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h8.67l-1.88,1.88c-0.39,0.39 -0.38,1.03 0,1.41zM19,3H5c-1.11,0 -2,0.9 -2,2v3c0,0.55 0.45,1 1,1s1,-0.45 1,-1V6c0,-0.55 0.45,-1 1,-1h12c0.55,0 1,0.45 1,1v12c0,0.55 -0.45,1 -1,1H6c-0.55,0 -1,-0.45 -1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@@ -4,7 +4,7 @@ import org.db3.airmq.sdk.auth.model.AuthProvider
import org.db3.airmq.sdk.auth.model.User
interface AuthService {
suspend fun getCurrentSession(): User?
suspend fun getUser(): User?
suspend fun isAuthenticated(): Boolean
suspend fun signIn(provider: AuthProvider, token: String): Result<User>
suspend fun signOut(): Result<Unit>

View File

@@ -17,7 +17,7 @@ class FirebaseAuthService @Inject constructor(
private val firebaseAuth: FirebaseAuth
) : AuthService {
override suspend fun getCurrentSession(): User? = firebaseAuth.currentUser?.toUser()
override suspend fun getUser(): User? = firebaseAuth.currentUser?.toUser()
override suspend fun isAuthenticated(): Boolean = firebaseAuth.currentUser != null