feat(auth): email sign-in and register via GraphQL
Add loginLocal and register Apollo mutations, LocalEmailAuthStore for profile snapshot when not using Firebase, extend AuthService/FirebaseAuthService with session handoff vs Google, wire EmailLoginViewModel, refresh Manage on resume, and expand unit tests. Made-with: Cursor
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package org.db3.airmq.features.login
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Patterns
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
@@ -11,14 +13,17 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.db3.airmq.R
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.Action
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.Event
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.State
|
||||
import org.db3.airmq.sdk.auth.AuthService
|
||||
|
||||
@HiltViewModel
|
||||
class EmailLoginViewModel @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val authService: AuthService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(State())
|
||||
@@ -35,19 +40,45 @@ class EmailLoginViewModel @Inject constructor(
|
||||
is Event.PasswordChanged -> {
|
||||
_uiState.value = _uiState.value.copy(password = event.value)
|
||||
}
|
||||
Event.SignInClicked -> {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
_actions.tryEmit(
|
||||
Action.ShowMessage(appContext.getString(R.string.coming_soon))
|
||||
)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||
}
|
||||
Event.RegisterClicked -> {
|
||||
_actions.tryEmit(
|
||||
Action.ShowMessage(appContext.getString(R.string.coming_soon))
|
||||
)
|
||||
}
|
||||
Event.SignInClicked -> submitEmailAuth(register = false)
|
||||
Event.RegisterClicked -> submitEmailAuth(register = true)
|
||||
Event.BackClicked -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitEmailAuth(register: Boolean) {
|
||||
val email = _uiState.value.email.trim()
|
||||
val password = _uiState.value.password
|
||||
if (email.isEmpty() || password.isEmpty()) {
|
||||
_actions.tryEmit(
|
||||
Action.ShowMessage(appContext.getString(R.string.toast_email_fields_required))
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
|
||||
_actions.tryEmit(
|
||||
Action.ShowMessage(appContext.getString(R.string.toast_invalid_email))
|
||||
)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
try {
|
||||
val result = if (register) {
|
||||
authService.registerWithEmail(email, password, name = "")
|
||||
} else {
|
||||
authService.loginWithEmailPassword(email, password)
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
_actions.tryEmit(Action.OpenManage)
|
||||
} else {
|
||||
val message = result.exceptionOrNull()?.message
|
||||
?: appContext.getString(R.string.toast_email_auth_failed)
|
||||
_actions.tryEmit(Action.ShowMessage(message))
|
||||
}
|
||||
} finally {
|
||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -63,6 +67,16 @@ fun ManageScreen(
|
||||
viewModel: ManageViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner, viewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
viewModel.refreshAuthState()
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.actions.collectLatest { action ->
|
||||
when (action) {
|
||||
|
||||
@@ -71,7 +71,7 @@ class ManageViewModel @Inject constructor(
|
||||
|
||||
private fun initialState(): State = anonymousState()
|
||||
|
||||
private fun refreshAuthState() {
|
||||
fun refreshAuthState() {
|
||||
viewModelScope.launch {
|
||||
val session = authService.getUser()
|
||||
if (session?.isAuthenticated == true) {
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
<string name="notification_device_status_title">Ваша прылада адключылася ад сеткі!</string>
|
||||
<string name="notification_device_status_text">%s цяпер неактыўны. Націсніце на гэта апавяшчэнне, каб адчыніць канфігурацыю прылады</string>
|
||||
<string name="toast_oauth_failed">Памылка аўтарызацыі Google</string>
|
||||
<string name="toast_email_fields_required">Увядзіце email і пароль</string>
|
||||
<string name="toast_invalid_email">Увядзіце карэктны email</string>
|
||||
<string name="toast_email_auth_failed">Не ўдалося ўвайсці</string>
|
||||
<string name="snackbar_not_registered">Патрабуецца рэгістрацыя</string>
|
||||
<string name="text_policy_label">Уваходзячы ў сістэму, вы згаджаецеся з нашымі %1$s и %2$s</string>
|
||||
<string name="text_what_does_it_mean"><u>Што гэта значыць?</u></string>
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
<string name="notification_device_status_title">Ваше устройство отключилось от сети!</string>
|
||||
<string name="notification_device_status_text">%s теперь неактивен. Нажмите на это уведомление, чтобы открыть конфигурацию устройства</string>
|
||||
<string name="toast_oauth_failed">Ошибка авторизации Google</string>
|
||||
<string name="toast_email_fields_required">Введите email и пароль</string>
|
||||
<string name="toast_invalid_email">Введите корректный email</string>
|
||||
<string name="toast_email_auth_failed">Не удалось войти</string>
|
||||
<string name="snackbar_not_registered">Требуется регистрация</string>
|
||||
<string name="text_policy_label">Входя в систему, вы соглашаетесь с нашими %1$s и %2$s</string>
|
||||
<string name="text_what_does_it_mean"><u>Что это значит?</u></string>
|
||||
|
||||
@@ -142,6 +142,9 @@
|
||||
|
||||
<!-- Snackbars and toasts -->
|
||||
<string name="toast_oauth_failed" tools:ignore="UnusedResources">Google authorisation failed</string>
|
||||
<string name="toast_email_fields_required">Enter email and password</string>
|
||||
<string name="toast_invalid_email">Enter a valid email address</string>
|
||||
<string name="toast_email_auth_failed">Sign-in failed</string>
|
||||
<string name="toast_copied">Copied</string>
|
||||
<string name="snackbar_not_registered">Device requires registration</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user