Automatic login with JWT and Logout
continuous-integration/drone/push Build is passing Details

pull/3/head
Arthur VALIN 1 year ago
parent 304508fa8c
commit b575f6e157

@ -7,11 +7,11 @@
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\Siph9\.android\avd\pixel_5_-_api_33.avd" />
<value value="C:\Users\Siph9\.android\avd\Pixel_4_XL_API_33.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-09-27T20:38:30.723570500Z" />
<timeTargetWasSelectedWithDropDown value="2024-01-09T09:46:59.648294500Z" />
</component>
</project>

@ -100,4 +100,7 @@ dependencies {
androidTestImplementation(libs.test.junit)
androidTestImplementation(libs.test.espresso)
androidTestImplementation(libs.test.androidx.junit)
androidTestImplementation(libs.hilt.androidTesting)
kaptAndroidTest(libs.hilt.androidCompiler)
}

@ -0,0 +1,17 @@
package fr.iut.alldev.allin.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import fr.iut.alldev.allin.keystore.impl.AllInKeystoreManagerImpl
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class KeystoreModule {
@Singleton
@Binds
abstract fun provideKeystoreManager(allInKeystoreManagerImpl: AllInKeystoreManagerImpl): AllInKeystoreManager
}

@ -0,0 +1,12 @@
package fr.iut.alldev.allin.keystore
import androidx.security.crypto.MasterKeys
abstract class AllInKeystoreManager {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
abstract fun createKeystore()
abstract fun putToken(token: String)
abstract fun getToken(): String?
abstract fun deleteToken()
}

@ -0,0 +1,42 @@
package fr.iut.alldev.allin.keystore.impl
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import dagger.hilt.android.qualifiers.ApplicationContext
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import javax.inject.Inject
private const val AUTH_TOKEN_KEY = "auth_token"
private const val PREFS_FILE_NAME = "secured_shared_prefs"
class AllInKeystoreManagerImpl @Inject constructor(
@ApplicationContext private val context: Context,
) : AllInKeystoreManager() {
private var sharedPreferences: SharedPreferences? = null
override fun createKeystore() {
if (sharedPreferences == null) {
sharedPreferences = EncryptedSharedPreferences.create(
PREFS_FILE_NAME,
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
override fun putToken(token: String) {
sharedPreferences?.edit()?.putString(AUTH_TOKEN_KEY, token)?.apply()
}
override fun getToken(): String? {
return sharedPreferences?.getString(AUTH_TOKEN_KEY, null)
}
override fun deleteToken() {
sharedPreferences?.edit()?.putString(AUTH_TOKEN_KEY, null)?.apply()
}
}

@ -32,6 +32,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.ext.formatToMediumDateNoYear
import fr.iut.alldev.allin.data.ext.formatToTime
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetFinishedStatus
import fr.iut.alldev.allin.data.model.bet.BetStatus
@ -50,7 +52,8 @@ private val bets = listOf(
endRegisterDate = ZonedDateTime.now(),
endBetDate = ZonedDateTime.now(),
isPublic = true,
betStatus = BetStatus.Waiting
betStatus = BetStatus.Waiting,
creator = "Lucas"
),
YesNoBet(
theme = "Études",
@ -58,7 +61,8 @@ private val bets = listOf(
endRegisterDate = ZonedDateTime.now(),
endBetDate = ZonedDateTime.now(),
isPublic = true,
betStatus = BetStatus.InProgress
betStatus = BetStatus.InProgress,
creator = "Lucas"
),
YesNoBet(
theme = "Études",
@ -66,7 +70,8 @@ private val bets = listOf(
endRegisterDate = ZonedDateTime.now(),
endBetDate = ZonedDateTime.now(),
isPublic = true,
betStatus = BetStatus.Finished(BetFinishedStatus.WON)
betStatus = BetStatus.Finished(BetFinishedStatus.WON),
creator = "Lucas"
),
MatchBet(
theme = "Études",
@ -76,7 +81,8 @@ private val bets = listOf(
isPublic = true,
betStatus = BetStatus.Waiting,
nameTeam1 = "Team 1",
nameTeam2 = "Team 2"
nameTeam2 = "Team 2",
creator = "Lucas"
),
)
@ -153,11 +159,11 @@ fun BetScreen(
}
items(bets) {
BetScreenCard(
creator = "Lucas",
category = "Études",
title = "Emre va réussir son TP de CI/CD mercredi?",
date = "11 Sept.",
time = "13:00",
creator = it.creator,
category = it.theme,
title = it.phrase,
date = it.endBetDate.formatToMediumDateNoYear(),
time = it.endBetDate.formatToTime(),
players = List(3) { null },
onClickParticipate = { selectBet(it, true) },
onClickCard = { selectBet(it, false) },

@ -24,6 +24,7 @@ import fr.iut.alldev.allin.ext.getIcon
import fr.iut.alldev.allin.ext.getTitleId
import fr.iut.alldev.allin.ui.betCreation.tabs.BetCreationScreenAnswerTab
import fr.iut.alldev.allin.ui.betCreation.tabs.BetCreationScreenQuestionTab
import fr.iut.alldev.allin.ui.core.AllInAlertDialog
import fr.iut.alldev.allin.ui.core.AllInDatePicker
import fr.iut.alldev.allin.ui.core.AllInSections
import fr.iut.alldev.allin.ui.core.AllInTimePicker
@ -42,6 +43,7 @@ fun BetCreationScreen(
) {
val interactionSource = remember { MutableInteractionSource() }
val betTypes = remember { BetType.values().toList() }
var hasError by remember { mutableStateOf(false) }
var theme by remember { viewModel.theme }
var phrase by remember { viewModel.phrase }
@ -151,8 +153,10 @@ fun BetCreationScreen(
themeFieldName = themeFieldName,
phraseFieldName = phraseFieldName,
registerDateFieldName = registerDateFieldName,
betDateFieldName = betDateFieldName
) { mainViewModel.loading.value = it }
betDateFieldName = betDateFieldName,
setLoading = { mainViewModel.loading.value = it },
onError = { hasError = true }
)
}
)
}
@ -182,6 +186,14 @@ fun BetCreationScreen(
}
)
}
AllInAlertDialog(
enabled = hasError,
title = stringResource(id = R.string.generic_error),
text = stringResource(id = R.string.bet_creation_error),
onDismiss = { hasError = false }
)
if (showRegisterTimePicker || showEndTimePicker) {
val timeToEdit = if (showRegisterTimePicker) registerDate else betDate
AllInTimePicker(

@ -4,11 +4,15 @@ 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.User
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
import fr.iut.alldev.allin.di.AllInCurrentUser
import fr.iut.alldev.allin.ext.FieldErrorState
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.ZonedDateTime
import javax.inject.Inject
@ -17,6 +21,7 @@ const val PHRASE_MIN_SIZE = 5
@HiltViewModel
class BetCreationViewModel @Inject constructor(
@AllInCurrentUser val currentUser: User,
private val betRepository: BetRepository,
) : ViewModel() {
@ -33,7 +38,7 @@ class BetCreationViewModel @Inject constructor(
val registerDateError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val betDateError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
private fun initErrorField(){
private fun initErrorField() {
themeError.value = FieldErrorState.NoError
phraseError.value = FieldErrorState.NoError
registerDateError.value = FieldErrorState.NoError
@ -46,30 +51,30 @@ class BetCreationViewModel @Inject constructor(
phraseFieldName: String,
registerDateFieldName: String,
betDateFieldName: String,
){
if(theme.value.length < THEME_MIN_SIZE){
) {
if (theme.value.length < THEME_MIN_SIZE) {
themeError.value =
FieldErrorState.TooShort(themeFieldName.lowercase(), THEME_MIN_SIZE)
hasError.value = true
}
if(phrase.value.length < PHRASE_MIN_SIZE){
if (phrase.value.length < PHRASE_MIN_SIZE) {
phraseError.value =
FieldErrorState.TooShort(phraseFieldName.lowercase(), PHRASE_MIN_SIZE)
hasError.value = true
}
if(registerDate.value <= ZonedDateTime.now()){
if (registerDate.value <= ZonedDateTime.now()) {
registerDateError.value =
FieldErrorState.PastDate(registerDateFieldName.lowercase())
hasError.value = true
}
if(betDate.value <= ZonedDateTime.now()){
if (betDate.value <= ZonedDateTime.now()) {
betDateError.value =
FieldErrorState.PastDate(betDateFieldName.lowercase())
hasError.value = true
}else if(betDate.value < registerDate.value){
} else if (betDate.value < registerDate.value) {
betDateError.value =
FieldErrorState.DateOrder(
registerDateFieldName.lowercase(),
@ -85,7 +90,8 @@ class BetCreationViewModel @Inject constructor(
phraseFieldName: String,
registerDateFieldName: String,
betDateFieldName: String,
setLoading: (Boolean)->Unit
onError: () -> Unit,
setLoading: (Boolean) -> Unit,
) {
viewModelScope.launch {
setLoading(true)
@ -98,7 +104,8 @@ class BetCreationViewModel @Inject constructor(
betDateFieldName,
)
if(!hasError.value){
if (!hasError.value) {
try {
val bet = BetFactory.createBet(
betType = selectedBetType.value,
theme = theme.value,
@ -108,9 +115,14 @@ class BetCreationViewModel @Inject constructor(
isPublic = isPublic.value,
nameTeam1 = "",
nameTeam2 = "",
possibleAnswers = setOf()
possibleAnswers = setOf(),
creator = currentUser.username
)
betRepository.createBet(bet)
} catch (e: AllInAPIException) {
Timber.e(e)
onError()
}
}
setLoading(false)
}

@ -52,7 +52,7 @@ fun BetHistoryScreen(
items(bets) {
BetHistoryScreenCard(
title = it.phrase,
creator = "creator",
creator = it.creator,
category = it.theme,
date = it.endBetDate.formatToMediumDateNoYear(),
time = it.endBetDate.formatToTime(),

@ -176,7 +176,8 @@ private fun YesNoBetPreview() {
endRegisterDate = ZonedDateTime.now(),
endBetDate = ZonedDateTime.now(),
isPublic = true,
betStatus = BetStatus.InProgress
betStatus = BetStatus.InProgress,
creator = "creator"
).toBetVO()?.Accept(
BetStatusBottomSheetDisplayBetVisitor(
userCoinAmount = coins,
@ -198,7 +199,8 @@ private fun YesNoBetFinishedPreview() {
endRegisterDate = ZonedDateTime.now(),
endBetDate = ZonedDateTime.now(),
isPublic = true,
betStatus = BetStatus.Finished(BetFinishedStatus.WON)
betStatus = BetStatus.Finished(BetFinishedStatus.WON),
creator = "creator"
).toBetVO()?.Accept(
BetStatusBottomSheetDisplayBetVisitor(
userCoinAmount = coins,
@ -222,7 +224,8 @@ private fun MatchBetPreview() {
isPublic = true,
betStatus = BetStatus.InProgress,
nameTeam1 = "Team 1",
nameTeam2 = "Team 2"
nameTeam2 = "Team 2",
creator = "creator"
).toBetVO()?.Accept(
BetStatusBottomSheetDisplayBetVisitor(
userCoinAmount = coins,

@ -6,6 +6,7 @@ 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
@ -13,7 +14,8 @@ import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository
private val userRepository: UserRepository,
private val keystoreManager: AllInKeystoreManager
) : ViewModel() {
var loading = mutableStateOf(false)
@ -22,19 +24,21 @@ class LoginViewModel @Inject constructor(
val username = mutableStateOf("")
val password = mutableStateOf("")
fun onLogin(
navigateToDashboard: ()->Unit
){
navigateToDashboard: () -> Unit
) {
viewModelScope.launch {
loading.value = true
withContext(Dispatchers.IO) {
try{
userRepository.login(username.value, password.value)
} catch (e: AllInAPIException){
try {
userRepository
.login(username.value, password.value)
?.let { token -> keystoreManager.putToken(token) }
} catch (e: AllInAPIException) {
hasError.value = true
}
}
if(!hasError.value){
if (!hasError.value) {
navigateToDashboard()
}
loading.value = false

@ -60,6 +60,7 @@ fun MainScreen(
drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed),
startDestination: String = Routes.PUBLIC_BETS,
mainViewModel: MainViewModel = hiltViewModel(),
navigateToWelcomeScreen: () -> Unit
) {
val loading by remember { mainViewModel.loading }
@ -105,6 +106,10 @@ fun MainScreen(
bestWin = 362,
navigateTo = { route ->
navController.popUpTo(route, startDestination)
},
logout = {
mainViewModel.deleteToken()
navigateToWelcomeScreen()
}
) {
AllInScaffold(

@ -8,18 +8,20 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.model.User
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.di.AllInCurrentUser
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
class UserState(val user: User){
class UserState(val user: User) {
val userCoins = mutableIntStateOf(user.coins)
}
@HiltViewModel
class MainViewModel @Inject constructor(
@AllInCurrentUser val currentUser: User
@AllInCurrentUser val currentUser: User,
private val keystoreManager: AllInKeystoreManager
) : ViewModel() {
var loading = mutableStateOf(false)
@ -27,9 +29,13 @@ class MainViewModel @Inject constructor(
val currentUserState = UserState(currentUser)
val selectedBet = mutableStateOf<Bet?>(null)
fun participateToBet(
stake: Int
){
fun deleteToken() {
viewModelScope.launch {
keystoreManager.deleteToken()
}
}
fun participateToBet(stake: Int) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
loading.value = true

@ -46,15 +46,12 @@ object NavArguments {
const val ARG_BET_HISTORY_IS_CURRENT = "ARG_BET_HISTORY_IS_CURRENT"
}
internal fun NavHostController.popUpTo(route: String, baseRoute: String) {
this.navigate(route) {
launchSingleTop = true
popUpTo(baseRoute) {
saveState = true
inclusive = true
}
restoreState = true
}
}
@ -67,21 +64,19 @@ fun AllInNavHost(
NavHost(
navController = navController,
startDestination = startDestination,
enterTransition =
{
if (navController.currentDestination?.route != Routes.DASHBOARD)
enterTransition = {
if (navController.currentDestination?.route != Routes.DASHBOARD &&
navController.currentDestination?.route != Routes.WELCOME
) {
slideInHorizontally(initialOffsetX = { it })
else
fadeIn(animationSpec = tween(1500))
} else fadeIn(animationSpec = tween(1500))
},
exitTransition =
{
if (navController.currentDestination?.route != Routes.DASHBOARD)
exitTransition = {
if (navController.currentDestination?.route != Routes.DASHBOARD &&
navController.currentDestination?.route != Routes.WELCOME
) {
slideOutHorizontally(targetOffsetX = { -it / 2 })
else
fadeOut(
animationSpec = tween(1500)
)
} else fadeOut(animationSpec = tween(1500))
},
modifier = modifier
.fillMaxSize()
@ -90,7 +85,7 @@ fun AllInNavHost(
allInWelcomeScreen(navController)
allInRegisterScreen(navController)
allInLoginScreen(navController)
allInDashboard()
allInDashboard(navController)
}
}
@ -149,14 +144,15 @@ private fun NavGraphBuilder.allInWelcomeScreen(
},
navigateToLogin = {
navController.popUpTo(Routes.LOGIN, Routes.WELCOME)
},
navigateToDashboard = {
navController.popUpTo(Routes.DASHBOARD, Routes.WELCOME)
}
)
}
}
private fun NavGraphBuilder.allInRegisterScreen(
navController: NavHostController,
) {
private fun NavGraphBuilder.allInRegisterScreen(navController: NavHostController) {
composable(route = Routes.REGISTER) {
RegisterScreen(
navigateToDashboard = {
@ -169,9 +165,7 @@ private fun NavGraphBuilder.allInRegisterScreen(
}
}
private fun NavGraphBuilder.allInLoginScreen(
navController: NavHostController,
) {
private fun NavGraphBuilder.allInLoginScreen(navController: NavHostController) {
composable(route = Routes.LOGIN) {
LoginScreen(
navigateToRegister = {
@ -184,10 +178,14 @@ private fun NavGraphBuilder.allInLoginScreen(
}
}
private fun NavGraphBuilder.allInDashboard() {
private fun NavGraphBuilder.allInDashboard(navController: NavHostController) {
composable(
route = Routes.DASHBOARD,
) {
MainScreen()
MainScreen(
navigateToWelcomeScreen = {
navController.popUpTo(Routes.WELCOME, Routes.DASHBOARD)
}
)
}
}

@ -11,6 +11,8 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -18,6 +20,8 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.navigation.TopLevelDestination
import fr.iut.alldev.allin.ui.navigation.drawer.components.DrawerCell
@ -35,7 +39,8 @@ fun AllInDrawer(
bestWin: Int,
nbFriends: Int,
navigateTo: (String) -> Unit,
content: @Composable () -> Unit,
logout: () -> Unit,
content: @Composable () -> Unit
) {
ModalNavigationDrawer(
drawerState = drawerState,
@ -63,6 +68,17 @@ fun AllInDrawer(
modifier = Modifier.padding(vertical = 5.dp, horizontal = 13.dp)
)
}
TextButton(
onClick = logout,
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text(
text = stringResource(id = R.string.Logout),
style = AllInTheme.typography.h3,
color = AllInTheme.colors.allInDarkGrey50,
fontSize = 16.sp
)
}
Box(
Modifier
.fillMaxSize()

@ -10,6 +10,7 @@ import fr.iut.alldev.allin.ext.ALLOWED_SYMBOLS
import fr.iut.alldev.allin.ext.FieldErrorState
import fr.iut.alldev.allin.ext.containsCharacter
import fr.iut.alldev.allin.ext.isEmail
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -20,11 +21,12 @@ private const val MIN_USERNAME_SIZE = 3
@HiltViewModel
class RegisterViewModel @Inject constructor(
private val userRepository: UserRepository
private val userRepository: UserRepository,
private val keystoreManager: AllInKeystoreManager
) : ViewModel() {
var loading = mutableStateOf(false)
var hasError = mutableStateOf(false)
private var hasError = mutableStateOf(false)
val username = mutableStateOf("")
val email = mutableStateOf("")
@ -36,78 +38,80 @@ class RegisterViewModel @Inject constructor(
val passwordError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val passwordValidationError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
private fun initErrorField(){
private fun initErrorField() {
usernameError.value = FieldErrorState.NoError
emailError.value = FieldErrorState.NoError
passwordError.value = FieldErrorState.NoError
passwordValidationError.value = FieldErrorState.NoError
hasError.value = false
}
private fun verifyField(
usernameFieldName:String,
emailFieldName:String,
passwordFieldName:String
){
if(username.value.length < MIN_USERNAME_SIZE){
usernameFieldName: String,
emailFieldName: String,
passwordFieldName: String
) {
if (username.value.length < MIN_USERNAME_SIZE) {
usernameError.value =
FieldErrorState.TooShort(usernameFieldName.lowercase(), MIN_USERNAME_SIZE)
hasError.value = true
}
if(password.value.length < MIN_PASSWORD_SIZE){
if (password.value.length < MIN_PASSWORD_SIZE) {
passwordError.value =
FieldErrorState.TooShort(passwordFieldName.lowercase(), MIN_PASSWORD_SIZE)
hasError.value = true
}else if(!password.value.containsCharacter(ALLOWED_SYMBOLS)){
} else if (!password.value.containsCharacter(ALLOWED_SYMBOLS)) {
passwordError.value =
FieldErrorState.NoSpecialCharacter(passwordFieldName.lowercase())
hasError.value = true
}
if(!email.value.isEmail()){
if (!email.value.isEmail()) {
emailError.value =
FieldErrorState.BadFormat(emailFieldName.lowercase(), "john@doe.com")
hasError.value = true
}
if(passwordValidation.value != password.value){
if (passwordValidation.value != password.value) {
passwordValidationError.value = FieldErrorState.NotIdentical
hasError.value = true
}
}
fun onRegister(
usernameFieldName:String,
usernameFieldName: String,
emailFieldName: String,
passwordFieldName:String,
navigateToDashboard: ()->Unit
){
passwordFieldName: String,
navigateToDashboard: () -> Unit
) {
viewModelScope.launch {
loading.value = true
withContext(Dispatchers.IO) {
initErrorField()
verifyField(
usernameFieldName,
emailFieldName,
passwordFieldName
)
if(!hasError.value) {
if (!hasError.value) {
try {
userRepository.register(
userRepository
.register(
username.value,
email.value,
password.value
)
}catch(e : AllInAPIException){
?.let { token -> keystoreManager.putToken(token) }
} catch (e: AllInAPIException) {
usernameError.value = FieldErrorState.AlreadyUsed(username.value)
emailError.value = FieldErrorState.AlreadyUsed(email.value)
hasError.value = true
}
}
}
if(!hasError.value){
if (!hasError.value) {
navigateToDashboard()
}
loading.value = false

@ -1,12 +1,23 @@
package fr.iut.alldev.allin.ui.welcome
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
@ -17,16 +28,25 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
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.AllInButton
import fr.iut.alldev.allin.ui.core.AllInLoading
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun WelcomeScreen(
navigateToRegister: () -> Unit,
navigateToLogin: () -> Unit,
navigateToDashboard: () -> Unit,
viewModel: WelcomeScreenViewModel = hiltViewModel()
) {
val loading by remember { viewModel.loading }
LaunchedEffect(viewModel) {
viewModel.tryAutoLogin(navigateToDashboard)
}
Box(
Modifier
.fillMaxWidth()
@ -102,6 +122,9 @@ fun WelcomeScreen(
}
}
}
AllInLoading(visible = loading)
}
@Preview
@ -109,6 +132,10 @@ fun WelcomeScreen(
@Composable
private fun WelcomeScreenPreview() {
AllInTheme {
WelcomeScreen(navigateToRegister = {}, navigateToLogin = {})
WelcomeScreen(
navigateToRegister = {},
navigateToLogin = {},
navigateToDashboard = {}
)
}
}

@ -0,0 +1,38 @@
package fr.iut.alldev.allin.ui.welcome
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.repository.UserRepository
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class WelcomeScreenViewModel @Inject constructor(
private val keystoreManager: AllInKeystoreManager,
private val userRepository: UserRepository
) : ViewModel() {
var loading = mutableStateOf(false)
fun tryAutoLogin(onSuccess: () -> Unit) {
viewModelScope.launch {
loading.value = true
keystoreManager.createKeystore()
keystoreManager.getToken()?.let { token ->
runCatching {
userRepository
.login(token)
?.let { newToken ->
keystoreManager.putToken(newToken)
}
onSuccess()
}
}
loading.value = false
}
}
}

@ -6,6 +6,7 @@
<string name="password">Mot de passe</string>
<string name="confirm_password">Confirmation du mot de passe</string>
<string name="Login">Se connecter</string>
<string name="Logout">Déconnexion</string>
<string name="already_have_account">Tu as déjà un compte ?</string>
<string name="forgot_password">Mot de passe oublié ?</string>
<string name="no_account">Pas encore inscrit ?</string>
@ -25,6 +26,7 @@
<string name="Details">Détails</string>
<string name="Stake">Mise</string>
<string name="Possible_winnings">Gains possibles</string>
<string name="generic_error">Erreur</string>
<!--Drawer-->
<string name="bets">Bets</string>
@ -79,6 +81,8 @@
<string name="yes_no_bottom_text_1">Les utilisateurs devront répondre au pari avec OUI ou NON.</string>
<string name="yes_no_bottom_text_2">Aucune autre réponse ne sera acceptée.</string>
<string name="custom_answers">Réponses personnalisées</string>
<string name="bet_creation_error">Erreur lors de la création du bet, veuillez rééssayer.</string>
<!--Bet Page-->
<string name="Popular">Populaire</string>
<string name="Public">Public</string>

@ -8,6 +8,7 @@
<string name="password">Password</string>
<string name="confirm_password">Confirm password</string>
<string name="Login">Login</string>
<string name="Logout">Logout</string>
<string name="already_have_account">Already have an account ?</string>
<string name="forgot_password">Forgot password ?</string>
<string name="no_account">Don\'t have an account ?</string>
@ -27,6 +28,7 @@
<string name="Details">Details</string>
<string name="Stake">Stake</string>
<string name="Possible_winnings">Possible winnings</string>
<string name="generic_error">Error</string>
<!--Drawer-->
<string name="bets">Bets</string>
@ -82,6 +84,8 @@
<string name="yes_no_bottom_text_2">No other answer will be accepted.</string>
<string name="sport_match">Sport match</string>
<string name="custom_answers">Custom answers</string>
<string name="bet_creation_error">Error while creating the bet. Please try again.</string>
<!--Bet Page-->
<string name="Popular">Popular</string>
<string name="Public">Public</string>
@ -107,6 +111,7 @@
<item quantity="one">%s point at stake</item>
<item quantity="other">%s points at stake</item>
</plurals>
<!--Bet status-->
<string name="bet_status_finished">Finished !</string>
<string name="bet_status_in_progress">In progress…</string>

@ -2,18 +2,23 @@ package fr.iut.alldev.allin.data.api
import fr.iut.alldev.allin.data.api.model.CheckUser
import fr.iut.alldev.allin.data.api.model.RequestUser
import fr.iut.alldev.allin.data.api.model.ResponseBet
import fr.iut.alldev.allin.data.api.model.ResponseUser
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface AllInApi {
@POST("users/login")
suspend fun login(
@Body body: CheckUser
): ResponseUser
suspend fun login(@Body body: CheckUser): ResponseUser
@POST("users/register")
suspend fun register(
@Body body: RequestUser
): ResponseUser
suspend fun register(@Body body: RequestUser): ResponseUser
@GET("users/token")
suspend fun login(@Header("Authorization") token: String): ResponseUser
@POST("bets/add")
suspend fun createBet(@Body body: ResponseBet)
}

@ -0,0 +1,48 @@
package fr.iut.alldev.allin.data.api.model
import androidx.annotation.Keep
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetStatus
import fr.iut.alldev.allin.data.model.bet.CustomBet
import fr.iut.alldev.allin.data.model.bet.YesNoBet
import fr.iut.alldev.allin.data.serialization.ZonedDateTimeSerializer
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
@Keep
@Serializable
data class ResponseBet(
val id: Int?,
val theme: String,
val sentenceBet: String,
@Serializable(ZonedDateTimeSerializer::class) val endRegistration: ZonedDateTime,
@Serializable(ZonedDateTimeSerializer::class) var endBet: ZonedDateTime,
var isPrivate: Boolean,
var response: List<String>,
val createdBy: String,
) {
fun toBet(): Bet {
if (response.toSet() == setOf("Yes", "No")) {
return YesNoBet(
theme = theme,
phrase = sentenceBet,
endRegisterDate = endRegistration,
endBetDate = endBet,
isPublic = !isPrivate,
betStatus = BetStatus.Waiting,
creator = createdBy
)
} else {
return CustomBet(
theme = theme,
phrase = sentenceBet,
endRegisterDate = endRegistration,
endBetDate = endBet,
isPublic = !isPrivate,
betStatus = BetStatus.Waiting,
creator = createdBy,
possibleAnswers = response.toSet()
)
}
}
}

@ -19,7 +19,8 @@ data class ResponseUser(
val username: String,
val email: String,
var nbCoins: Int,
){
var token: String? = null,
) {
fun toUser() = User(
username = username,
email = email,

@ -1,12 +1,29 @@
package fr.iut.alldev.allin.data.model.bet
import fr.iut.alldev.allin.data.api.model.ResponseBet
import java.time.ZonedDateTime
abstract class Bet(
open val id: Int? = null,
open val creator: String,
open val theme: String,
open val phrase: String,
open val endRegisterDate: ZonedDateTime,
open val endBetDate: ZonedDateTime,
open val isPublic: Boolean,
open val betStatus: BetStatus,
)
) {
abstract fun getResponses(): List<String>
fun toResponseBet(): ResponseBet {
return ResponseBet(
id = id,
theme = theme,
sentenceBet = phrase,
endRegistration = endRegisterDate,
endBet = endBetDate,
isPrivate = !isPublic,
response = getResponses(),
createdBy = creator
)
}
}

@ -6,6 +6,7 @@ class BetFactory {
companion object {
fun createBet(
betType: BetType,
creator: String,
theme: String,
phrase: String,
endRegisterDate: ZonedDateTime,
@ -20,6 +21,7 @@ class BetFactory {
BetType.YES_NO -> {
YesNoBet(
theme = theme,
creator = creator,
phrase = phrase,
endRegisterDate = endRegisterDate,
endBetDate = endBetDate,
@ -31,6 +33,7 @@ class BetFactory {
BetType.MATCH -> {
MatchBet(
theme = theme,
creator = creator,
phrase = phrase,
endRegisterDate = endRegisterDate,
endBetDate = endBetDate,
@ -45,6 +48,7 @@ class BetFactory {
BetType.CUSTOM -> {
CustomBet(
theme = theme,
creator = creator,
phrase = phrase,
endRegisterDate = endRegisterDate,
endBetDate = endBetDate,

@ -3,18 +3,24 @@ package fr.iut.alldev.allin.data.model.bet
import java.time.ZonedDateTime
data class CustomBet(
override val id: Int? = null,
override val creator: String,
override val theme: String,
override val phrase: String,
override val endRegisterDate: ZonedDateTime,
override val endBetDate: ZonedDateTime,
override val isPublic: Boolean,
override val betStatus: BetStatus,
val possibleAnswers: Set<String>
val possibleAnswers: Set<String>,
) : Bet(
id,
creator,
theme,
phrase,
endRegisterDate,
endBetDate,
isPublic,
betStatus
)
) {
override fun getResponses(): List<String> = possibleAnswers.toList()
}

@ -3,6 +3,8 @@ package fr.iut.alldev.allin.data.model.bet
import java.time.ZonedDateTime
data class MatchBet(
override val id: Int? = null,
override val creator: String,
override val theme: String,
override val phrase: String,
override val endRegisterDate: ZonedDateTime,
@ -10,12 +12,17 @@ data class MatchBet(
override val isPublic: Boolean,
override val betStatus: BetStatus,
val nameTeam1: String,
val nameTeam2: String
val nameTeam2: String,
) : Bet(
id,
creator,
theme,
phrase,
endRegisterDate,
endBetDate,
isPublic,
betStatus
)
) {
override fun getResponses(): List<String> = listOf(nameTeam1, nameTeam2)
}

@ -3,17 +3,23 @@ package fr.iut.alldev.allin.data.model.bet
import java.time.ZonedDateTime
data class YesNoBet(
override val id: Int? = null,
override val creator: String,
override val theme: String,
override val phrase: String,
override val endRegisterDate: ZonedDateTime,
override val endBetDate: ZonedDateTime,
override val isPublic: Boolean,
override val betStatus: BetStatus
override val betStatus: BetStatus,
) : Bet(
id,
creator,
theme,
phrase,
endRegisterDate,
endBetDate,
isPublic,
betStatus
)
) {
override fun getResponses(): List<String> = listOf("Yes", "No")
}

@ -4,10 +4,7 @@ import fr.iut.alldev.allin.data.model.bet.Bet
import kotlinx.coroutines.flow.Flow
abstract class BetRepository {
abstract suspend fun createBet(
bet: Bet,
)
abstract suspend fun createBet(bet: Bet)
abstract suspend fun getHistory(): Flow<List<Bet>>
abstract suspend fun getCurrentBets(): Flow<List<Bet>>
}

@ -7,10 +7,13 @@ abstract class UserRepository {
abstract suspend fun login(
username: String,
password: String
)
): String?
abstract suspend fun login(token: String): String?
abstract suspend fun register(
username: String,
email: String,
password: String
)
): String?
}

@ -8,24 +8,22 @@ import fr.iut.alldev.allin.data.model.bet.YesNoBet
import fr.iut.alldev.allin.data.repository.BetRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import timber.log.Timber
import java.time.ZonedDateTime
import javax.inject.Inject
class BetRepositoryImpl @Inject constructor(
private val api: AllInApi,
private val api: AllInApi
) : BetRepository() {
override suspend fun createBet(bet: Bet) {
// TODO
Timber.d("$bet")
api.createBet(bet.toResponseBet())
}
override suspend fun getHistory(): Flow<List<Bet>> {
// TODO
return flowOf(
listOf(
YesNoBet(
creator = "Lucas",
theme = "Theme",
phrase = "Bet phrase 1",
endRegisterDate = ZonedDateTime.now().minusDays(4),
@ -34,6 +32,7 @@ class BetRepositoryImpl @Inject constructor(
betStatus = BetStatus.Finished(BetFinishedStatus.WON)
),
YesNoBet(
creator = "Lucas",
theme = "Theme",
phrase = "Bet phrase 2",
endRegisterDate = ZonedDateTime.now().minusDays(3),
@ -42,6 +41,7 @@ class BetRepositoryImpl @Inject constructor(
betStatus = BetStatus.Finished(BetFinishedStatus.LOST)
),
YesNoBet(
creator = "Lucas",
theme = "Theme",
phrase = "Bet phrase 3",
endRegisterDate = ZonedDateTime.now().minusDays(15),
@ -58,6 +58,7 @@ class BetRepositoryImpl @Inject constructor(
return flowOf(
listOf(
YesNoBet(
creator = "Lucas",
theme = "Theme",
phrase = "Bet phrase 1",
endRegisterDate = ZonedDateTime.now().plusDays(5),
@ -66,6 +67,7 @@ class BetRepositoryImpl @Inject constructor(
betStatus = BetStatus.InProgress
),
YesNoBet(
creator = "Lucas",
theme = "Theme",
phrase = "Bet phrase 2",
endRegisterDate = ZonedDateTime.now().plusDays(1),
@ -74,6 +76,7 @@ class BetRepositoryImpl @Inject constructor(
betStatus = BetStatus.InProgress
),
YesNoBet(
creator = "Lucas",
theme = "Theme",
phrase = "Bet phrase 3",
endRegisterDate = ZonedDateTime.now().plusDays(3),

@ -10,25 +10,34 @@ class UserRepositoryImpl @Inject constructor(
private val api: AllInApi,
) : UserRepository() {
override suspend fun login(username: String, password: String) {
currentUser = api.login(
override suspend fun login(username: String, password: String): String? {
val response = api.login(
CheckUser(
login = username,
password = password
)
).toUser()
)
currentUser = response.toUser()
return response.token
}
override suspend fun login(token: String): String? {
val response = api.login(token = "Bearer $token")
currentUser = response.toUser()
return response.token
}
override suspend fun register(username: String, email: String, password: String) {
currentUser = api.register(
override suspend fun register(username: String, email: String, password: String): String? {
val response = api.register(
RequestUser(
username = username,
email = email,
password = password,
nbCoins = 0
)
).toUser()
)
currentUser = response.toUser()
return response.token
}
}

@ -0,0 +1,25 @@
package fr.iut.alldev.allin.data.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
object ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("ZonedDateTime", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: ZonedDateTime) {
encoder.encodeLong(value.toEpochSecond())
}
override fun deserialize(decoder: Decoder): ZonedDateTime {
val epoch = decoder.decodeLong()
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epoch), ZoneId.systemDefault())
}
}

@ -10,6 +10,7 @@ kotlin = "1.9.20"
androidxCore = "1.12.0"
androidxActivity = "1.8.2"
androidxSecurity = "1.0.0"
composeBom = "2023.10.01"
composePreview = "1.6.0-beta03"
@ -39,6 +40,7 @@ resgenPlugin = "2.5"
# Android
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
androidx-security = { module = "androidx.security:security-crypto", version.ref = "androidxSecurity" }
# Lifecycle
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
@ -50,7 +52,8 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru
test-junit = { group = "junit", name = "junit", version.ref = "junit" }
test-androidx-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExtJunit" }
test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
hilt-androidTesting = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-androidCompiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
# Compose
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
@ -95,6 +98,6 @@ plugin-hilt = { module = "com.google.dagger:hilt-android-gradle-plugin", version
[bundles]
android = ["androidx-core", "androidx-activity"]
android = ["androidx-core", "androidx-activity", "androidx-security"]
androidx-lifecycle = ["androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", "androidx-lifecycle-process", "androidx-lifecycle-runtime-compose"]
compose = ["compose-ui", "compose-ui-graphics", "compose-tooling-preview", "compose-ui-tooling", "compose-foundation", "compose-material", "compose-material3", "compose-material-icons", "compose-material-icons-extended", "compose-navigation", "compose-ui-googlefonts"]

Loading…
Cancel
Save