Rework credential manager flow
This commit is contained in:
parent
a3b53d6fa6
commit
4333ad764b
24 changed files with 566 additions and 148 deletions
|
@ -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<Destination.AuthLogin> {
|
||||
val viewModel = hiltViewModel<LoginScreenViewModel>()
|
||||
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<Destination.AuthUserConfirmation> {
|
||||
val (userId, verificationToken) = it.toRoute<Destination.AuthUserConfirmation>()
|
||||
ConfirmationScreen(ConfirmationData.UserConfirmationData(userId, verificationToken))
|
||||
ConfirmationScreen(
|
||||
ConfirmationData.UserConfirmationData(
|
||||
userId,
|
||||
verificationToken
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
isPasswordValid: State<Boolean>,
|
||||
toastMessages: State<String>,
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<CredentialManagerRequest>
|
||||
|
||||
suspend fun getCredentials()
|
||||
}
|
||||
|
||||
class CredentialManagerImpl() : CredentialManager {
|
||||
private val _credentialRequests = Channel<CredentialManagerRequest>()
|
||||
override val credentialRequests = _credentialRequests.receiveAsFlow()
|
||||
|
||||
override suspend fun getCredentials() {
|
||||
this._credentialRequests.send(CredentialManagerRequest.GetCredentials)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<Unit>
|
||||
suspend fun makeUserCredentialsRequest()
|
||||
|
||||
val userCredentialsResponse: Flow<UserCredentialResponse>
|
||||
suspend fun returnUserCredentialsResponse(response: UserCredentialResponse)
|
||||
|
||||
val storeUserCredentials: Flow<Pair<String, String>>
|
||||
suspend fun storeUserCredentials(emailAddress: String, password: String)
|
||||
}
|
||||
|
||||
class UserCredentialManagerImpl : UserCredentialManager {
|
||||
private val _requestUserCredentials = Channel<Unit>()
|
||||
override val requestUserCredentials = _requestUserCredentials.receiveAsFlow()
|
||||
|
||||
private val _userCredentialsResponse = Channel<UserCredentialResponse>()
|
||||
override val userCredentialsResponse = _userCredentialsResponse.receiveAsFlow()
|
||||
|
||||
private val _storeUserCredentials = Channel<Pair<String, String>>()
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -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...
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||
|
||||
interface Navigator {
|
||||
val startDestination: Destination
|
||||
val navigationActions: Flow<NavigationAction>
|
||||
val actions: Flow<NavigationAction>
|
||||
|
||||
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<NavigationAction>()
|
||||
override val navigationActions = _navigationActions.receiveAsFlow()
|
||||
private val _actions = Channel<NavigationAction>()
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
29
bruno/Debt Pirate/account/create.bru
Normal file
29
bruno/Debt Pirate/account/create.bru
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
28
bruno/Debt Pirate/account/read.bru
Normal file
28
bruno/Debt Pirate/account/read.bru
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
41
bruno/Debt Pirate/auth/login.bru
Normal file
41
bruno/Debt Pirate/auth/login.bru
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
33
bruno/Debt Pirate/auth/session.bru
Normal file
33
bruno/Debt Pirate/auth/session.bru
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
9
bruno/Debt Pirate/bruno.json
Normal file
9
bruno/Debt Pirate/bruno.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"version": "1",
|
||||
"name": "Debt Pirate",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
28
bruno/Debt Pirate/budget/create.bru
Normal file
28
bruno/Debt Pirate/budget/create.bru
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
28
bruno/Debt Pirate/budget/read.bru
Normal file
28
bruno/Debt Pirate/budget/read.bru
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
0
bruno/Debt Pirate/collection.bru
Normal file
0
bruno/Debt Pirate/collection.bru
Normal file
11
bruno/Debt Pirate/environments/Localhost.bru
Normal file
11
bruno/Debt Pirate/environments/Localhost.bru
Normal file
|
@ -0,0 +1,11 @@
|
|||
vars {
|
||||
domain: localhost:42069
|
||||
protocol: http:
|
||||
email: zachary@dziura.email
|
||||
}
|
||||
vars:secret [
|
||||
password,
|
||||
verificationToken,
|
||||
sessionToken,
|
||||
authToken
|
||||
]
|
34
bruno/Debt Pirate/user/create.bru
Normal file
34
bruno/Debt Pirate/user/create.bru
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
42
bruno/Debt Pirate/user/verify.bru
Normal file
42
bruno/Debt Pirate/user/verify.bru
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue