From a343f2e2a0db11dcc0bfa5564ae906e9eac8abdc Mon Sep 17 00:00:00 2001 From: "Z. Charles Dziura" Date: Thu, 14 Nov 2024 14:32:51 -0500 Subject: [PATCH] Store user credentials after registration --- app/app/build.gradle.kts | 4 +- .../debtpirate/app/host/MainActivity.kt | 1 + .../auth/presentation/login/LoginScreen.kt | 37 +++++- .../login/LoginScreenStateAction.kt | 6 + .../login/LoginScreenViewModel.kt | 14 ++- .../register/RegistrationScreen.kt | 118 +++++++++--------- .../register/RegistrationScreenAction.kt | 10 ++ .../register/RegistrationScreenViewModel.kt | 51 +++++--- .../domain/model/AccountManagerResult.kt | 8 ++ .../domain/repository/AccountManager.kt | 42 +++++++ .../debtpirate/theme/Theme.kt | 2 - app/gradle/libs.versions.toml | 3 + 12 files changed, 209 insertions(+), 87 deletions(-) create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/model/AccountManagerResult.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 4c19b97..44f3719 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -62,7 +62,7 @@ android { ) } debug { - applicationIdSuffix = ".d" + applicationIdSuffix = ".dev" buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:42069\"") } } @@ -110,6 +110,8 @@ dependencies { kapt(libs.hilt.kapt) implementation(libs.hilt.compose) implementation(libs.google.fonts) + implementation(libs.androidx.credentials.core) + implementation(libs.androidx.credentials.compat) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt index fcb286d..d96013c 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt @@ -2,6 +2,7 @@ package ing.bikeshedengineer.debtpirate.app.host import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedContentTransitionScope diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt index 55bca63..6c20715 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreen.kt @@ -1,6 +1,7 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login import android.annotation.SuppressLint +import androidx.activity.ComponentActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -28,10 +29,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -40,13 +41,18 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import ing.bikeshedengineer.debtpirate.R +import ing.bikeshedengineer.debtpirate.domain.repository.AccountManager @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun LoginScreen( viewModel: LoginScreenViewModel = hiltViewModel() ) { + val context = LocalContext.current as ComponentActivity + val accountManager = remember { + AccountManager(context) + } + Scaffold( modifier = Modifier.fillMaxSize() ) { @@ -67,13 +73,32 @@ fun LoginScreen( LoginComponent( emailAddress = emailAddress, - onUpdateEmailAddress = viewModel::updateEmailAddress, + onUpdateEmailAddress = { + viewModel.updateState( + LoginScreenStateAction.UpdateEmailAddress( + it + ) + ) + }, password = password, - onUpdatePassword = viewModel::updatePassword, + onUpdatePassword = { + viewModel.updateState( + LoginScreenStateAction.UpdatePassword( + it + ) + ) + }, submitLoginRequest = viewModel::submitLoginRequest ) - Separator(modifier = Modifier.padding(PaddingValues(top = 24.dp, bottom = 24.dp))) + Separator( + modifier = Modifier.padding( + PaddingValues( + top = 24.dp, + bottom = 24.dp + ) + ) + ) RegisterButton(viewModel = viewModel) } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt new file mode 100644 index 0000000..88faa43 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenStateAction.kt @@ -0,0 +1,6 @@ +package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login + +sealed interface LoginScreenStateAction { + data class UpdateEmailAddress(val emailAddress: String) : LoginScreenStateAction + data class UpdatePassword(val password: String) : LoginScreenStateAction +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt index 63dc0a3..2377853 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt @@ -49,12 +49,16 @@ class LoginScreenViewModel @Inject constructor( private val _password = MutableStateFlow("") val password = _password.asStateFlow() - fun updateEmailAddress(emailAddress: String) { - _emailAddress.value = emailAddress - } + fun updateState(action: LoginScreenStateAction) { + when (action) { + is LoginScreenStateAction.UpdateEmailAddress -> { + _emailAddress.value = action.emailAddress + } - fun updatePassword(password: String) { - _password.value = password + is LoginScreenStateAction.UpdatePassword -> { + _password.value = action.password + } + } } fun submitLoginRequest() { diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt index 0762b07..bb783ee 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreen.kt @@ -1,6 +1,8 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register import android.annotation.SuppressLint +import android.util.Log +import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -26,12 +28,15 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -45,14 +50,41 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import ing.bikeshedengineer.debtpirate.R +import ing.bikeshedengineer.debtpirate.domain.model.AccountManagerResult +import ing.bikeshedengineer.debtpirate.domain.repository.AccountManager +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable() -fun RegistrationScreen(viewModel: RegistrationScreenViewModel = hiltViewModel()) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) +fun RegistrationScreen( + viewModel: RegistrationScreenViewModel = hiltViewModel() +) { + val context = LocalContext.current as ComponentActivity + val accountManager = remember { AccountManager(context) } + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(true) { + coroutineScope.launch { + viewModel.onRegistrationComplete.collect { credentials -> + val (emailAddress, password) = credentials + val result = accountManager.storeCredentials(emailAddress, password) + + when (result) { + is AccountManagerResult.Unavailable -> { + viewModel.navigateUp() + } + + else -> { + + } + } + Log.d("RegistrationScreen", "$result") + } + } + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( topBar = { RegistrationTopAppBar(onNavigateUp = viewModel::navigateUp) }, modifier = Modifier @@ -64,40 +96,8 @@ fun RegistrationScreen(viewModel: RegistrationScreenViewModel = hiltViewModel Unit, modifier: Modifier = @Composable private fun RegistrationComponent( - emailAddress: State, - emailAddressError: State, - isInvalidEmailAddress: State, - updateEmailAddress: (String) -> Unit, - name: State, - nameError: State, - isInvalidName: State, - updateName: (String) -> Unit, - password: State, - passwordError: State, - isInvalidPassword: State, - updatePassword: (String) -> Unit, - confirmPassword: State, - confirmPasswordError: State, - isInvalidConfirmPassword: State, - updateConfirmPassword: (String) -> Unit, - registerNewAccount: (String, String, String, String) -> Unit, + viewModel: RegistrationScreenViewModel, modifier: Modifier = Modifier ) { + val emailAddress = viewModel.emailAddress.collectAsState() + val emailAddressError = viewModel.emailAddressError.collectAsState() + val isInvalidEmailAddress = viewModel.isInvalidEmailAddress.collectAsState(false) + + val name = viewModel.name.collectAsState() + val nameError = viewModel.nameError.collectAsState() + val isInvalidName = viewModel.isInvalidName.collectAsState(false) + + val password = viewModel.password.collectAsState() + val passwordError = viewModel.passwordError.collectAsState() + val isInvalidPassword = viewModel.isInvalidPassword.collectAsState(false) + + val confirmPassword = viewModel.confirmPassword.collectAsState() + val confirmPasswordError = viewModel.confirmPasswordError.collectAsState() + val isInvalidConfirmPassword = viewModel.isInvalidConfirmPassword.collectAsState(false) + Column(modifier = modifier.verticalScroll(rememberScrollState())) { OutlinedTextField( value = emailAddress.value, @@ -167,7 +167,7 @@ private fun RegistrationComponent( keyboardType = KeyboardType.Email, imeAction = ImeAction.Next ), - onValueChange = updateEmailAddress, + onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateEmailAddress(it)) }, modifier = Modifier.fillMaxWidth() ) @@ -184,7 +184,7 @@ private fun RegistrationComponent( keyboardType = KeyboardType.Text, imeAction = ImeAction.Next ), - onValueChange = updateName, + onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateName(it)) }, modifier = Modifier .fillMaxWidth() .padding(PaddingValues(top = 4.dp)) @@ -209,7 +209,7 @@ private fun RegistrationComponent( keyboardType = KeyboardType.Password, imeAction = ImeAction.Next ), - onValueChange = updatePassword, + onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdatePassword(it)) }, modifier = Modifier .fillMaxWidth() .padding(PaddingValues(top = 4.dp)) @@ -234,7 +234,7 @@ private fun RegistrationComponent( keyboardType = KeyboardType.Password, imeAction = ImeAction.Done ), - onValueChange = updateConfirmPassword, + onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateConfirmPassword(it)) }, modifier = Modifier .fillMaxWidth() .padding(PaddingValues(top = 4.dp)) @@ -268,7 +268,7 @@ private fun RegistrationComponent( name, password, confirmPassword, - registerNewAccount + registerNewAccount = viewModel::registerNewAccount ) } } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt new file mode 100644 index 0000000..3b3b67f --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenAction.kt @@ -0,0 +1,10 @@ +package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register + +sealed interface RegistrationScreenAction { + data class UpdateEmailAddress(val emailAddress: String) : RegistrationScreenAction + data class UpdateName(val name: String) : RegistrationScreenAction + data class UpdatePassword(val password: String) : RegistrationScreenAction + data class UpdateConfirmPassword(val confirmPassword: String) : RegistrationScreenAction + data object ResetFields : RegistrationScreenAction + data class RegisterNewUser(val emailAddress: String, val name: String, val password: String, val confirmPassword: String) : RegistrationScreenAction +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt index f02c91e..ee2f805 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt @@ -6,9 +6,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.NewAccountRegistrationValidationResult import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitAccountRegistrationRequestUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateNewAccountRegistrationUseCase -import ing.bikeshedengineer.debtpirate.navigation.Destination import ing.bikeshedengineer.debtpirate.navigation.Navigator +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ import javax.inject.Inject class RegistrationScreenViewModel @Inject constructor( private val navigator: Navigator, private val validateNewAccount: ValidateNewAccountRegistrationUseCase, - private val submitAccountRegistrationRequest: SubmitAccountRegistrationRequestUseCase + private val submitAccountRegistrationRequest: SubmitAccountRegistrationRequestUseCase, ) : ViewModel() { fun navigateUp() { viewModelScope.launch { @@ -54,20 +55,40 @@ class RegistrationScreenViewModel @Inject constructor( val confirmPasswordError = _confirmPasswordError.asStateFlow() val isInvalidConfirmPassword = _confirmPasswordError.asStateFlow().map { it.isNotEmpty() } - fun updateEmailAddress(emailAddress: String) { - _emailAddress.value = emailAddress - } + private val _onRegistrationComplete = MutableSharedFlow>() + val onRegistrationComplete = _onRegistrationComplete.asSharedFlow() - fun updateName(name: String) { - _name.value = name - } + fun onAction(action: RegistrationScreenAction) { + when (action) { + is RegistrationScreenAction.UpdateEmailAddress -> { + _emailAddress.value = action.emailAddress + } - fun updatePassword(password: String) { - _password.value = password - } + is RegistrationScreenAction.UpdateName -> { + _name.value = action.name + } - fun updateConfirmPassword(confirmPassword: String) { - _confirmPassword.value = confirmPassword + is RegistrationScreenAction.UpdatePassword -> { + _password.value = action.password + } + + is RegistrationScreenAction.UpdateConfirmPassword -> { + _confirmPassword.value = action.confirmPassword + } + + is RegistrationScreenAction.ResetFields -> { + resetFields() + } + + is RegistrationScreenAction.RegisterNewUser -> { + registerNewAccount( + action.emailAddress, + action.name, + action.password, + action.confirmPassword + ) + } + } } fun registerNewAccount( @@ -81,10 +102,12 @@ class RegistrationScreenViewModel @Inject constructor( if (fieldsAreValid) { viewModelScope.launch { try { + // TODO: Store the registration result data in the store val result = submitAccountRegistrationRequest(emailAddress, name, confirmPassword) - navigator.navigate(Destination.LoginScreen) + _onRegistrationComplete.emit(Pair(emailAddress, confirmPassword)) + resetFields() } catch (err: Throwable) { // TODO... diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/model/AccountManagerResult.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/model/AccountManagerResult.kt new file mode 100644 index 0000000..52279b8 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/model/AccountManagerResult.kt @@ -0,0 +1,8 @@ +package ing.bikeshedengineer.debtpirate.domain.model + +sealed interface AccountManagerResult { + data class Success(val username: String) : AccountManagerResult + data object Unavailable : AccountManagerResult + data object Canceled : AccountManagerResult + data object Failure : AccountManagerResult +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt new file mode 100644 index 0000000..56bc7d9 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/AccountManager.kt @@ -0,0 +1,42 @@ +package ing.bikeshedengineer.debtpirate.domain.repository + +import android.app.Activity +import android.util.Log +import androidx.credentials.CreatePasswordRequest +import androidx.credentials.CredentialManager +import androidx.credentials.exceptions.CreateCredentialCancellationException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException +import ing.bikeshedengineer.debtpirate.domain.model.AccountManagerResult + +class AccountManager(private val context: Activity) { + private val credentialManager = CredentialManager.create(context) + + suspend fun storeCredentials( + emailAddress: String, + password: String + ): AccountManagerResult { + return try { + credentialManager.createCredential( + context, request = CreatePasswordRequest( + id = emailAddress, + password + ) + ) + + AccountManagerResult.Success(emailAddress) + } catch (err: CreateCredentialNoCreateOptionException) { + Log.w( + "DebtPirate::AccountManager", + "Cannot store credentials; a Google account isn't associated with this device" + ) + AccountManagerResult.Unavailable + } catch (err: CreateCredentialCancellationException) { + err.printStackTrace() + AccountManagerResult.Canceled + } catch (err: CreateCredentialException) { + err.printStackTrace() + AccountManagerResult.Failure + } + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/theme/Theme.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/theme/Theme.kt index 4a47d86..f4fd7b6 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/theme/Theme.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/theme/Theme.kt @@ -44,8 +44,6 @@ private val darkScheme = darkColorScheme( @Composable fun DebtPirateTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - dynamicColor: Boolean = true, content: @Composable() () -> Unit ) { val colorScheme = darkScheme diff --git a/app/gradle/libs.versions.toml b/app/gradle/libs.versions.toml index d98ea1d..d0f7bf2 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -25,12 +25,15 @@ okhttp = "4.10.0" retrofit = "2.9.0" hiltNavigationCompose = "1.2.0" fonts = "1.7.5" +credentialManager = "1.5.0-beta01" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-credentials-core = { group = "androidx.credentials", name = "credentials", version.ref = "credentialManager" } +androidx-credentials-compat = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialManager" } androidx-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }