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 e24adc5..68dc103 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 @@ -32,11 +32,13 @@ import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginS import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreen 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.credential.GetCredentialsUseCase +import ing.bikeshedengineer.debtpirate.domain.credential.StoreCredentialsUseCase +import ing.bikeshedengineer.debtpirate.domain.credential.UserCredentialManager import ing.bikeshedengineer.debtpirate.domain.navigation.Destination import ing.bikeshedengineer.debtpirate.domain.navigation.NavigationAction import ing.bikeshedengineer.debtpirate.domain.navigation.Navigator import ing.bikeshedengineer.debtpirate.domain.usecase.GetStoredTokensUseCase -import ing.bikeshedengineer.debtpirate.domain.usecase.StoreCredentialsUseCase import ing.bikeshedengineer.debtpirate.theme.DebtPirateTheme import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -52,6 +54,12 @@ class MainActivity : ComponentActivity() { @Inject lateinit var getStoredTokens: GetStoredTokensUseCase + @Inject + lateinit var userCredentialManager: UserCredentialManager + + @Inject + lateinit var getCredentials: GetCredentialsUseCase + @Inject lateinit var storeCredentials: StoreCredentialsUseCase @@ -64,8 +72,21 @@ class MainActivity : ComponentActivity() { setContent { val navController = rememberNavController() + ObserveAsEvents(userCredentialManager.requestUserCredentials) { + lifecycleScope.launch { + val result = getCredentials() + userCredentialManager.returnUserCredentialsResponse(result) + } + } - ObserveAsEvents(navigator.navigationActions) { action -> + ObserveAsEvents(userCredentialManager.storeUserCredentials) { credentials -> + val (emailAddress, password) = credentials + lifecycleScope.launch { + storeCredentials(emailAddress, password) + } + } + + ObserveAsEvents(navigator.actions) { action -> when (action) { is NavigationAction.Navigate -> { navController.navigate(action.destination) { @@ -108,15 +129,17 @@ class MainActivity : ComponentActivity() { composable { val viewModel = hiltViewModel() val toastMessages = viewModel.toastMessages.collectAsState("") - val storeCredentialMessages = viewModel.storeCredentialsMessages.collectAsState(null); + val storeCredentialMessages = + viewModel.storeCredentialsMessages.collectAsState(null) LoginScreen( emailAddress = viewModel.emailAddress.collectAsState(""), - isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true), + isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState( + true + ), password = viewModel.password.collectAsState(""), isPasswordValid = viewModel.isPasswordValid.collectAsState(true), toastMessages = toastMessages, - handleCredentialManagerSignIn = viewModel::handleCredentialManagerSignIn, onRegisterButtonClick = viewModel::onRegisterButtonClick, onAction = viewModel::onAction, ) @@ -127,7 +150,9 @@ class MainActivity : ComponentActivity() { RegistrationScreen( emailAddress = viewModel.emailAddress.collectAsState(""), emailAddressError = viewModel.emailAddressError.collectAsState(""), - isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true), + isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState( + true + ), name = viewModel.name.collectAsState(""), nameError = viewModel.nameError.collectAsState(""), isNameValid = viewModel.isNameValid.collectAsState(true), @@ -135,10 +160,16 @@ class MainActivity : ComponentActivity() { passwordError = viewModel.passwordError.collectAsState(""), isPasswordValid = viewModel.isPasswordValid.collectAsState(true), confirmPassword = viewModel.confirmPassword.collectAsState(""), - confirmPasswordError = viewModel.confirmPasswordError.collectAsState(""), - isConfirmPasswordValid = viewModel.isConfirmPasswordValid.collectAsState(true), + confirmPasswordError = viewModel.confirmPasswordError.collectAsState( + "" + ), + isConfirmPasswordValid = viewModel.isConfirmPasswordValid.collectAsState( + true + ), onAction = viewModel::onAction, - onRegistrationMessage = viewModel.onRegistrationComplete.collectAsState(null), + onRegistrationMessage = viewModel.onRegistrationComplete.collectAsState( + null + ), navigateToConfirmationScreen = viewModel::navigateToConfirmationScreen, onNavigateUp = viewModel::navigateUp ) @@ -151,7 +182,12 @@ class MainActivity : ComponentActivity() { } composable { val (userId, verificationToken) = it.toRoute() - ConfirmationScreen(ConfirmationData.UserConfirmationData(userId, verificationToken)) + ConfirmationScreen( + ConfirmationData.UserConfirmationData( + userId, + verificationToken + ) + ) } } 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 1ae01d8..2dd41a8 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,7 +1,6 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login import android.annotation.SuppressLint -import android.util.Log import android.widget.Toast import androidx.activity.compose.LocalActivity import androidx.compose.foundation.background @@ -40,12 +39,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.credentials.CredentialManager -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import androidx.credentials.GetPasswordOption -import androidx.credentials.exceptions.GetCredentialException -import androidx.credentials.exceptions.NoCredentialException @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable @@ -55,35 +48,10 @@ fun LoginScreen( password: State, isPasswordValid: State, toastMessages: State, - handleCredentialManagerSignIn: (GetCredentialResponse) -> Unit, onRegisterButtonClick: () -> Unit, onAction: (LoginScreenStateAction) -> Unit, ) { val context = LocalActivity.current!! - val credentialManager = CredentialManager.create(context) - LaunchedEffect(Unit) { - try { - val result = credentialManager.getCredential( - context, GetCredentialRequest( - listOf(GetPasswordOption()) - ) - ) - - handleCredentialManagerSignIn(result) - } catch (err: GetCredentialException) { - when (err) { - is NoCredentialException -> { - Log.i("LoginScreen", "No credentials stored") - } - - else -> { - Log.e("LoginScreen", "Exception thrown when getting credentials: $err") - - } - } - } - } - LaunchedEffect(toastMessages.value) { val message = toastMessages.value 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 index c160618..820b19f 100644 --- 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 @@ -5,4 +5,8 @@ sealed interface LoginScreenStateAction { data class UpdatePassword(val password: String) : LoginScreenStateAction data object ValidateCredentials : LoginScreenStateAction data object SubmitLoginRequest : LoginScreenStateAction + data class SubmitLoginRequestWithStoredCredentials( + val emailAddress: String, + 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 cf1b092..9a24e61 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 @@ -2,8 +2,6 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login import android.util.Log import android.util.Patterns -import androidx.credentials.GetCredentialResponse -import androidx.credentials.PasswordCredential import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,11 +10,14 @@ import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.LoginCredentialsV import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.UserNotFoundException import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateLoginCredentialsUseCase +import ing.bikeshedengineer.debtpirate.domain.credential.UserCredentialManager +import ing.bikeshedengineer.debtpirate.domain.credential.UserCredentialResponse import ing.bikeshedengineer.debtpirate.domain.navigation.Destination import ing.bikeshedengineer.debtpirate.domain.navigation.Navigator import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn @@ -29,6 +30,7 @@ import javax.inject.Inject @HiltViewModel class LoginScreenViewModel @Inject constructor( private val navigator: Navigator, + private val userCredentialManager: UserCredentialManager, private val submitLoginCredentials: SubmitLoginCredentialsUseCase, private val validateLoginCredentials: ValidateLoginCredentialsUseCase, private val updateStoreData: UpdateStoreDataUseCase @@ -68,8 +70,32 @@ class LoginScreenViewModel @Inject constructor( } init { + viewModelScope.launch { + userCredentialManager.makeUserCredentialsRequest() + } + + userCredentialManager.userCredentialsResponse.distinctUntilChanged() + .onEach { response -> + when (response) { + is UserCredentialResponse.HasCredentials -> { + val (emailAddress, password) = response + onAction( + LoginScreenStateAction.SubmitLoginRequestWithStoredCredentials( + emailAddress, + password + ) + ) + } + + is UserCredentialResponse.NoCredentials -> {} + } + } + _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() + } .onEach { isEmailAddressValid -> _state.update { it.copy( @@ -112,14 +138,17 @@ class LoginScreenViewModel @Inject constructor( onSubmitLoginRequest() } } + + is LoginScreenStateAction.SubmitLoginRequestWithStoredCredentials -> { + onValidateLoginCredentials(action.emailAddress, action.password) + } } } private fun onValidateLoginCredentials(emailAddress: String, password: String) { _state.update { it.copy(isEmailAddressPristine = true, isPasswordPristine = true) } - val validationResult = validateLoginCredentials(emailAddress, password) - when (validationResult) { + when (val validationResult = validateLoginCredentials(emailAddress, password)) { is LoginCredentialsValidationResult.ValidCredentials -> { onAction(LoginScreenStateAction.SubmitLoginRequest) } @@ -133,7 +162,11 @@ class LoginScreenViewModel @Inject constructor( private suspend fun onSubmitLoginRequest() { try { - val (userId, auth, session) = submitLoginCredentials(_state.value.emailAddress, _state.value.password) + val (userId, auth, session) = submitLoginCredentials( + _state.value.emailAddress, + _state.value.password + ) + updateStoreData( userId = userId, authToken = auth.token, @@ -141,6 +174,11 @@ class LoginScreenViewModel @Inject constructor( sessionToken = session.token, sessionTokenExpiresAt = session.expiresAt ) + + userCredentialManager.storeUserCredentials( + _state.value.emailAddress, + _state.value.password + ) } catch (err: Exception) { when (err) { is InvalidCredentialsException -> { @@ -163,20 +201,4 @@ class LoginScreenViewModel @Inject constructor( navigator.navigate(destination = Destination.AuthRegistration) } } - - fun handleCredentialManagerSignIn(result: GetCredentialResponse) { - val credentials = result.credential - when (credentials) { - is PasswordCredential -> { - val emailAddress = credentials.id - val password = credentials.password - - onAction(LoginScreenStateAction.SubmitLoginRequest) - } - - else -> { - // TODO: Handle this... - } - } - } } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/CredentialManager.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/CredentialManager.kt deleted file mode 100644 index 3b3c954..0000000 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/CredentialManager.kt +++ /dev/null @@ -1,42 +0,0 @@ -package ing.bikeshedengineer.debtpirate.domain.credential - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.receiveAsFlow -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object CredentialManagerModule { - - @Provides - @Singleton - fun provideCredentialManager(): CredentialManager = CredentialManagerImpl() -} - -sealed interface CredentialManagerRequest { - data object GetCredentials : CredentialManagerRequest -} - -sealed interface CredentialManagerResponse { - data object NoCredentials : CredentialManagerResponse -} - -interface CredentialManager { - val credentialRequests: Flow - - suspend fun getCredentials() -} - -class CredentialManagerImpl() : CredentialManager { - private val _credentialRequests = Channel() - override val credentialRequests = _credentialRequests.receiveAsFlow() - - override suspend fun getCredentials() { - this._credentialRequests.send(CredentialManagerRequest.GetCredentials) - } -} diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/GetCredentialsUseCase.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/GetCredentialsUseCase.kt new file mode 100644 index 0000000..f231745 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/GetCredentialsUseCase.kt @@ -0,0 +1,23 @@ +package ing.bikeshedengineer.debtpirate.domain.credential + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +object GetCredentialsUseCaseModule { + + @Provides + @ActivityScoped + fun provideGetCredentialsUseCase(repository: UserCredentialRepository) = + GetCredentialsUseCase(repository) +} + +class GetCredentialsUseCase(private val repository: UserCredentialRepository) { + suspend operator fun invoke(): UserCredentialResponse { + return repository.getUserCredentials() + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/StoreCredentialsUseCase.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/StoreCredentialsUseCase.kt new file mode 100644 index 0000000..5b9bf5d --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/StoreCredentialsUseCase.kt @@ -0,0 +1,23 @@ +package ing.bikeshedengineer.debtpirate.domain.credential + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +object StoreCredentialsUseCaseModule { + + @Provides + @ActivityScoped + fun provideStoreCredentialsUseCase(repository: UserCredentialRepository) = + StoreCredentialsUseCase(repository) +} + +class StoreCredentialsUseCase(private val repository: UserCredentialRepository) { + suspend operator fun invoke(emailAddress: String, password: String) { + this.repository.storeUserCredentials(emailAddress, password) + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialManager.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialManager.kt new file mode 100644 index 0000000..56dda2a --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialManager.kt @@ -0,0 +1,53 @@ +package ing.bikeshedengineer.debtpirate.domain.credential + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object UserCredentialManagerModule { + + @Provides + @Singleton + fun provideUserCredentialManager(): UserCredentialManager = UserCredentialManagerImpl() +} + +interface UserCredentialManager { + val requestUserCredentials: Flow + suspend fun makeUserCredentialsRequest() + + val userCredentialsResponse: Flow + suspend fun returnUserCredentialsResponse(response: UserCredentialResponse) + + val storeUserCredentials: Flow> + suspend fun storeUserCredentials(emailAddress: String, password: String) +} + +class UserCredentialManagerImpl : UserCredentialManager { + private val _requestUserCredentials = Channel() + override val requestUserCredentials = _requestUserCredentials.receiveAsFlow() + + private val _userCredentialsResponse = Channel() + override val userCredentialsResponse = _userCredentialsResponse.receiveAsFlow() + + private val _storeUserCredentials = Channel>() + override val storeUserCredentials = _storeUserCredentials.receiveAsFlow() + + override suspend fun makeUserCredentialsRequest() { + _requestUserCredentials.send(Unit) + } + + override suspend fun returnUserCredentialsResponse(response: UserCredentialResponse) { + _userCredentialsResponse.send(response) + } + + override suspend fun storeUserCredentials(emailAddress: String, password: String) { + _storeUserCredentials.send(Pair(emailAddress, password)) + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialRepository.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialRepository.kt new file mode 100644 index 0000000..af91a6d --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialRepository.kt @@ -0,0 +1,77 @@ +package ing.bikeshedengineer.debtpirate.domain.credential + +import android.content.Context +import android.util.Log +import androidx.credentials.CreatePasswordRequest +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetPasswordOption +import androidx.credentials.PasswordCredential +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialException +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 kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Module +@InstallIn(ActivityComponent::class) +object UserCredentialModule { + + @Provides + @ActivityScoped + fun provideUserCredentialRepository(@ActivityContext context: Context): UserCredentialRepository = + UserCredentialRepositoryImpl(context = context) +} + +interface UserCredentialRepository { + suspend fun getUserCredentials(): UserCredentialResponse + suspend fun storeUserCredentials(emailAddress: String, password: String) +} + +class UserCredentialRepositoryImpl( + private val context: Context, + private val defaultDispatcher: CoroutineDispatcher = Dispatchers.IO +) : UserCredentialRepository { + private val credentialManager = CredentialManager.create(this.context) + + override suspend fun getUserCredentials(): UserCredentialResponse { + val getCredentialRequest = GetCredentialRequest(listOf(GetPasswordOption())) + return try { + val result = withContext(this.defaultDispatcher) { + credentialManager.getCredential( + context, + getCredentialRequest + ) + } + + val credential = result.credential as PasswordCredential + UserCredentialResponse.HasCredentials( + emailAddress = credential.id, + password = credential.password + ) + } catch (err: GetCredentialException) { + UserCredentialResponse.NoCredentials + } + } + + override suspend fun storeUserCredentials(emailAddress: String, password: String) { + val storeCredentialsRequest = CreatePasswordRequest(id = emailAddress, password = password) + try { + withContext(this.defaultDispatcher) { + credentialManager.createCredential(context, storeCredentialsRequest) + } + } catch (err: CreateCredentialException) { + Log.e( + "UserCredentialRepository", + "Unable to store user credentials in the credential manager: $err" + ) + // TODO: Do something with this error... + } + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialResponse.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialResponse.kt new file mode 100644 index 0000000..e052ea8 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/credential/UserCredentialResponse.kt @@ -0,0 +1,7 @@ +package ing.bikeshedengineer.debtpirate.domain.credential + +sealed interface UserCredentialResponse { + data object NoCredentials : UserCredentialResponse + data class HasCredentials(val emailAddress: String, val password: String) : + UserCredentialResponse +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/navigation/Navigator.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/navigation/Navigator.kt index 9d27103..b357c3a 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/navigation/Navigator.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/navigation/Navigator.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.receiveAsFlow interface Navigator { val startDestination: Destination - val navigationActions: Flow + val actions: Flow suspend fun navigate(destination: Destination, navOptions: NavOptionsBuilder.() -> Unit = {}) @@ -17,17 +17,17 @@ interface Navigator { class NavigatorImpl( override val startDestination: Destination = Destination.AuthGraph ) : Navigator { - private val _navigationActions = Channel() - override val navigationActions = _navigationActions.receiveAsFlow() + private val _actions = Channel() + override val actions = _actions.receiveAsFlow() override suspend fun navigate( destination: Destination, navOptions: NavOptionsBuilder.() -> Unit ) { - _navigationActions.send(NavigationAction.Navigate(destination, navOptions)) + _actions.send(NavigationAction.Navigate(destination, navOptions)) } override suspend fun navigateUp() { - _navigationActions.send(NavigationAction.NavigateUp) + _actions.send(NavigationAction.NavigateUp) } } \ No newline at end of file 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 deleted file mode 100644 index d4d426d..0000000 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/usecase/StoreCredentialsUseCase.kt +++ /dev/null @@ -1,36 +0,0 @@ -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 -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 - -@Module -@InstallIn(ActivityComponent::class) -object StoreCredentialsUseCaseModule { - - @Provides - @ActivityScoped - fun provideStoreCredentialsUseCase(@ActivityContext context: Context) = StoreCredentialsUseCase(context) -} - -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/gradle/libs.versions.toml b/app/gradle/libs.versions.toml index 104f8e4..0b8c4df 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] activityCompose = "1.10.1" -agp = "8.9.0" +agp = "8.9.1" appcompat = "1.7.0" biometric = "1.4.0-alpha02" composeBom = "2025.03.00" diff --git a/bruno/Debt Pirate/account/create.bru b/bruno/Debt Pirate/account/create.bru new file mode 100644 index 0000000..04521ab --- /dev/null +++ b/bruno/Debt Pirate/account/create.bru @@ -0,0 +1,29 @@ +meta { + name: create + type: http + seq: 1 +} + +post { + url: {{protocol}}//{{domain}}/account + body: json + auth: bearer +} + +headers { + Accept: application/json;charset=utf-8 + Content-Type: application/json;charset=utf-8 +} + +auth:bearer { + token: {{sessionToken}} +} + +body:json { + { + "type": "asset", + "name": "Checking", + "description": "Bank 1", + "currencyCode": "USD" + } +} diff --git a/bruno/Debt Pirate/account/read.bru b/bruno/Debt Pirate/account/read.bru new file mode 100644 index 0000000..9b40a84 --- /dev/null +++ b/bruno/Debt Pirate/account/read.bru @@ -0,0 +1,28 @@ +meta { + name: read + type: http + seq: 2 +} + +get { + url: {{protocol}}//{{domain}}/account + body: none + auth: bearer +} + +headers { + Accept: application/json;charset=utf-8 +} + +auth:bearer { + token: {{sessionToken}} +} + +body:json { + { + "type": "asset", + "name": "Quick Cash Account", + "description": "Franklin Savings Bank", + "currencyCode": "USD" + } +} diff --git a/bruno/Debt Pirate/auth/login.bru b/bruno/Debt Pirate/auth/login.bru new file mode 100644 index 0000000..01f250e --- /dev/null +++ b/bruno/Debt Pirate/auth/login.bru @@ -0,0 +1,41 @@ +meta { + name: login + type: http + seq: 1 +} + +post { + url: {{protocol}}//{{domain}}/auth/login + body: json + auth: none +} + +headers { + Accept: application/json; charset=utf-8 + Content-Type: application/json; charset=utf-8 +} + +body:json { + { + "email": "{{email}}", + "password": "{{password}}" + } +} + +script:post-response { + const body = res.getBody(); + const data = body.data; + + if (res.status < 300) { + bru.setEnvVar("userId", data.id); + + if (data.auth?.token) { + bru.setEnvVar("authToken", data.auth.token); + } + + if (data.session?.token) { + bru.setEnvVar("sessionToken", data.session.token); + } + } + +} diff --git a/bruno/Debt Pirate/auth/session.bru b/bruno/Debt Pirate/auth/session.bru new file mode 100644 index 0000000..f1e0b35 --- /dev/null +++ b/bruno/Debt Pirate/auth/session.bru @@ -0,0 +1,33 @@ +meta { + name: session + type: http + seq: 2 +} + +get { + url: {{protocol}}//{{domain}}/auth/session + body: none + auth: bearer +} + +headers { + Accept: application/json; charset=utf-8 +} + +auth:bearer { + token: {{authToken}} +} + +script:post-response { + const body = res.getBody(); + const data = body.data; + + if (res.status < 300) { + bru.setEnvVar("userId", data.id); + + if (data.session?.token) { + bru.setEnvVar("sessionToken", data.token); + } + } + +} diff --git a/bruno/Debt Pirate/bruno.json b/bruno/Debt Pirate/bruno.json new file mode 100644 index 0000000..cbb2a99 --- /dev/null +++ b/bruno/Debt Pirate/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Debt Pirate", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/bruno/Debt Pirate/budget/create.bru b/bruno/Debt Pirate/budget/create.bru new file mode 100644 index 0000000..df2feed --- /dev/null +++ b/bruno/Debt Pirate/budget/create.bru @@ -0,0 +1,28 @@ +meta { + name: create + type: http + seq: 1 +} + +post { + url: {{protocol}}//{{domain}}/budget + body: json + auth: bearer +} + +headers { + Accept: application/json;charset=utf-8 + Content-Type: application/json;charset=utf-8 +} + +auth:bearer { + token: {{sessionToken}} +} + +body:json { + { + "name": "Daily Living", + "description": "Daily living expenses", + "icon": "person" + } +} diff --git a/bruno/Debt Pirate/budget/read.bru b/bruno/Debt Pirate/budget/read.bru new file mode 100644 index 0000000..bd3580b --- /dev/null +++ b/bruno/Debt Pirate/budget/read.bru @@ -0,0 +1,28 @@ +meta { + name: read + type: http + seq: 2 +} + +get { + url: {{protocol}}//{{domain}}/budget + body: none + auth: bearer +} + +headers { + Accept: application/json;charset=utf-8 +} + +auth:bearer { + token: {{sessionToken}} +} + +body:json { + { + "type": "asset", + "name": "Quick Cash Account", + "description": "Franklin Savings Bank", + "currencyCode": "USD" + } +} diff --git a/bruno/Debt Pirate/collection.bru b/bruno/Debt Pirate/collection.bru new file mode 100644 index 0000000..e69de29 diff --git a/bruno/Debt Pirate/environments/Localhost.bru b/bruno/Debt Pirate/environments/Localhost.bru new file mode 100644 index 0000000..7b5c205 --- /dev/null +++ b/bruno/Debt Pirate/environments/Localhost.bru @@ -0,0 +1,11 @@ +vars { + domain: localhost:42069 + protocol: http: + email: zachary@dziura.email +} +vars:secret [ + password, + verificationToken, + sessionToken, + authToken +] diff --git a/bruno/Debt Pirate/user/create.bru b/bruno/Debt Pirate/user/create.bru new file mode 100644 index 0000000..a02762b --- /dev/null +++ b/bruno/Debt Pirate/user/create.bru @@ -0,0 +1,34 @@ +meta { + name: create + type: http + seq: 1 +} + +post { + url: {{protocol}}//{{domain}}/user + body: json + auth: none +} + +headers { + Content-Type: application/json; charset=utf-8 + Accept: application/json; charset=utf-8 +} + +body:json { + { + "email": "{{email}}", + "password": "{{password}}", + "name": "Z. Charles Dziura" + } +} + +script:post-response { + const body = res.getBody(); + const data = body.data; + + if (res.status < 300 && data.sessionToken) { + bru.setEnvVar("verificationToken", data.sessionToken); + } + +} diff --git a/bruno/Debt Pirate/user/verify.bru b/bruno/Debt Pirate/user/verify.bru new file mode 100644 index 0000000..9f7311f --- /dev/null +++ b/bruno/Debt Pirate/user/verify.bru @@ -0,0 +1,42 @@ +meta { + name: verify + type: http + seq: 2 +} + +get { + url: {{protocol}}//{{domain}}/user/verify?t={{verificationToken}} + body: json + auth: none +} + +params:query { + t: {{verificationToken}} +} + +body:json { + { + "username": "{{username}}", + "password": "{{password}}", + "email": "zachary@dziura.email", + "name": "Z. Charles Dziura" + } +} + +script:post-response { + const body = res.getBody(); + const data = body.data; + + if (res.status < 300) { + bru.setEnvVar("userId", data.userId); + + if (data.auth?.token) { + bru.setEnvVar("authToken", data.auth.token); + } + + if (data.session?.token) { + bru.setEnvVar("sessionToken", data.session.token); + } + } + +}