After a successful registration, store the user's credentials with credential manager
This commit is contained in:
parent
3a093b60b4
commit
5a437a9813
22 changed files with 216 additions and 220 deletions
|
@ -85,6 +85,7 @@ kapt {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.androidx.biometric)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.datastore)
|
implementation(libs.androidx.datastore)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|
6
app/app/proguard-rules.pro
vendored
6
app/app/proguard-rules.pro
vendored
|
@ -18,4 +18,8 @@
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
-if class androidx.credentials.CredentialManager
|
||||||
|
-keep class androidx.credentials.playservices.** {
|
||||||
|
*;
|
||||||
|
}
|
|
@ -14,6 +14,11 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.DebtPirate"
|
android:theme="@style/Theme.DebtPirate"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="asset_statements"
|
||||||
|
android:resource="@string/asset_statements" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".app.host.MainActivity"
|
android:name=".app.host.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
@ -22,7 +27,7 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm.Conf
|
||||||
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm.ConfirmationScreen
|
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm.ConfirmationScreen
|
||||||
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginScreen
|
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginScreen
|
||||||
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreen
|
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreen
|
||||||
|
import ing.bikeshedengineer.debtpirate.app.screen.home.presentation.overview.OverviewScreen
|
||||||
|
import ing.bikeshedengineer.debtpirate.domain.usecase.GetStoredTokensUseCase
|
||||||
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
|
||||||
|
@ -42,6 +44,9 @@ class MainActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var navigator: Navigator
|
lateinit var navigator: Navigator
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var getStoredTokens: GetStoredTokensUseCase
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -104,6 +109,14 @@ class MainActivity : ComponentActivity() {
|
||||||
ConfirmationScreen(ConfirmationData.UserConfirmationData(userId, verificationToken))
|
ConfirmationScreen(ConfirmationData.UserConfirmationData(userId, verificationToken))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigation<Destination.HomeGraph>(
|
||||||
|
startDestination = Destination.HomeOverview
|
||||||
|
) {
|
||||||
|
composable<Destination.HomeOverview> {
|
||||||
|
OverviewScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
@ -32,8 +31,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
@ -44,10 +41,8 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.credentials.CredentialManager
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import ing.bikeshedengineer.debtpirate.domain.model.AccountManagerResult
|
|
||||||
import ing.bikeshedengineer.debtpirate.domain.repository.AccountManager
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -55,23 +50,7 @@ fun LoginScreen(
|
||||||
viewModel: LoginScreenViewModel = hiltViewModel<LoginScreenViewModel>()
|
viewModel: LoginScreenViewModel = hiltViewModel<LoginScreenViewModel>()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current as ComponentActivity
|
val context = LocalContext.current as ComponentActivity
|
||||||
val accountManager = remember {
|
val _credentialManager = CredentialManager.create(context)
|
||||||
AccountManager(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
LaunchedEffect(true) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
val result = accountManager.getCredentials()
|
|
||||||
when (result) {
|
|
||||||
is AccountManagerResult.FoundCredentials -> {
|
|
||||||
val (emailAddress, password) = result
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val toastMessages = viewModel.toastMessages.collectAsState("")
|
val toastMessages = viewModel.toastMessages.collectAsState("")
|
||||||
LaunchedEffect(toastMessages.value) {
|
LaunchedEffect(toastMessages.value) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
|
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.credentials.CreatePasswordRequest
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -31,9 +32,10 @@ class LoginScreenViewModel @Inject constructor(
|
||||||
private val _isEmailAddressPristine = MutableStateFlow(true)
|
private val _isEmailAddressPristine = MutableStateFlow(true)
|
||||||
private val _emailAddress = MutableStateFlow("")
|
private val _emailAddress = MutableStateFlow("")
|
||||||
val emailAddress = _emailAddress.asStateFlow()
|
val emailAddress = _emailAddress.asStateFlow()
|
||||||
val isValidEmailAddress = _isEmailAddressPristine.combine(_emailAddress.map { it.isNotBlank() }) { isPristine, isValid ->
|
val isValidEmailAddress =
|
||||||
isPristine || isValid
|
_isEmailAddressPristine.combine(_emailAddress.map { it.isNotBlank() }) { isPristine, isValid ->
|
||||||
}
|
isPristine || isValid
|
||||||
|
}
|
||||||
|
|
||||||
private val _isPasswordPristine = MutableStateFlow(true)
|
private val _isPasswordPristine = MutableStateFlow(true)
|
||||||
private val _password = MutableStateFlow("")
|
private val _password = MutableStateFlow("")
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register
|
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
@ -32,8 +30,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
@ -50,37 +46,33 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.credentials.CreatePasswordRequest
|
||||||
|
import androidx.credentials.CredentialManager
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialException
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import ing.bikeshedengineer.debtpirate.domain.model.AccountManagerResult
|
|
||||||
import ing.bikeshedengineer.debtpirate.domain.repository.AccountManager
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
|
||||||
@Composable()
|
@Composable()
|
||||||
fun RegistrationScreen(
|
fun RegistrationScreen(
|
||||||
viewModel: RegistrationScreenViewModel = hiltViewModel<RegistrationScreenViewModel>()
|
viewModel: RegistrationScreenViewModel = hiltViewModel<RegistrationScreenViewModel>()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current as ComponentActivity
|
val localContext = LocalContext.current
|
||||||
val accountManager = remember { AccountManager(context) }
|
val onRegistrationMessage = viewModel.onRegistrationComplete.collectAsState(null)
|
||||||
|
LaunchedEffect(onRegistrationMessage.value) {
|
||||||
|
val message = onRegistrationMessage.value
|
||||||
|
if (message != null) {
|
||||||
|
val (username, password) = message
|
||||||
|
val credentialManager = CredentialManager.create(localContext)
|
||||||
|
val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
try {
|
||||||
LaunchedEffect(true) {
|
credentialManager.createCredential(localContext, createPasswordRequest)
|
||||||
coroutineScope.launch {
|
Log.d("RegistrationScreen", "Successfully stored login credentials")
|
||||||
viewModel.onRegistrationComplete.collect { credentials ->
|
} catch (err: CreateCredentialException) {
|
||||||
val (emailAddress, password) = credentials
|
// TODO: Display a toast...
|
||||||
val result = accountManager.storeCredentials(emailAddress, password)
|
|
||||||
|
|
||||||
if (result !is AccountManagerResult.Success) {
|
|
||||||
Log.i(
|
|
||||||
"RegistrationScreen",
|
|
||||||
"Not able to store credentials in CredentialManager"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.onAction(RegistrationScreenAction.ResetFields)
|
|
||||||
viewModel.navigateToConfirmationScreen(emailAddress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.navigateToConfirmationScreen(username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register
|
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register
|
||||||
|
|
||||||
|
import androidx.credentials.CreatePasswordRequest
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -32,11 +33,7 @@ class RegistrationScreenViewModel @Inject constructor(
|
||||||
|
|
||||||
fun navigateToConfirmationScreen(emailAddress: String) {
|
fun navigateToConfirmationScreen(emailAddress: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
navigator.navigate(Destination.AuthRegistrationConfirmation(emailAddress)) {
|
navigator.navigate(Destination.AuthRegistrationConfirmation(emailAddress))
|
||||||
popUpTo(Destination.AuthLogin) {
|
|
||||||
inclusive = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +115,7 @@ class RegistrationScreenViewModel @Inject constructor(
|
||||||
val result =
|
val result =
|
||||||
submitAccountRegistrationRequest(emailAddress, name, confirmPassword)
|
submitAccountRegistrationRequest(emailAddress, name, confirmPassword)
|
||||||
|
|
||||||
val (userId, expiresAt, sessionToken) = result
|
val (_, expiresAt, sessionToken) = result
|
||||||
|
|
||||||
updateStoreData(
|
updateStoreData(
|
||||||
userId = result.userId,
|
userId = result.userId,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ing.bikeshedengineer.debtpirate.app.screen.home.presentation.overview
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable()
|
||||||
|
fun OverviewScreen() {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(16.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
OverviewComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OverviewComponent() {
|
||||||
|
Text("Hello, world!")
|
||||||
|
}
|
|
@ -1,10 +1,25 @@
|
||||||
package ing.bikeshedengineer.debtpirate.data.repository
|
package ing.bikeshedengineer.debtpirate.data.repository
|
||||||
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
||||||
|
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
interface AuthRepository {
|
interface AuthRepository {
|
||||||
suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>>
|
suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository {
|
||||||
|
private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java)
|
||||||
|
|
||||||
|
override suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val response = authEndpoint.submitAuthLoginRequest(credentials)
|
||||||
|
return@withContext response
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,59 @@
|
||||||
package ing.bikeshedengineer.debtpirate.data.repository
|
package ing.bikeshedengineer.debtpirate.data.repository
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
||||||
|
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.UserEndpoint
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest
|
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse
|
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetRequest
|
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetRequest
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetResponse
|
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetResponse
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
interface UserRepository {
|
interface UserRepository {
|
||||||
suspend fun createUserPostRequest(request: UserCreatePostRequest): UserCreatePostResponse
|
suspend fun createUserPostRequest(request: UserCreatePostRequest): UserCreatePostResponse
|
||||||
suspend fun verifyUserGetRequest(request: UserVerificationGetRequest): UserVerificationGetResponse
|
suspend fun verifyUserGetRequest(request: UserVerificationGetRequest): UserVerificationGetResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserRepositoryImpl(httpClient: Retrofit) : UserRepository {
|
||||||
|
private val userEndpoint: UserEndpoint = httpClient.create(UserEndpoint::class.java)
|
||||||
|
|
||||||
|
override suspend fun createUserPostRequest(request: UserCreatePostRequest): UserCreatePostResponse {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val response = userEndpoint.createUserPostRequest(request)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body()
|
||||||
|
return@withContext body!!.data!!
|
||||||
|
} else {
|
||||||
|
val gson = Gson()
|
||||||
|
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
|
||||||
|
val body =
|
||||||
|
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
|
||||||
|
|
||||||
|
throw Throwable(body!!.error!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun verifyUserGetRequest(request: UserVerificationGetRequest): UserVerificationGetResponse {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val (userId, verificationToken) = request
|
||||||
|
val response = userEndpoint.userVerificationGetRequest(userId, verificationToken)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body()
|
||||||
|
return@withContext body!!.data!!
|
||||||
|
} else {
|
||||||
|
val gson = Gson()
|
||||||
|
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
|
||||||
|
val body =
|
||||||
|
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
|
||||||
|
|
||||||
|
throw Throwable(body!!.error!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@ 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.AppDataStore
|
import ing.bikeshedengineer.debtpirate.AppDataStore
|
||||||
|
import ing.bikeshedengineer.debtpirate.domain.usecase.GetStoredTokensUseCase
|
||||||
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
|
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@ -16,4 +17,8 @@ object UseCaseModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideUpdateStoreDataUseCase(store: DataStore<AppDataStore>) = UpdateStoreDataUseCase(store)
|
fun provideUpdateStoreDataUseCase(store: DataStore<AppDataStore>) = UpdateStoreDataUseCase(store)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGetStoredTokensUseCase(store: DataStore<AppDataStore>) = GetStoredTokensUseCase(store)
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ViewModelComponent
|
import dagger.hilt.android.components.ViewModelComponent
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
||||||
import ing.bikeshedengineer.debtpirate.domain.repository.AuthRepositoryImpl
|
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepositoryImpl
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
@ -6,7 +6,7 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ViewModelComponent
|
import dagger.hilt.android.components.ViewModelComponent
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
import ing.bikeshedengineer.debtpirate.data.repository.UserRepository
|
import ing.bikeshedengineer.debtpirate.data.repository.UserRepository
|
||||||
import ing.bikeshedengineer.debtpirate.domain.repository.UserRepositoryImpl
|
import ing.bikeshedengineer.debtpirate.data.repository.UserRepositoryImpl
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
package ing.bikeshedengineer.debtpirate.domain.repository
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
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.CreateCredentialCancellationException
|
|
||||||
import androidx.credentials.exceptions.CreateCredentialException
|
|
||||||
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
|
|
||||||
import ing.bikeshedengineer.debtpirate.domain.model.AccountManagerResult
|
|
||||||
|
|
||||||
class AccountManager(private val context: Activity) {
|
|
||||||
private val credentialManager = CredentialManager.create(context)
|
|
||||||
|
|
||||||
suspend fun storeCredentials(
|
|
||||||
emailAddress: String,
|
|
||||||
password: String
|
|
||||||
): AccountManagerResult {
|
|
||||||
return try {
|
|
||||||
credentialManager.createCredential(
|
|
||||||
context, request = CreatePasswordRequest(
|
|
||||||
id = emailAddress,
|
|
||||||
password
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
AccountManagerResult.Success
|
|
||||||
} catch (err: CreateCredentialNoCreateOptionException) {
|
|
||||||
Log.w(
|
|
||||||
"AccountManager",
|
|
||||||
"Cannot store credentials; a Google account isn't associated with this device"
|
|
||||||
)
|
|
||||||
|
|
||||||
AccountManagerResult.Unavailable
|
|
||||||
} catch (_: CreateCredentialCancellationException) {
|
|
||||||
AccountManagerResult.Canceled
|
|
||||||
} catch (err: CreateCredentialException) {
|
|
||||||
err.printStackTrace()
|
|
||||||
Log.i(
|
|
||||||
"AccountManager",
|
|
||||||
"Unable to store credentials: ${err.message}"
|
|
||||||
)
|
|
||||||
|
|
||||||
AccountManagerResult.Failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getCredentials(): AccountManagerResult {
|
|
||||||
return try {
|
|
||||||
val result = credentialManager.getCredential(
|
|
||||||
context, request = GetCredentialRequest(
|
|
||||||
credentialOptions = listOf(GetPasswordOption(isAutoSelectAllowed = true))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val credentials = result.credential
|
|
||||||
when (credentials) {
|
|
||||||
is PasswordCredential -> {
|
|
||||||
val emailAddress = credentials.id
|
|
||||||
val password = credentials.password
|
|
||||||
|
|
||||||
AccountManagerResult.FoundCredentials(emailAddress, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> AccountManagerResult.Invalid
|
|
||||||
}
|
|
||||||
} catch (err: Exception) {
|
|
||||||
AccountManagerResult.Failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package ing.bikeshedengineer.debtpirate.domain.repository
|
|
||||||
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostRequest
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginPostResponse
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
|
|
||||||
class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository {
|
|
||||||
private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java)
|
|
||||||
|
|
||||||
override suspend fun submitAuthLoginRequest(credentials: AuthLoginPostRequest): Response<ApiResponse<AuthLoginPostResponse>> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val response = authEndpoint.submitAuthLoginRequest(credentials)
|
|
||||||
return@withContext response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package ing.bikeshedengineer.debtpirate.domain.repository
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.UserEndpoint
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetRequest
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetResponse
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.repository.UserRepository
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
|
|
||||||
class UserRepositoryImpl(httpClient: Retrofit) : UserRepository {
|
|
||||||
private val userEndpoint: UserEndpoint = httpClient.create(UserEndpoint::class.java)
|
|
||||||
|
|
||||||
override suspend fun createUserPostRequest(request: UserCreatePostRequest): UserCreatePostResponse {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val response = userEndpoint.createUserPostRequest(request)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
val body = response.body()
|
|
||||||
return@withContext body!!.data!!
|
|
||||||
} else {
|
|
||||||
val gson = Gson()
|
|
||||||
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
|
|
||||||
val body =
|
|
||||||
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
|
|
||||||
|
|
||||||
throw Throwable(body!!.error!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun verifyUserGetRequest(request: UserVerificationGetRequest): UserVerificationGetResponse {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val (userId, verificationToken) = request
|
|
||||||
val response = userEndpoint.userVerificationGetRequest(userId, verificationToken)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
val body = response.body()
|
|
||||||
return@withContext body!!.data!!
|
|
||||||
} else {
|
|
||||||
val gson = Gson()
|
|
||||||
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
|
|
||||||
val body =
|
|
||||||
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
|
|
||||||
|
|
||||||
throw Throwable(body!!.error!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package ing.bikeshedengineer.debtpirate.domain.usecase
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import ing.bikeshedengineer.debtpirate.AppDataStore
|
||||||
|
import ing.bikeshedengineer.debtpirate.domain.model.Token
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
class GetStoredTokensUseCase(private val store: DataStore<AppDataStore>) {
|
||||||
|
operator fun invoke(): Flow<Pair<Token, Token?>?> {
|
||||||
|
return store.data.map { store ->
|
||||||
|
if (store.isInitialized) {
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
val authTokenExpiration = ZonedDateTime.ofInstant(Instant.ofEpochSecond(store.authToken.expiresAt), ZoneOffset.UTC)
|
||||||
|
if (authTokenExpiration.isBefore(now)) {
|
||||||
|
return@map null
|
||||||
|
}
|
||||||
|
|
||||||
|
val sessionTokenExpiration = ZonedDateTime.ofInstant(Instant.ofEpochSecond(store.sessionToken.expiresAt), ZoneOffset.UTC)
|
||||||
|
return@map Pair(
|
||||||
|
Token(
|
||||||
|
store.authToken.token,
|
||||||
|
authTokenExpiration
|
||||||
|
),
|
||||||
|
if (sessionTokenExpiration.isBefore(now)) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Token(
|
||||||
|
store.sessionToken.token,
|
||||||
|
sessionTokenExpiration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return@map null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import androidx.datastore.core.DataStore
|
||||||
import ing.bikeshedengineer.debtpirate.AppDataStore
|
import ing.bikeshedengineer.debtpirate.AppDataStore
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class UpdateStoreDataUseCase(val store: DataStore<AppDataStore>) {
|
class UpdateStoreDataUseCase(private val store: DataStore<AppDataStore>) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
userId: Int? = null,
|
userId: Int? = null,
|
||||||
authToken: String? = null,
|
authToken: String? = null,
|
||||||
|
|
|
@ -24,4 +24,11 @@ sealed interface Destination {
|
||||||
val userId: Int,
|
val userId: Int,
|
||||||
val verificationToken: String
|
val verificationToken: String
|
||||||
) : Destination
|
) : Destination
|
||||||
|
|
||||||
|
/* Home Destinations */
|
||||||
|
@Serializable
|
||||||
|
data object HomeGraph : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object HomeOverview : Destination
|
||||||
}
|
}
|
|
@ -1,3 +1,8 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Debt Pirate</string>
|
<string name="app_name">Debt Pirate</string>
|
||||||
|
<string name="asset_statements" translatable="false">
|
||||||
|
[{
|
||||||
|
\"include\": \"https://debtpirate.app/.well-known/assetlinks.json\"
|
||||||
|
}]
|
||||||
|
</string>
|
||||||
</resources>
|
</resources>
|
|
@ -2,12 +2,13 @@
|
||||||
activityCompose = "1.9.3"
|
activityCompose = "1.9.3"
|
||||||
agp = "8.7.3"
|
agp = "8.7.3"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
composeBom = "2024.11.00"
|
biometric = "1.4.0-alpha02"
|
||||||
|
composeBom = "2024.12.01"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
datastore = "1.1.1"
|
datastore = "1.1.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
hilt = "2.51.1"
|
hilt = "2.51.1"
|
||||||
iconsExtended = "1.7.5"
|
iconsExtended = "1.7.6"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
kotlin = "2.0.10"
|
kotlin = "2.0.10"
|
||||||
|
@ -17,19 +18,20 @@ lifecycleRuntimeKtx = "2.8.7"
|
||||||
lifecycleViewModelKtx = "2.8.7"
|
lifecycleViewModelKtx = "2.8.7"
|
||||||
lifecycleViewmodelCompose = "2.8.7"
|
lifecycleViewmodelCompose = "2.8.7"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
material3 = "1.4.0-alpha04"
|
material3 = "1.4.0-alpha05"
|
||||||
navigation = "2.8.4"
|
navigation = "2.8.5"
|
||||||
protobuf = "0.9.4"
|
protobuf = "0.9.4"
|
||||||
protoLite = "3.21.11"
|
protoLite = "3.21.11"
|
||||||
okhttp = "4.10.0"
|
okhttp = "4.10.0"
|
||||||
retrofit = "2.9.0"
|
retrofit = "2.9.0"
|
||||||
hiltNavigationCompose = "1.2.0"
|
hiltNavigationCompose = "1.2.0"
|
||||||
fonts = "1.7.5"
|
fonts = "1.7.6"
|
||||||
credentialManager = "1.5.0-beta01"
|
credentialManager = "1.5.0-beta01"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
androidx-credentials-core = { group = "androidx.credentials", name = "credentials", version.ref = "credentialManager" }
|
androidx-credentials-core = { group = "androidx.credentials", name = "credentials", version.ref = "credentialManager" }
|
||||||
|
|
Loading…
Add table
Reference in a new issue