Fix exception handling
continuous-integration/drone/push Build is passing Details

pull/5/head
avalin 9 months ago
parent 71deba6f12
commit 4ce266860f

@ -4,7 +4,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.api.interceptors.AllInAPIException
import fr.iut.alldev.allin.data.model.bet.BetFactory
import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.data.repository.BetRepository
@ -114,7 +113,7 @@ class BetCreationViewModel @Inject constructor(
betRepository.createBet(bet, keystoreManager.getTokenOrEmpty())
onSuccess()
} ?: onError()
} catch (e: AllInAPIException) {
} catch (e: Exception) {
Timber.e(e)
onError()
}

@ -46,7 +46,7 @@ import fr.iut.alldev.allin.ui.core.AllInTextField
fun LoginScreen(
navigateToDashboard: () -> Unit,
navigateToRegister: () -> Unit,
loginViewModel: LoginViewModel = hiltViewModel(),
loginViewModel: LoginViewModel = hiltViewModel()
) {
val focusManager = LocalFocusManager.current
@ -170,7 +170,9 @@ fun LoginScreen(
}
}
}
AllInLoading(visible = loading)
AllInAlertDialog(
enabled = hasLoginError,
title = stringResource(id = R.string.Login_Error_Title),

@ -4,12 +4,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.api.interceptors.AllInAPIException
import fr.iut.alldev.allin.data.repository.UserRepository
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@ -23,9 +24,7 @@ class LoginViewModel @Inject constructor(
val username = mutableStateOf("")
val password = mutableStateOf("")
fun onLogin(
navigateToDashboard: () -> Unit
) {
fun onLogin(navigateToDashboard: () -> Unit) {
viewModelScope.launch {
loading.value = true
@ -34,13 +33,15 @@ class LoginViewModel @Inject constructor(
userRepository
.login(username.value, password.value)
?.let { token -> keystoreManager.putToken(token) }
} catch (e: AllInAPIException) {
navigateToDashboard()
} catch (e: Exception) {
hasError.value = true
if (e !is HttpException || e.code() != 404) {
Timber.e(e)
}
}
}
if (!hasError.value) {
navigateToDashboard()
}
loading.value = false
}
}

@ -56,6 +56,7 @@ private val topLevelDestinations = listOf(
TopLevelDestination.BetCreation,
TopLevelDestination.BetHistory,
TopLevelDestination.Friends,
TopLevelDestination.Ranking,
TopLevelDestination.CurrentBets
)

@ -44,6 +44,7 @@ object Routes {
const val BET_HISTORY = "BET_HISTORY"
const val BET_CURRENT = "BET_CURRENT"
const val FRIENDS = "FRIENDS"
const val RANKING = "RANKING"
}
private val fadingRoutes
@ -66,7 +67,7 @@ internal fun NavHostController.popUpTo(route: String, baseRoute: String) {
fun AllInNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = Routes.SPLASH,
startDestination: String = Routes.SPLASH
) {
NavHost(
navController = navController,
@ -141,7 +142,7 @@ internal fun AllInDrawerNavHost(
}
composable(
route = Routes.FRIENDS
route = Routes.RANKING
) {
backHandlers()
RankingScreen()

@ -42,4 +42,11 @@ sealed class TopLevelDestination(
subtitle = R.string.current_bets_subtitle,
emoji = R.drawable.money_with_wings
)
data object Ranking : TopLevelDestination(
route = Routes.RANKING,
title = R.string.ranking,
subtitle = R.string.ranking_subtitle,
emoji = R.drawable.ranking
)
}

@ -1,31 +1,17 @@
package fr.iut.alldev.allin.ui.register
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.core.AllInGradientButton
import fr.iut.alldev.allin.ui.core.AllInLoading
import fr.iut.alldev.allin.ui.core.AllInPasswordField
import fr.iut.alldev.allin.ui.core.AllInTextField
import fr.iut.alldev.allin.ui.core.AllInAlertDialog
import fr.iut.alldev.allin.ui.register.components.RegisterScreenContent
@Composable
@ -39,6 +25,7 @@ fun RegisterScreen(
val loading by remember { registerViewModel.loading }
var hasNetworkError by remember { mutableStateOf(false) }
val usernameError by remember { registerViewModel.usernameError }
val emailError by remember { registerViewModel.emailError }
val passwordError by remember { registerViewModel.passwordError }
@ -58,10 +45,11 @@ fun RegisterScreen(
onDone = {
focusManager.clearFocus()
registerViewModel.onRegister(
usernameFieldName,
emailFieldName,
passwordFieldName,
navigateToDashboard
usernameFieldName = usernameFieldName,
emailFieldName = emailFieldName,
passwordFieldName = passwordFieldName,
navigateToDashboard = navigateToDashboard,
displayNetworkErrorAlert = { hasNetworkError = false }
)
},
onNext = {
@ -74,10 +62,11 @@ fun RegisterScreen(
navigateToLogin = navigateToLogin,
onRegister = {
registerViewModel.onRegister(
usernameFieldName,
emailFieldName,
passwordFieldName,
navigateToDashboard
usernameFieldName = usernameFieldName,
emailFieldName = emailFieldName,
passwordFieldName = passwordFieldName,
navigateToDashboard = navigateToDashboard,
displayNetworkErrorAlert = { hasNetworkError = false }
)
},
keyboardActions = keyboardActions,
@ -99,4 +88,11 @@ fun RegisterScreen(
setPasswordValidation = setPasswordValidation
)
AllInAlertDialog(
enabled = hasNetworkError,
title = stringResource(id = R.string.network_error),
text = stringResource(id = R.string.network_error_text),
onDismiss = { hasNetworkError = false }
)
}

@ -4,7 +4,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.api.interceptors.AllInAPIException
import fr.iut.alldev.allin.data.repository.UserRepository
import fr.iut.alldev.allin.ext.ALLOWED_SYMBOLS
import fr.iut.alldev.allin.ext.FieldErrorState
@ -14,6 +13,8 @@ import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import timber.log.Timber
import javax.inject.Inject
private const val MIN_PASSWORD_SIZE = 8
@ -83,7 +84,8 @@ class RegisterViewModel @Inject constructor(
usernameFieldName: String,
emailFieldName: String,
passwordFieldName: String,
navigateToDashboard: () -> Unit
navigateToDashboard: () -> Unit,
displayNetworkErrorAlert: () -> Unit
) {
viewModelScope.launch {
loading.value = true
@ -104,10 +106,19 @@ class RegisterViewModel @Inject constructor(
password.value
)
?.let { token -> keystoreManager.putToken(token) }
} catch (e: AllInAPIException) {
usernameError.value = FieldErrorState.AlreadyUsed(username.value)
emailError.value = FieldErrorState.AlreadyUsed(email.value)
hasError.value = true
} catch (e: Exception) {
when {
e is HttpException && e.code() == 409 -> {
usernameError.value = FieldErrorState.AlreadyUsed(username.value)
emailError.value = FieldErrorState.AlreadyUsed(email.value)
hasError.value = true
}
else -> {
Timber.e(e)
displayNetworkErrorAlert()
}
}
}
}
}

@ -28,6 +28,8 @@
<string name="Stake">Mise</string>
<string name="Possible_winnings">Gains possibles</string>
<string name="generic_error">Erreur</string>
<string name="network_error">Une erreur est survenue</string>
<string name="network_error_text">Assurez-vous d\'être bien connecté au réseau puis réessayez.</string>
<!--Drawer-->
<string name="bets">Bets</string>
@ -42,6 +44,8 @@
<string name="friends_subtitle">Défiez vos porches en les ajoutant en amis.</string>
<string name="current_bets">Bets en cours</string>
<string name="current_bets_subtitle">Gérez vos bets et récompensez les gagnants.</string>
<string name="ranking">Classement</string>
<string name="ranking_subtitle">Consultez votre classement parmi vos amis.</string>
<!--Welcome Page-->
<string name="welcome_title">Bienvenue sur</string>
<string name="welcome_subtitle">Récupère tes Allcoins et viens parier avec tes amis pour prouver qui est le meilleur.</string>

@ -30,6 +30,8 @@
<string name="Stake">Stake</string>
<string name="Possible_winnings">Possible winnings</string>
<string name="generic_error">Error</string>
<string name="network_error">An error occurred</string>
<string name="network_error_text">Make sure to be properly connected to the network then try again.</string>
<!--Drawer-->
<string name="bets">Bets</string>
@ -44,6 +46,8 @@
<string name="friends_subtitle">Challenge your folks by adding them as friends.</string>
<string name="current_bets">Current bets</string>
<string name="current_bets_subtitle">Manage your bets and reward the winners.</string>
<string name="ranking">Ranking</string>
<string name="ranking_subtitle">Browse your ranking with your friends.</string>
<!--Welcome Page-->
<string name="welcome_title">Welcome to</string>
<string name="welcome_appname" translatable="false">Allin.</string>

@ -4,7 +4,6 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.iut.alldev.allin.data.api.interceptors.ErrorInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import javax.inject.Singleton
@ -17,11 +16,10 @@ internal object DebugNetworkModule {
@Singleton
fun provideOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.addInterceptor(ErrorInterceptor())
.build()
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.build()
}

@ -1,7 +1,5 @@
package fr.iut.alldev.allin.data.api
import fr.iut.alldev.allin.data.api.interceptors.AllInAPIException
import fr.iut.alldev.allin.data.api.interceptors.AllInUnauthorizedException
import fr.iut.alldev.allin.data.api.model.CheckUser
import fr.iut.alldev.allin.data.api.model.RequestBet
import fr.iut.alldev.allin.data.api.model.RequestParticipation
@ -25,6 +23,8 @@ import java.time.Duration
import java.time.ZonedDateTime
import java.util.UUID
class MockAllInApiException(override val message: String?) : Exception()
class MockAllInApi : AllInApi {
init {
@ -71,12 +71,12 @@ class MockAllInApi : AllInApi {
override suspend fun login(body: CheckUser): ResponseUser {
return mockUsers.find { it.first.username == body.login && it.second == body.password }?.first
?: throw AllInAPIException("Invalid login/password.")
?: throw MockAllInApiException("Invalid login/password.")
}
override suspend fun login(token: String): ResponseUser {
return getUserFromToken(token)?.first
?: throw AllInAPIException("Invalid token")
?: throw MockAllInApiException("Invalid token")
}
override suspend fun register(body: RequestUser): ResponseUser {
@ -109,7 +109,7 @@ class MockAllInApi : AllInApi {
}
override suspend fun dailyGift(token: String): Int {
getUserFromToken(token) ?: throw AllInAPIException("Invalid login/password.")
getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
if (mockGifts[token] == null) {
mockGifts[token] = ZonedDateTime.now().minusDays(1)
}
@ -130,16 +130,16 @@ class MockAllInApi : AllInApi {
} else it
}
return amount
} else throw AllInUnauthorizedException("Gift already taken today")
} else throw MockAllInApiException("Gift already taken today")
}
override suspend fun getAllBets(token: String): List<ResponseBet> {
getUserFromToken(token) ?: throw AllInAPIException("Invalid login/password.")
getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
return mockBets
}
override suspend fun getToConfirm(token: String): List<ResponseBetDetail> {
val user = getUserFromToken(token) ?: throw AllInAPIException("Invalid login/password.")
val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
return mockBets.filter {
it.createdBy == user.first.username && it.status == BetStatus.CLOSING
}.map { bet ->
@ -156,8 +156,8 @@ class MockAllInApi : AllInApi {
}
override suspend fun confirmBet(token: String, id: String, value: String) {
getUserFromToken(token) ?: throw AllInAPIException("Invalid login/password.")
val bet = mockBets.find { it.id == id } ?: throw AllInAPIException("Unauthorized")
getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
val bet = mockBets.find { it.id == id } ?: throw MockAllInApiException("Unauthorized")
mockResults.add(
ResponseBetResult(
betId = id,
@ -168,8 +168,8 @@ class MockAllInApi : AllInApi {
}
override suspend fun getBet(token: String, id: String): ResponseBetDetail {
val bet = mockBets.find { it.id == id } ?: throw AllInAPIException("Bet not found")
val user = getUserFromToken(token) ?: throw AllInAPIException("Invalid login/password.")
val bet = mockBets.find { it.id == id } ?: throw MockAllInApiException("Bet not found")
val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
val betParticipations = mockParticipations.filter { it.betId == bet.id }
val userParticipation = betParticipations.find { it.username == user.first.username }
val wonParticipation = if (bet.status == BetStatus.FINISHED) {
@ -187,7 +187,7 @@ class MockAllInApi : AllInApi {
}
override suspend fun getBetCurrent(token: String): List<ResponseBetDetail> {
val user = getUserFromToken(token) ?: throw AllInAPIException("Invalid login/password.")
val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
val betParticipations = mockParticipations.filter { it.username == user.first.username }
return betParticipations.map { p ->
getBet(token, p.betId)
@ -197,7 +197,7 @@ class MockAllInApi : AllInApi {
}
override suspend fun getBetHistory(token: String): List<ResponseBetResultDetail> {
val user = getUserFromToken(token) ?: throw AllInAPIException("Invalid login/password.")
val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
val betParticipations = mockParticipations.filter { it.username == user.first.username }
return betParticipations.map { p ->
getBet(token, p.betId)
@ -252,7 +252,7 @@ class MockAllInApi : AllInApi {
)
)
} ?: throw AllInAPIException("Invalid token")
} ?: throw MockAllInApiException("Invalid token")
}
companion object {

@ -1,31 +0,0 @@
package fr.iut.alldev.allin.data.api.interceptors
import okhttp3.Interceptor
import okhttp3.Response
import okio.IOException
open class AllInAPIException(message: String) : IOException(message)
class AllInNotFoundException(message: String) : AllInAPIException(message)
class AllInUnauthorizedException(message: String) : AllInAPIException(message)
class AllInUnsuccessfulException(message: String) : AllInAPIException(message)
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (!response.isSuccessful) {
when (response.code) {
404 -> throw AllInNotFoundException(response.message)
401 -> throw AllInUnauthorizedException(response.message)
else -> throw AllInUnsuccessfulException(response.message)
}
}
response.body?.contentType()?.subtype?.takeIf { it != "json" }?.let {
throw AllInAPIException(response.message)
}
return response
}
}

@ -1,3 +1,3 @@
package fr.iut.alldev.allin.data.ext
fun Float.toPercentageString(precision: Int = 0) = String.format("%.${precision}f", this*100) + "%"
fun Float.toPercentageString(precision: Int = 0) = String.format("%.${precision}f", this * 100) + "%"

@ -4,7 +4,6 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.iut.alldev.allin.data.api.interceptors.ErrorInterceptor
import okhttp3.OkHttpClient
import javax.inject.Singleton
@ -16,6 +15,5 @@ internal object ReleaseNetworkModule {
@Singleton
fun provideOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(ErrorInterceptor())
.build()
}
Loading…
Cancel
Save