Start saving data to proto datastore from successful rest api requests

This commit is contained in:
Z. Charles Dziura 2024-11-28 07:00:11 -05:00
parent 78a4b75acd
commit ece116a411
14 changed files with 131 additions and 59 deletions

View file

@ -111,13 +111,13 @@ async fn register_new_user_request(
UserRegistrationResponse { UserRegistrationResponse {
user_id, user_id,
expires_at, expires_at: None,
session_token: None, session_token: None,
} }
} else { } else {
UserRegistrationResponse { UserRegistrationResponse {
user_id, user_id,
expires_at, expires_at: Some(expires_at),
session_token: Some(verification_token), session_token: Some(verification_token),
} }
}; };

View file

@ -11,7 +11,7 @@ pub struct UserRegistrationResponse {
pub user_id: i32, pub user_id: i32,
#[serde(serialize_with = "humantime_serde::serialize")] #[serde(serialize_with = "humantime_serde::serialize")]
pub expires_at: SystemTime, pub expires_at: Option<SystemTime>,
pub session_token: Option<String>, pub session_token: Option<String>,
} }

View file

@ -5,7 +5,7 @@ import androidx.datastore.core.DataStore
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
import ing.bikeshedengineer.debtpirate.PrefsDataStore import ing.bikeshedengineer.debtpirate.AppDataStore
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitNewUserVerificationRequestUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitNewUserVerificationRequestUseCase
import ing.bikeshedengineer.debtpirate.navigation.Navigator import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -16,7 +16,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ConfirmationScreenViewModel @Inject constructor( class ConfirmationScreenViewModel @Inject constructor(
private val navigator: Navigator, private val navigator: Navigator,
private val prefsStore: DataStore<PrefsDataStore>, private val prefsStore: DataStore<AppDataStore>,
private val verifyNewUser: SubmitNewUserVerificationRequestUseCase private val verifyNewUser: SubmitNewUserVerificationRequestUseCase
) : ViewModel() { ) : ViewModel() {
private var _isLoading = MutableStateFlow(true) private var _isLoading = MutableStateFlow(true)

View file

@ -1,7 +1,6 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log
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
@ -76,8 +75,10 @@ fun LoginScreen(
val toastMessages = viewModel.toastMessages.collectAsState("") val toastMessages = viewModel.toastMessages.collectAsState("")
LaunchedEffect(toastMessages.value) { LaunchedEffect(toastMessages.value) {
val message = toastMessages.value val message = toastMessages.value
Log.d("LoginScreen", message)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() if (message.isNotEmpty()) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
} }
Scaffold( Scaffold(

View file

@ -1,16 +1,13 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.util.Log
import android.widget.Toast
import androidx.datastore.core.DataStore
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
import ing.bikeshedengineer.debtpirate.PrefsDataStore
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.LoginCredentialsValidationResult import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.LoginCredentialsValidationResult
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.domain.model.Token import ing.bikeshedengineer.debtpirate.domain.repository.InvalidCredentialsException
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
import ing.bikeshedengineer.debtpirate.navigation.Destination import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -37,13 +34,12 @@ enum class InvalidReason {
PasswordTooShort PasswordTooShort
} }
@HiltViewModel @HiltViewModel
class LoginScreenViewModel @Inject constructor( class LoginScreenViewModel @Inject constructor(
private val navigator: Navigator, private val navigator: Navigator,
private val prefsStore: DataStore<PrefsDataStore>,
private val submitLoginCredentials: SubmitLoginCredentialsUseCase, private val submitLoginCredentials: SubmitLoginCredentialsUseCase,
private val validateLoginCredentials: ValidateLoginCredentialsUseCase, private val validateLoginCredentials: ValidateLoginCredentialsUseCase,
private val updateStoreData: UpdateStoreDataUseCase
) : ViewModel() { ) : ViewModel() {
private val _emailAddress = MutableStateFlow("") private val _emailAddress = MutableStateFlow("")
val emailAddress = _emailAddress.asStateFlow() val emailAddress = _emailAddress.asStateFlow()
@ -93,28 +89,23 @@ class LoginScreenViewModel @Inject constructor(
private suspend fun onSubmitLoginRequest(emailAddress: String, password: String) { private suspend fun onSubmitLoginRequest(emailAddress: String, password: String) {
try { try {
val result = submitLoginCredentials(emailAddress, password) val (userId, auth, session) = submitLoginCredentials(emailAddress, password)
updateStoreData(
userId = userId,
authToken = auth.token,
authTokenExpiresAt = auth.expiresAt,
sessionToken = session.token,
sessionTokenExpiresAt = session.expiresAt
)
} catch (err: Exception) { } catch (err: Exception) {
_toastMessages.emit("Invalid Email Address or Password") when (err) {
} is InvalidCredentialsException -> {
} _toastMessages.emit("Invalid Email Address or Password")
}
fun storeAuthData(userId: Int, sessionToken: Token, authToken: Token) { else -> {
viewModelScope.launch { _toastMessages.emit("Cannot Login, Please Try Again Later")
prefsStore.updateData { currentPrefs -> }
val updatedSessionToken = currentPrefs.sessionToken.toBuilder()
.setToken(sessionToken.token)
.setExpiresAt(sessionToken.expiresAt.toEpochSecond())
val updatedAuthToken = currentPrefs.authToken.toBuilder()
.setToken(authToken.token)
.setExpiresAt(authToken.expiresAt.toEpochSecond())
currentPrefs.toBuilder()
.setUserId(userId)
.setSessionToken(updatedSessionToken)
.setAuthToken(updatedAuthToken)
.build()
} }
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -231,9 +232,19 @@ private fun RegistrationComponent(
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done imeAction = ImeAction.Send
), ),
onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateConfirmPassword(it)) }, onValueChange = { viewModel.onAction(RegistrationScreenAction.UpdateConfirmPassword(it)) },
keyboardActions = KeyboardActions(
onSend = {
viewModel.registerNewAccount(
emailAddress.value,
name.value,
password.value,
confirmPassword.value
)
}
),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(PaddingValues(top = 4.dp)) .padding(PaddingValues(top = 4.dp))

View file

@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.NewAccountRegistrationValidationResult import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.NewAccountRegistrationValidationResult
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitAccountRegistrationRequestUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitAccountRegistrationRequestUseCase
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateNewAccountRegistrationUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateNewAccountRegistrationUseCase
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
import ing.bikeshedengineer.debtpirate.navigation.Destination import ing.bikeshedengineer.debtpirate.navigation.Destination
import ing.bikeshedengineer.debtpirate.navigation.Navigator import ing.bikeshedengineer.debtpirate.navigation.Navigator
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -21,6 +22,7 @@ class RegistrationScreenViewModel @Inject constructor(
private val navigator: Navigator, private val navigator: Navigator,
private val validateNewAccount: ValidateNewAccountRegistrationUseCase, private val validateNewAccount: ValidateNewAccountRegistrationUseCase,
private val submitAccountRegistrationRequest: SubmitAccountRegistrationRequestUseCase, private val submitAccountRegistrationRequest: SubmitAccountRegistrationRequestUseCase,
private val updateStoreData: UpdateStoreDataUseCase,
) : ViewModel() { ) : ViewModel() {
fun navigateUp() { fun navigateUp() {
viewModelScope.launch { viewModelScope.launch {
@ -113,10 +115,17 @@ class RegistrationScreenViewModel @Inject constructor(
if (fieldsAreValid) { if (fieldsAreValid) {
viewModelScope.launch { viewModelScope.launch {
try { try {
// TODO: Store the registration result data in the store
val result = val result =
submitAccountRegistrationRequest(emailAddress, name, confirmPassword) submitAccountRegistrationRequest(emailAddress, name, confirmPassword)
val (userId, expiresAt, sessionToken) = result
updateStoreData(
userId = result.userId,
sessionToken = sessionToken,
sessionTokenExpiresAt = expiresAt
)
_onRegistrationComplete.emit(Pair(emailAddress, confirmPassword)) _onRegistrationComplete.emit(Pair(emailAddress, confirmPassword))
} catch (err: Throwable) { } catch (err: Throwable) {
// TODO... // TODO...

View file

@ -6,26 +6,26 @@ import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
import androidx.datastore.dataStore import androidx.datastore.dataStore
import com.google.protobuf.InvalidProtocolBufferException import com.google.protobuf.InvalidProtocolBufferException
import ing.bikeshedengineer.debtpirate.PrefsDataStore import ing.bikeshedengineer.debtpirate.AppDataStore
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
object PrefsDataStoreSerializer : Serializer<PrefsDataStore> { object PrefsDataStoreSerializer : Serializer<AppDataStore> {
override val defaultValue: PrefsDataStore override val defaultValue: AppDataStore
get() = PrefsDataStore.getDefaultInstance() get() = AppDataStore.getDefaultInstance()
override suspend fun readFrom(input: InputStream): PrefsDataStore { override suspend fun readFrom(input: InputStream): AppDataStore {
try { try {
return PrefsDataStore.parseFrom(input) return AppDataStore.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) { } catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto file.", exception) throw CorruptionException("Cannot read proto file.", exception)
} }
} }
override suspend fun writeTo(t: PrefsDataStore, output: OutputStream) = t.writeTo(output) override suspend fun writeTo(t: AppDataStore, output: OutputStream) = t.writeTo(output)
} }
val Context.prefsDataStore: DataStore<PrefsDataStore> by dataStore( val Context.appDataStore: DataStore<AppDataStore> by dataStore(
fileName = "prefs_data_store.proto", fileName = "app_data_store.proto",
serializer = PrefsDataStoreSerializer serializer = PrefsDataStoreSerializer
) )

View file

@ -1,9 +1,16 @@
package ing.bikeshedengineer.debtpirate.data.remote.model.auth package ing.bikeshedengineer.debtpirate.data.remote.model.auth
import com.google.gson.annotations.JsonAdapter
import ing.bikeshedengineer.debtpirate.domain.adapter.OffsetDateTimeAdapter
import java.time.OffsetDateTime
data class AuthLoginPostResponse( data class AuthLoginPostResponse(
val userId: Int, val userId: Int,
val auth: AuthLoginPostResponseTokenData,
val session: AuthLoginPostResponseTokenData, val session: AuthLoginPostResponseTokenData,
val auth: AuthLoginPostResponseTokenData
) )
data class AuthLoginPostResponseTokenData(val token: String, val expiresAt: String) data class AuthLoginPostResponseTokenData(
val token: String,
@JsonAdapter(OffsetDateTimeAdapter::class) val expiresAt: OffsetDateTime
)

View file

@ -7,6 +7,6 @@ import java.time.OffsetDateTime
data class UserCreatePostResponse( data class UserCreatePostResponse(
val userId: Int, val userId: Int,
@JsonAdapter(OffsetDateTimeAdapter::class) @JsonAdapter(OffsetDateTimeAdapter::class)
val expiresAt: OffsetDateTime, val expiresAt: OffsetDateTime? = null,
val sessionToken: String? = null val sessionToken: String? = null
) )

View file

@ -6,17 +6,17 @@ import dagger.Module
import dagger.Provides 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.PrefsDataStore import ing.bikeshedengineer.debtpirate.AppDataStore
import ing.bikeshedengineer.debtpirate.data.pref.prefsDataStore import ing.bikeshedengineer.debtpirate.data.pref.appDataStore
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object PrefsDataStoreProvider { object AppDataStoreProvider {
@Provides @Provides
@Singleton @Singleton
fun providePrefsDataStore(application: Application): DataStore<PrefsDataStore> { fun provideAppDataStore(application: Application): DataStore<AppDataStore> {
return application.prefsDataStore return application.appDataStore
} }
} }

View file

@ -0,0 +1,19 @@
package ing.bikeshedengineer.debtpirate.di
import androidx.datastore.core.DataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ing.bikeshedengineer.debtpirate.AppDataStore
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object UseCaseModule {
@Provides
@Singleton
fun provideUpdateStoreDataUseCase(store: DataStore<AppDataStore>) = UpdateStoreDataUseCase(store)
}

View file

@ -0,0 +1,35 @@
package ing.bikeshedengineer.debtpirate.domain.usecase
import androidx.datastore.core.DataStore
import ing.bikeshedengineer.debtpirate.AppDataStore
import java.time.OffsetDateTime
class UpdateStoreDataUseCase(val store: DataStore<AppDataStore>) {
suspend operator fun invoke(
userId: Int? = null,
authToken: String? = null,
authTokenExpiresAt: OffsetDateTime? = null,
sessionToken: String? = null,
sessionTokenExpiresAt: OffsetDateTime? = null
) {
store.updateData { currentStore ->
currentStore.toBuilder().apply {
if (userId != null) {
this.setUserId(userId)
}
if (authToken != null && authTokenExpiresAt != null) {
this.authToken.toBuilder().setToken(authToken).setExpiresAt(authTokenExpiresAt.toEpochSecond())
.build()
}
if (sessionToken != null && sessionTokenExpiresAt != null) {
this.sessionToken.toBuilder().setToken(sessionToken)
.setExpiresAt(sessionTokenExpiresAt.toEpochSecond()).build()
}
}
.build()
}
}
}

View file

@ -8,9 +8,8 @@ message Token {
int64 expires_at = 2; int64 expires_at = 2;
} }
message PrefsDataStore { message AppDataStore {
string current_route = 1; int32 user_id = 1;
int32 user_id = 2; Token auth_token = 2;
Token auth_token = 3; Token session_token = 3;
Token session_token = 4;
} }