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

View file

@ -46,7 +46,6 @@ import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.NoCredentialException
import androidx.hilt.navigation.compose.hiltViewModel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
@ -56,9 +55,9 @@ fun LoginScreen(
password: State<String>,
isPasswordValid: State<Boolean>,
toastMessages: State<String>,
onAction: (LoginScreenStateAction) -> Unit,
handleCredentialManagerSignIn: (GetCredentialResponse) -> Unit,
onRegisterButtonClick: () -> Unit,
onAction: (LoginScreenStateAction) -> Unit,
) {
val context = LocalActivity.current!!
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 kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -51,6 +51,22 @@ class LoginScreenViewModel @Inject constructor(
val isEmailAddressValid = _state.map { it.isEmailAddressValid }
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 {
_state.distinctUntilChangedBy { it.emailAddress }
.map { it.emailAddress.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(it.emailAddress).matches() }
@ -77,9 +93,6 @@ class LoginScreenViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private val _toastMessages = MutableSharedFlow<String>()
val toastMessages = _toastMessages.asSharedFlow()
fun onAction(action: LoginScreenStateAction) {
when (action) {
is LoginScreenStateAction.UpdateEmailAddress -> {
@ -96,7 +109,7 @@ class LoginScreenViewModel @Inject constructor(
is LoginScreenStateAction.SubmitLoginRequest -> {
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 {
val (userId, auth, session) = submitLoginCredentials(emailAddress, password)
val (userId, auth, session) = submitLoginCredentials(_state.value.emailAddress, _state.value.password)
updateStoreData(
userId = userId,
authToken = auth.token,
@ -131,15 +144,15 @@ class LoginScreenViewModel @Inject constructor(
} catch (err: Exception) {
when (err) {
is InvalidCredentialsException -> {
_toastMessages.emit("Invalid Email Address or Password")
_messages.emit(LoginScreenMessage.Toast("Invalid Email Address or Password"))
}
is UserNotFoundException -> {
_toastMessages.emit("User Not Found")
_messages.emit(LoginScreenMessage.Toast("User Not Found"))
}
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.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator
import ing.bikeshedengineer.debtpirate.navigation.NavigatorImpl
import javax.inject.Singleton
@ -15,5 +14,5 @@ object NavigatorModule {
@Provides
@Singleton
fun provideNavigator(): Navigator = NavigatorImpl(startDestination = Destination.AuthGraph)
fun provideNavigator(): Navigator = NavigatorImpl()
}

View file

@ -12,7 +12,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object UseCaseModule {
object SingletonUseCaseModule {
@Provides
@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.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Inject
interface Navigator {
val startDestination: Destination
@ -15,7 +16,7 @@ interface Navigator {
}
class NavigatorImpl(
override val startDestination: Destination,
override val startDestination: Destination = Destination.AuthGraph
) : Navigator {
private val _navigationActions = Channel<NavigationAction>()
override val navigationActions = _navigationActions.receiveAsFlow()