Handle login errors

This commit is contained in:
Z. Charles Dziura 2024-10-27 09:45:46 -04:00
parent 29893f9ee7
commit 511a36516b
17 changed files with 68 additions and 37 deletions

View file

@ -1,13 +1,11 @@
package ing.bikeshedengineer.debtpirate
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />

View file

@ -8,8 +8,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import ing.bikeshedengineer.debtpirate.presentation.ui.auth.AuthScreen
import ing.bikeshedengineer.debtpirate.presentation.theme.DebtPirateTheme
import ing.bikeshedengineer.debtpirate.presentation.ui.auth.AuthScreen
import kotlinx.serialization.Serializable
@Serializable
@ -27,7 +27,7 @@ class MainActivity : ComponentActivity() {
DebtPirateTheme {
NavHost(navController = navController, startDestination = AuthRoute) {
composable<AuthRoute> {
AuthScreen( )
AuthScreen()
}
}
}

View file

@ -3,9 +3,9 @@ package ing.bikeshedengineer.debtpirate.data.remote.endpoint
import ing.bikeshedengineer.debtpirate.data.remote.model.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
import retrofit2.http.POST
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface AuthEndpoint {
@POST("auth/login")

View file

@ -1,3 +1,3 @@
package ing.bikeshedengineer.debtpirate.data.remote.model
data class ApiResponse<T>(val data: T?, val err: Any?)
data class ApiResponse<T>(val data: T?, val error: String?)

View file

@ -1,5 +1,9 @@
package ing.bikeshedengineer.debtpirate.data.remote.model.auth
data class AuthLoginResponse(val userId: Int, val session: AuthLoginTokenData, val auth: AuthLoginTokenData)
data class AuthLoginResponse(
val userId: Int,
val session: AuthLoginTokenData,
val auth: AuthLoginTokenData
)
data class AuthLoginTokenData(val token: String, val expiresAt: String)

View file

@ -1,9 +1,8 @@
package ing.bikeshedengineer.debtpirate.data.remote.repository
import ing.bikeshedengineer.debtpirate.data.remote.model.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
interface AuthRepository {
suspend fun submitLoginRequest(credentials: AuthLoginRequest): Result<AuthLoginResponse>
suspend fun submitLoginRequest(credentials: AuthLoginRequest): AuthLoginResponse
}

View file

@ -4,13 +4,9 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ing.bikeshedengineer.debtpirate.BuildConfig
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
import ing.bikeshedengineer.debtpirate.domain.repository.AuthRepositoryImpl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module

View file

@ -21,11 +21,12 @@ object HttpClientModule {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
.client(
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
)
.build()
}

View file

@ -4,5 +4,8 @@ import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_INSTANT
data class Token(val token: String, val expiresAt: ZonedDateTime) {
constructor(token: String, expiresAtStr: String): this(token, ZonedDateTime.parse(expiresAtStr, ISO_INSTANT))
constructor(token: String, expiresAtStr: String) : this(
token,
ZonedDateTime.parse(expiresAtStr, ISO_INSTANT)
)
}

View file

@ -1,5 +1,7 @@
package ing.bikeshedengineer.debtpirate.domain.repository
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
import ing.bikeshedengineer.debtpirate.data.remote.model.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
@ -8,21 +10,24 @@ import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Retrofit
import kotlin.Result
class AuthRepositoryImpl constructor(private val httpClient: Retrofit): AuthRepository {
class AuthRepositoryImpl(private val httpClient: Retrofit) : AuthRepository {
private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java)
override suspend fun submitLoginRequest(credentials: AuthLoginRequest): Result<AuthLoginResponse> {
override suspend fun submitLoginRequest(credentials: AuthLoginRequest): AuthLoginResponse {
return withContext(Dispatchers.IO) {
val response = authEndpoint.submitLoginRequest(credentials)
if (response.isSuccessful) {
val body = response.body()
return@withContext Result.success(body?.data!!)
return@withContext body!!.data!!
} else {
val error = response.errorBody()
return@withContext Result.failure(Throwable(error.toString()))
val gson = Gson()
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
val body =
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
throw Throwable(body!!.error!!)
}
}
}

View file

@ -94,7 +94,7 @@ private fun LoginComponent(
leadingIcon = { Icon(Icons.Outlined.Person, "person") },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
@ -117,7 +117,8 @@ private fun LoginComponent(
onSend = { submitLoginRequest() }
),
onValueChange = onUpdatePassword,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.padding(PaddingValues(top = 4.dp))
)

View file

@ -1,18 +1,24 @@
package ing.bikeshedengineer.debtpirate.presentation.ui.auth
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import ing.bikeshedengineer.debtpirate.PrefsDataStore
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
import ing.bikeshedengineer.debtpirate.domain.model.Token
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AuthScreenViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() {
class AuthScreenViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val prefsStore: DataStore<PrefsDataStore>
) : ViewModel() {
private val tag: String
get() {
return javaClass.simpleName
@ -46,4 +52,24 @@ class AuthScreenViewModel @Inject constructor(private val authRepository: AuthRe
}
}
}
fun storeAuthData(userId: Int, sessionToken: Token, authToken: Token) {
viewModelScope.launch {
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

@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<!-- Base application theme. -->
<style name="Theme.DebtPirate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->

View file

@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<!-- Base application theme. -->
<style name="Theme.DebtPirate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--Set application-wide security config using base-config tag.-->
<base-config cleartextTrafficPermitted="true"/>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

View file

@ -1,8 +1,7 @@
package ing.bikeshedengineer.debtpirate
import org.junit.Test
import org.junit.Assert.*
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).