Start reworking credentials manager into its own dependency module

This commit is contained in:
Z. Charles Dziura 2025-03-21 21:16:52 -04:00
parent 42d2705a84
commit 582e7015a9
10 changed files with 84 additions and 19 deletions

View file

@ -33,6 +33,7 @@ import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.Reg
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreenViewModel import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreenViewModel
import ing.bikeshedengineer.debtpirate.app.screen.home.presentation.overview.OverviewScreen import ing.bikeshedengineer.debtpirate.app.screen.home.presentation.overview.OverviewScreen
import ing.bikeshedengineer.debtpirate.domain.usecase.GetStoredTokensUseCase import ing.bikeshedengineer.debtpirate.domain.usecase.GetStoredTokensUseCase
import ing.bikeshedengineer.debtpirate.domain.usecase.StoreCredentialsUseCase
import ing.bikeshedengineer.debtpirate.navigation.Destination import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.NavigationAction import ing.bikeshedengineer.debtpirate.navigation.NavigationAction
import ing.bikeshedengineer.debtpirate.navigation.Navigator import ing.bikeshedengineer.debtpirate.navigation.Navigator
@ -51,6 +52,9 @@ class MainActivity : ComponentActivity() {
@Inject @Inject
lateinit var getStoredTokens: GetStoredTokensUseCase lateinit var getStoredTokens: GetStoredTokensUseCase
@Inject
lateinit var storeCredentials: StoreCredentialsUseCase
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -59,6 +63,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val navController = rememberNavController() val navController = rememberNavController()
ObserveAsEvents(navigator.navigationActions) { action -> ObserveAsEvents(navigator.navigationActions) { action ->
when (action) { when (action) {
@ -102,16 +107,18 @@ class MainActivity : ComponentActivity() {
) { ) {
composable<Destination.AuthLogin> { composable<Destination.AuthLogin> {
val viewModel = hiltViewModel<LoginScreenViewModel>() val viewModel = hiltViewModel<LoginScreenViewModel>()
val toastMessages = viewModel.toastMessages.collectAsState("")
val storeCredentialMessages = viewModel.storeCredentialsMessages.collectAsState(null);
LoginScreen( LoginScreen(
emailAddress = viewModel.emailAddress.collectAsState(""), emailAddress = viewModel.emailAddress.collectAsState(""),
isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true), isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true),
password = viewModel.password.collectAsState(""), password = viewModel.password.collectAsState(""),
isPasswordValid = viewModel.isPasswordValid.collectAsState(true), isPasswordValid = viewModel.isPasswordValid.collectAsState(true),
toastMessages = viewModel.toastMessages.collectAsState(""), toastMessages = toastMessages,
onAction = viewModel::onAction,
handleCredentialManagerSignIn = viewModel::handleCredentialManagerSignIn, handleCredentialManagerSignIn = viewModel::handleCredentialManagerSignIn,
onRegisterButtonClick = viewModel::onRegisterButtonClick onRegisterButtonClick = viewModel::onRegisterButtonClick,
onAction = viewModel::onAction,
) )
} }
composable<Destination.AuthRegistration> { composable<Destination.AuthRegistration> {

View file

@ -46,7 +46,6 @@ import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption import androidx.credentials.GetPasswordOption
import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.NoCredentialException import androidx.credentials.exceptions.NoCredentialException
import androidx.hilt.navigation.compose.hiltViewModel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
@ -56,9 +55,9 @@ fun LoginScreen(
password: State<String>, password: State<String>,
isPasswordValid: State<Boolean>, isPasswordValid: State<Boolean>,
toastMessages: State<String>, toastMessages: State<String>,
onAction: (LoginScreenStateAction) -> Unit,
handleCredentialManagerSignIn: (GetCredentialResponse) -> Unit, handleCredentialManagerSignIn: (GetCredentialResponse) -> Unit,
onRegisterButtonClick: () -> Unit, onRegisterButtonClick: () -> Unit,
onAction: (LoginScreenStateAction) -> Unit,
) { ) {
val context = LocalActivity.current!! val context = LocalActivity.current!!
val credentialManager = CredentialManager.create(context) val credentialManager = CredentialManager.create(context)

View file

@ -0,0 +1,6 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
sealed interface LoginScreenMessage {
data class Toast(val message: String) : LoginScreenMessage
data class StoreCredentials(val username: String, val password: String) : LoginScreenMessage
}

View file

@ -17,8 +17,8 @@ import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -51,6 +51,22 @@ class LoginScreenViewModel @Inject constructor(
val isEmailAddressValid = _state.map { it.isEmailAddressValid } val isEmailAddressValid = _state.map { it.isEmailAddressValid }
val isPasswordValid = _state.map { it.isPasswordValid } val isPasswordValid = _state.map { it.isPasswordValid }
private val _messages = MutableSharedFlow<LoginScreenMessage>()
val toastMessages = _messages
.filter { message ->
message is LoginScreenMessage.Toast
}
.map { message -> (message as LoginScreenMessage.Toast).message }
val storeCredentialsMessages = _messages
.filter { message ->
message is LoginScreenMessage.StoreCredentials
}
.map { message ->
val credentialsMessage = (message as LoginScreenMessage.StoreCredentials)
Pair(credentialsMessage.username, credentialsMessage.password)
}
init { init {
_state.distinctUntilChangedBy { it.emailAddress } _state.distinctUntilChangedBy { it.emailAddress }
.map { it.emailAddress.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(it.emailAddress).matches() } .map { it.emailAddress.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(it.emailAddress).matches() }
@ -77,9 +93,6 @@ class LoginScreenViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
private val _toastMessages = MutableSharedFlow<String>()
val toastMessages = _toastMessages.asSharedFlow()
fun onAction(action: LoginScreenStateAction) { fun onAction(action: LoginScreenStateAction) {
when (action) { when (action) {
is LoginScreenStateAction.UpdateEmailAddress -> { is LoginScreenStateAction.UpdateEmailAddress -> {
@ -96,7 +109,7 @@ class LoginScreenViewModel @Inject constructor(
is LoginScreenStateAction.SubmitLoginRequest -> { is LoginScreenStateAction.SubmitLoginRequest -> {
viewModelScope.launch { viewModelScope.launch {
onSubmitLoginRequest(_state.value.emailAddress, _state.value.password) onSubmitLoginRequest()
} }
} }
} }
@ -118,9 +131,9 @@ class LoginScreenViewModel @Inject constructor(
} }
private suspend fun onSubmitLoginRequest(emailAddress: String, password: String) { private suspend fun onSubmitLoginRequest() {
try { try {
val (userId, auth, session) = submitLoginCredentials(emailAddress, password) val (userId, auth, session) = submitLoginCredentials(_state.value.emailAddress, _state.value.password)
updateStoreData( updateStoreData(
userId = userId, userId = userId,
authToken = auth.token, authToken = auth.token,
@ -131,15 +144,15 @@ class LoginScreenViewModel @Inject constructor(
} catch (err: Exception) { } catch (err: Exception) {
when (err) { when (err) {
is InvalidCredentialsException -> { is InvalidCredentialsException -> {
_toastMessages.emit("Invalid Email Address or Password") _messages.emit(LoginScreenMessage.Toast("Invalid Email Address or Password"))
} }
is UserNotFoundException -> { is UserNotFoundException -> {
_toastMessages.emit("User Not Found") _messages.emit(LoginScreenMessage.Toast("User Not Found"))
} }
else -> { else -> {
_toastMessages.emit("Cannot Login, Please Try Again Later") _messages.emit(LoginScreenMessage.Toast("Cannot Login, Please Try Again Later"))
} }
} }
} }

View file

@ -0,0 +1,19 @@
package ing.bikeshedengineer.debtpirate.di
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import ing.bikeshedengineer.debtpirate.domain.usecase.StoreCredentialsUseCase
@Module
@InstallIn(ActivityComponent::class)
object AppCredentialsModule {
@Provides
@ActivityScoped
fun provideStoreCredentialsUseCase(@ActivityContext context: Context) = StoreCredentialsUseCase(context)
}

View file

@ -4,7 +4,6 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator import ing.bikeshedengineer.debtpirate.navigation.Navigator
import ing.bikeshedengineer.debtpirate.navigation.NavigatorImpl import ing.bikeshedengineer.debtpirate.navigation.NavigatorImpl
import javax.inject.Singleton import javax.inject.Singleton
@ -15,5 +14,5 @@ object NavigatorModule {
@Provides @Provides
@Singleton @Singleton
fun provideNavigator(): Navigator = NavigatorImpl(startDestination = Destination.AuthGraph) fun provideNavigator(): Navigator = NavigatorImpl()
} }

View file

@ -12,7 +12,7 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object UseCaseModule { object SingletonUseCaseModule {
@Provides @Provides
@Singleton @Singleton

View file

@ -0,0 +1,21 @@
package ing.bikeshedengineer.debtpirate.domain.usecase
import android.content.Context
import android.util.Log
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.CreateCredentialException
class StoreCredentialsUseCase(val context: Context) {
suspend operator fun invoke(username: String, password: String) {
val credentialManager = CredentialManager.create(this.context)
val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
try {
credentialManager.createCredential(this.context, createPasswordRequest)
Log.d("StoreCredentialsUseCase", "Successfully stored login credentials")
} catch (err: CreateCredentialException) {
// TODO: Throw an error to be displayed as a Toast
}
}
}

View file

@ -4,6 +4,7 @@ import androidx.navigation.NavOptionsBuilder
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Inject
interface Navigator { interface Navigator {
val startDestination: Destination val startDestination: Destination
@ -15,7 +16,7 @@ interface Navigator {
} }
class NavigatorImpl( class NavigatorImpl(
override val startDestination: Destination, override val startDestination: Destination = Destination.AuthGraph
) : Navigator { ) : Navigator {
private val _navigationActions = Channel<NavigationAction>() private val _navigationActions = Channel<NavigationAction>()
override val navigationActions = _navigationActions.receiveAsFlow() override val navigationActions = _navigationActions.receiveAsFlow()