Handle login errors
This commit is contained in:
parent
29893f9ee7
commit
511a36516b
17 changed files with 68 additions and 37 deletions
|
@ -1,13 +1,11 @@
|
||||||
package ing.bikeshedengineer.debtpirate
|
package ing.bikeshedengineer.debtpirate
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@ import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.theme.DebtPirateTheme
|
||||||
|
import ing.bikeshedengineer.debtpirate.presentation.ui.auth.AuthScreen
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -27,7 +27,7 @@ class MainActivity : ComponentActivity() {
|
||||||
DebtPirateTheme {
|
DebtPirateTheme {
|
||||||
NavHost(navController = navController, startDestination = AuthRoute) {
|
NavHost(navController = navController, startDestination = AuthRoute) {
|
||||||
composable<AuthRoute> {
|
composable<AuthRoute> {
|
||||||
AuthScreen( )
|
AuthScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.ApiResponse
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
|
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
|
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
interface AuthEndpoint {
|
interface AuthEndpoint {
|
||||||
@POST("auth/login")
|
@POST("auth/login")
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
package ing.bikeshedengineer.debtpirate.data.remote.model
|
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?)
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package ing.bikeshedengineer.debtpirate.data.remote.model.auth
|
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)
|
data class AuthLoginTokenData(val token: String, val expiresAt: String)
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package ing.bikeshedengineer.debtpirate.data.remote.repository
|
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.AuthLoginRequest
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
|
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
|
||||||
|
|
||||||
interface AuthRepository {
|
interface AuthRepository {
|
||||||
suspend fun submitLoginRequest(credentials: AuthLoginRequest): Result<AuthLoginResponse>
|
suspend fun submitLoginRequest(credentials: AuthLoginRequest): AuthLoginResponse
|
||||||
}
|
}
|
|
@ -4,13 +4,9 @@ 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.BuildConfig
|
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
|
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
|
||||||
import ing.bikeshedengineer.debtpirate.domain.repository.AuthRepositoryImpl
|
import ing.bikeshedengineer.debtpirate.domain.repository.AuthRepositoryImpl
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
@ -21,11 +21,12 @@ object HttpClientModule {
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl(BuildConfig.API_BASE_URL)
|
.baseUrl(BuildConfig.API_BASE_URL)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.client(OkHttpClient.Builder()
|
.client(
|
||||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
OkHttpClient.Builder()
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
})
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
.build()
|
})
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,8 @@ import java.time.ZonedDateTime
|
||||||
import java.time.format.DateTimeFormatter.ISO_INSTANT
|
import java.time.format.DateTimeFormatter.ISO_INSTANT
|
||||||
|
|
||||||
data class Token(val token: String, val expiresAt: ZonedDateTime) {
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package ing.bikeshedengineer.debtpirate.domain.repository
|
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.endpoint.AuthEndpoint
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.model.ApiResponse
|
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.AuthLoginRequest
|
||||||
|
@ -8,21 +10,24 @@ import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import retrofit2.Retrofit
|
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)
|
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) {
|
return withContext(Dispatchers.IO) {
|
||||||
val response = authEndpoint.submitLoginRequest(credentials)
|
val response = authEndpoint.submitLoginRequest(credentials)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val body = response.body()
|
val body = response.body()
|
||||||
return@withContext Result.success(body?.data!!)
|
return@withContext body!!.data!!
|
||||||
} else {
|
} else {
|
||||||
val error = response.errorBody()
|
val gson = Gson()
|
||||||
return@withContext Result.failure(Throwable(error.toString()))
|
val errorType = object : TypeToken<ApiResponse<Unit>>() {}.type
|
||||||
|
val body =
|
||||||
|
gson.fromJson<ApiResponse<Unit>>(response.errorBody()!!.charStream(), errorType)
|
||||||
|
|
||||||
|
throw Throwable(body!!.error!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ private fun LoginComponent(
|
||||||
leadingIcon = { Icon(Icons.Outlined.Person, "person") },
|
leadingIcon = { Icon(Icons.Outlined.Person, "person") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.Words,
|
capitalization = KeyboardCapitalization.None,
|
||||||
keyboardType = KeyboardType.Text,
|
keyboardType = KeyboardType.Text,
|
||||||
imeAction = ImeAction.Next
|
imeAction = ImeAction.Next
|
||||||
),
|
),
|
||||||
|
@ -117,7 +117,8 @@ private fun LoginComponent(
|
||||||
onSend = { submitLoginRequest() }
|
onSend = { submitLoginRequest() }
|
||||||
),
|
),
|
||||||
onValueChange = onUpdatePassword,
|
onValueChange = onUpdatePassword,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.padding(PaddingValues(top = 4.dp))
|
.padding(PaddingValues(top = 4.dp))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
package ing.bikeshedengineer.debtpirate.presentation.ui.auth
|
package ing.bikeshedengineer.debtpirate.presentation.ui.auth
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
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.data.remote.model.auth.AuthLoginRequest
|
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
|
||||||
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
|
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
|
||||||
|
import ing.bikeshedengineer.debtpirate.domain.model.Token
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@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
|
private val tag: String
|
||||||
get() {
|
get() {
|
||||||
return javaClass.simpleName
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.DebtPirate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.DebtPirate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.DebtPirate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.DebtPirate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
<!--Set application-wide security config using base-config tag.-->
|
<!--Set application-wide security config using base-config tag.-->
|
||||||
<base-config cleartextTrafficPermitted="true"/>
|
<base-config cleartextTrafficPermitted="true" />
|
||||||
</network-security-config>
|
</network-security-config>
|
|
@ -1,8 +1,7 @@
|
||||||
package ing.bikeshedengineer.debtpirate
|
package ing.bikeshedengineer.debtpirate
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
|
Loading…
Add table
Reference in a new issue