From 582e7015a907afb9343e12102953da821dfacd82 Mon Sep 17 00:00:00 2001 From: "Z. Charles Dziura" Date: Fri, 21 Mar 2025 21:16:52 -0400 Subject: [PATCH] Start reworking credentials manager into its own dependency module --- ...otlin-compiler-14485427041908763179.salive | 0 .../debtpirate/app/host/MainActivity.kt | 13 ++++++-- .../auth/presentation/login/LoginScreen.kt | 3 +- .../presentation/login/LoginScreenMessage.kt | 6 ++++ .../login/LoginScreenViewModel.kt | 33 +++++++++++++------ .../debtpirate/di/AppCredentialsModule.kt | 19 +++++++++++ .../debtpirate/di/NavigatorModule.kt | 3 +- .../debtpirate/di/UseCaseModule.kt | 2 +- .../domain/usecase/StoreCredentialsUseCase.kt | 21 ++++++++++++ .../debtpirate/navigation/Navigator.kt | 3 +- 10 files changed, 84 insertions(+), 19 deletions(-) delete mode 100644 app/.kotlin/sessions/kotlin-compiler-14485427041908763179.salive create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenMessage.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/AppCredentialsModule.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/usecase/StoreCredentialsUseCase.kt diff --git a/app/.kotlin/sessions/kotlin-compiler-14485427041908763179.salive b/app/.kotlin/sessions/kotlin-compiler-14485427041908763179.salive deleted file mode 100644 index e69de29..0000000 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 721dd85..5ca42bb 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 @@ -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 { val viewModel = hiltViewModel() + 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 { 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 07f1537..1ae01d8 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 @@ -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, isPasswordValid: State, toastMessages: State, - onAction: (LoginScreenStateAction) -> Unit, handleCredentialManagerSignIn: (GetCredentialResponse) -> Unit, onRegisterButtonClick: () -> Unit, + onAction: (LoginScreenStateAction) -> Unit, ) { val context = LocalActivity.current!! val credentialManager = CredentialManager.create(context) diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenMessage.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenMessage.kt new file mode 100644 index 0000000..afd4dc6 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenMessage.kt @@ -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 +} \ 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 7266107..353811d 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 @@ -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() + 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() - 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")) } } } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/AppCredentialsModule.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/AppCredentialsModule.kt new file mode 100644 index 0000000..bfa522a --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/AppCredentialsModule.kt @@ -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) +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/NavigatorModule.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/NavigatorModule.kt index 68a18c1..e028996 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/NavigatorModule.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/NavigatorModule.kt @@ -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() } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/UseCaseModule.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/UseCaseModule.kt index fd2b582..98f1929 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/UseCaseModule.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/di/UseCaseModule.kt @@ -12,7 +12,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object UseCaseModule { +object SingletonUseCaseModule { @Provides @Singleton diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/usecase/StoreCredentialsUseCase.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/usecase/StoreCredentialsUseCase.kt new file mode 100644 index 0000000..0373bb0 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/usecase/StoreCredentialsUseCase.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Navigator.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Navigator.kt index cb67f20..da31339 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Navigator.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Navigator.kt @@ -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() override val navigationActions = _navigationActions.receiveAsFlow()