Merge pull request #49 from SELab1/firebaseIntegration
Firebase integration
This commit is contained in:
		
						commit
						227473895a
					
				
					 27 changed files with 623 additions and 23 deletions
				
			
		|  | @ -3,6 +3,7 @@ | ||||||
|     xmlns:tools="http://schemas.android.com/tools"> |     xmlns:tools="http://schemas.android.com/tools"> | ||||||
| 
 | 
 | ||||||
|     <application |     <application | ||||||
|  |         android:name=".StudeezHiltApp" | ||||||
|         android:allowBackup="true" |         android:allowBackup="true" | ||||||
|         android:dataExtractionRules="@xml/data_extraction_rules" |         android:dataExtractionRules="@xml/data_extraction_rules" | ||||||
|         android:fullBackupContent="@xml/backup_rules" |         android:fullBackupContent="@xml/backup_rules" | ||||||
|  |  | ||||||
							
								
								
									
										81
									
								
								app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | ||||||
|  | package be.ugent.sel.studeez | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.material.* | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.ReadOnlyComposable | ||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.runtime.rememberCoroutineScope | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.platform.LocalConfiguration | ||||||
|  | import androidx.compose.ui.platform.LocalContext | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import androidx.navigation.NavGraphBuilder | ||||||
|  | import androidx.navigation.NavHostController | ||||||
|  | import androidx.navigation.compose.NavHost | ||||||
|  | import androidx.navigation.compose.composable | ||||||
|  | import androidx.navigation.compose.rememberNavController | ||||||
|  | import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||||
|  | import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||||
|  | import be.ugent.sel.studeez.screens.splash.SplashScreen | ||||||
|  | import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun StudeezApp() { | ||||||
|  |     StudeezTheme { | ||||||
|  |         Surface(color = MaterialTheme.colors.background) { | ||||||
|  |             val appState = rememberAppState() | ||||||
|  | 
 | ||||||
|  |             Scaffold( | ||||||
|  |                 snackbarHost = { | ||||||
|  |                     SnackbarHost( | ||||||
|  |                         hostState = it, | ||||||
|  |                         modifier = Modifier.padding(8.dp), | ||||||
|  |                         snackbar = { snackbarData -> | ||||||
|  |                             Snackbar(snackbarData, contentColor = MaterialTheme.colors.onPrimary) | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |                 }, | ||||||
|  |                 scaffoldState = appState.scaffoldState | ||||||
|  |             ) { innerPaddingModifier -> | ||||||
|  |                 NavHost( | ||||||
|  |                     navController = appState.navController, | ||||||
|  |                     startDestination = StudeezDestinations.SPLASH_SCREEN, | ||||||
|  |                     modifier = Modifier.padding(innerPaddingModifier) | ||||||
|  |                 ) { | ||||||
|  |                     studeezGraph(appState) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun rememberAppState( | ||||||
|  |     scaffoldState: ScaffoldState = rememberScaffoldState(), | ||||||
|  |     navController: NavHostController = rememberNavController(), | ||||||
|  |     snackbarManager: SnackbarManager = SnackbarManager, | ||||||
|  |     resources: Resources = resources(), | ||||||
|  |     coroutineScope: CoroutineScope = rememberCoroutineScope() | ||||||
|  | ) = | ||||||
|  |     remember(scaffoldState, navController, snackbarManager, resources, coroutineScope) { | ||||||
|  |         StudeezAppstate(scaffoldState, navController, snackbarManager, resources, coroutineScope) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | @ReadOnlyComposable | ||||||
|  | fun resources(): Resources { | ||||||
|  |     LocalConfiguration.current | ||||||
|  |     return LocalContext.current.resources | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) { | ||||||
|  | 
 | ||||||
|  |     composable(StudeezDestinations.SPLASH_SCREEN) { | ||||||
|  |         SplashScreen(openAndPopUp = { route, popUp -> appState.navigateAndPopUp(route, popUp) }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								app/src/main/java/be/ugent/sel/studeez/StudeezAppstate.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/src/main/java/be/ugent/sel/studeez/StudeezAppstate.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | package be.ugent.sel.studeez | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources | ||||||
|  | import androidx.compose.material.ScaffoldState | ||||||
|  | import androidx.compose.runtime.Stable | ||||||
|  | import androidx.navigation.NavHostController | ||||||
|  | import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||||
|  | import be.ugent.sel.studeez.common.snackbar.SnackbarMessage.Companion.toMessage | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.flow.filterNotNull | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | 
 | ||||||
|  | @Stable | ||||||
|  | class StudeezAppstate( | ||||||
|  |     val scaffoldState: ScaffoldState, | ||||||
|  |     val navController: NavHostController, | ||||||
|  |     private val snackbarManager: SnackbarManager, | ||||||
|  |     private val resources: Resources, | ||||||
|  |     coroutineScope: CoroutineScope | ||||||
|  | ) { | ||||||
|  |     init { | ||||||
|  |         coroutineScope.launch { | ||||||
|  |             snackbarManager.snackbarMessages.filterNotNull().collect { snackbarMessage -> | ||||||
|  |                 val text = snackbarMessage.toMessage(resources) | ||||||
|  |                 scaffoldState.snackbarHostState.showSnackbar(text) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun popUp() { | ||||||
|  |         navController.popBackStack() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun navigate(route: String) { | ||||||
|  |         navController.navigate(route) { launchSingleTop = true } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun navigateAndPopUp(route: String, popUp: String) { | ||||||
|  |         navController.navigate(route) { | ||||||
|  |             launchSingleTop = true | ||||||
|  |             popUpTo(popUp) { inclusive = true } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun clearAndNavigate(route: String) { | ||||||
|  |         navController.navigate(route) { | ||||||
|  |             launchSingleTop = true | ||||||
|  |             popUpTo(0) { inclusive = true } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								app/src/main/java/be/ugent/sel/studeez/StudeezHiltApp.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/src/main/java/be/ugent/sel/studeez/StudeezHiltApp.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | package be.ugent.sel.studeez | ||||||
|  | 
 | ||||||
|  | import android.app.Application | ||||||
|  | import dagger.hilt.android.HiltAndroidApp | ||||||
|  | 
 | ||||||
|  | @HiltAndroidApp | ||||||
|  | class StudeezHiltApp : Application() | ||||||
|  | @ -10,8 +10,11 @@ import androidx.compose.material.Text | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import be.ugent.sel.studeez.StudeezApp | ||||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||||
|  | import dagger.hilt.android.AndroidEntryPoint | ||||||
| 
 | 
 | ||||||
|  | @AndroidEntryPoint | ||||||
| class MainActivity : ComponentActivity() { | class MainActivity : ComponentActivity() { | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
|  | @ -22,7 +25,7 @@ class MainActivity : ComponentActivity() { | ||||||
|                     modifier = Modifier.fillMaxSize(), |                     modifier = Modifier.fillMaxSize(), | ||||||
|                     color = MaterialTheme.colors.background |                     color = MaterialTheme.colors.background | ||||||
|                 ) { |                 ) { | ||||||
|                     Greeting("Android") |                     StudeezApp() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | package be.ugent.sel.studeez.common.composable | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.StringRes | ||||||
|  | import androidx.compose.material.* | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.compose.ui.unit.sp | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { | ||||||
|  |     TextButton(onClick = action, modifier = modifier) { Text(text = stringResource(text)) } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun BasicButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { | ||||||
|  |     Button( | ||||||
|  |         onClick = action, | ||||||
|  |         modifier = modifier | ||||||
|  |     ) { | ||||||
|  |         Text(text = stringResource(text), fontSize = 16.sp) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun DialogConfirmButton(@StringRes text: Int, action: () -> Unit) { | ||||||
|  |     Button( | ||||||
|  |         onClick = action | ||||||
|  |     ) { | ||||||
|  |         Text(text = stringResource(text)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun DialogCancelButton(@StringRes text: Int, action: () -> Unit) { | ||||||
|  |     Button( | ||||||
|  |         onClick = action, | ||||||
|  |         colors = | ||||||
|  |         ButtonDefaults.buttonColors( | ||||||
|  |             backgroundColor = MaterialTheme.colors.onPrimary, | ||||||
|  |             contentColor = MaterialTheme.colors.primary | ||||||
|  |         ) | ||||||
|  |     ) { | ||||||
|  |         Text(text = stringResource(text)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,102 @@ | ||||||
|  | package be.ugent.sel.studeez.common.composable | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.StringRes | ||||||
|  | import androidx.compose.foundation.text.KeyboardOptions | ||||||
|  | import androidx.compose.material.Icon | ||||||
|  | import androidx.compose.material.IconButton | ||||||
|  | import androidx.compose.material.OutlinedTextField | ||||||
|  | import androidx.compose.material.Text | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.Email | ||||||
|  | import androidx.compose.material.icons.filled.Lock | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import be.ugent.sel.studeez.R.string as AppText | ||||||
|  | import be.ugent.sel.studeez.R.drawable as AppIcon | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.res.painterResource | ||||||
|  | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.compose.ui.text.input.KeyboardType | ||||||
|  | import androidx.compose.ui.text.input.PasswordVisualTransformation | ||||||
|  | import androidx.compose.ui.text.input.VisualTransformation | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun BasicField( | ||||||
|  |     @StringRes text: Int, | ||||||
|  |     value: String, | ||||||
|  |     onNewValue: (String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier | ||||||
|  | ) { | ||||||
|  |     OutlinedTextField( | ||||||
|  |         singleLine = true, | ||||||
|  |         modifier = modifier, | ||||||
|  |         value = value, | ||||||
|  |         onValueChange = { onNewValue(it) }, | ||||||
|  |         placeholder = { Text(stringResource(text)) } | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun EmailField( | ||||||
|  |     value: String, | ||||||
|  |     onNewValue: (String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier | ||||||
|  | ) { | ||||||
|  |     OutlinedTextField( | ||||||
|  |         singleLine = true, | ||||||
|  |         modifier = modifier, | ||||||
|  |         value = value, | ||||||
|  |         onValueChange = { onNewValue(it) }, | ||||||
|  |         placeholder = { Text(stringResource(AppText.email)) }, | ||||||
|  |         leadingIcon = { Icon(imageVector = Icons.Default.Email, contentDescription = "Email") } | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun PasswordField( | ||||||
|  |     value: String, | ||||||
|  |     onNewValue: (String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier | ||||||
|  | ) { | ||||||
|  |     PasswordField(value, AppText.password, onNewValue, modifier) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun RepeatPasswordField( | ||||||
|  |     value: String, | ||||||
|  |     onNewValue: (String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier | ||||||
|  | ) { | ||||||
|  |     PasswordField(value, AppText.repeat_password, onNewValue, modifier) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | private fun PasswordField( | ||||||
|  |     value: String, | ||||||
|  |     @StringRes placeholder: Int, | ||||||
|  |     onNewValue: (String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier | ||||||
|  | ) { | ||||||
|  |     var isVisible by remember { mutableStateOf(false) } | ||||||
|  | 
 | ||||||
|  |     val icon = | ||||||
|  |         if (isVisible) painterResource(AppIcon.ic_visibility_on) | ||||||
|  |         else painterResource(AppIcon.ic_visibility_off) | ||||||
|  | 
 | ||||||
|  |     val visualTransformation = | ||||||
|  |         if (isVisible) VisualTransformation.None else PasswordVisualTransformation() | ||||||
|  | 
 | ||||||
|  |     OutlinedTextField( | ||||||
|  |         modifier = modifier, | ||||||
|  |         value = value, | ||||||
|  |         onValueChange = { onNewValue(it) }, | ||||||
|  |         placeholder = { Text(text = stringResource(placeholder)) }, | ||||||
|  |         leadingIcon = { Icon(imageVector = Icons.Default.Lock, contentDescription = "Lock") }, | ||||||
|  |         trailingIcon = { | ||||||
|  |             IconButton(onClick = { isVisible = !isVisible }) { | ||||||
|  |                 Icon(painter = icon, contentDescription = "Visibility") | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), | ||||||
|  |         visualTransformation = visualTransformation | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | package be.ugent.sel.studeez.common.ext | ||||||
|  | 
 | ||||||
|  | import androidx.compose.foundation.layout.* | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | 
 | ||||||
|  | fun Modifier.textButton(): Modifier { | ||||||
|  |     return this.fillMaxWidth().padding(16.dp, 8.dp, 16.dp, 0.dp) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.basicButton(): Modifier { | ||||||
|  |     return this.fillMaxWidth().padding(16.dp, 8.dp) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.card(): Modifier { | ||||||
|  |     return this.padding(16.dp, 0.dp, 16.dp, 8.dp) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.contextMenu(): Modifier { | ||||||
|  |     return this.wrapContentWidth() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.dropdownSelector(): Modifier { | ||||||
|  |     return this.fillMaxWidth() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.fieldModifier(): Modifier { | ||||||
|  |     return this.fillMaxWidth().padding(16.dp, 4.dp) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.toolbarActions(): Modifier { | ||||||
|  |     return this.wrapContentSize(Alignment.TopEnd) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.spacer(): Modifier { | ||||||
|  |     return this.fillMaxWidth().padding(12.dp) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Modifier.smallSpacer(): Modifier { | ||||||
|  |     return this.fillMaxWidth().height(8.dp) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | package be.ugent.sel.studeez.common.ext | ||||||
|  | 
 | ||||||
|  | import android.util.Patterns | ||||||
|  | import java.util.regex.Pattern | ||||||
|  | 
 | ||||||
|  | private const val MIN_PASS_LENGTH = 6 | ||||||
|  | private const val PASS_PATTERN = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=\\S+$).{4,}$" | ||||||
|  | 
 | ||||||
|  | fun String.isValidEmail(): Boolean { | ||||||
|  |     return this.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(this).matches() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun String.isValidPassword(): Boolean { | ||||||
|  |     return this.isNotBlank() && | ||||||
|  |             this.length >= MIN_PASS_LENGTH && | ||||||
|  |             Pattern.compile(PASS_PATTERN).matcher(this).matches() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun String.passwordMatches(repeated: String): Boolean { | ||||||
|  |     return this == repeated | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun String.idFromParameter(): String { | ||||||
|  |     return this.substring(1, this.length - 1) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,20 @@ | ||||||
|  | package be.ugent.sel.studeez.common.snackbar | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.StringRes | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | 
 | ||||||
|  | object SnackbarManager { | ||||||
|  |     private val messages: MutableStateFlow<SnackbarMessage?> = MutableStateFlow(null) | ||||||
|  |     val snackbarMessages: StateFlow<SnackbarMessage?> | ||||||
|  |         get() = messages.asStateFlow() | ||||||
|  | 
 | ||||||
|  |     fun showMessage(@StringRes message: Int) { | ||||||
|  |         messages.value = SnackbarMessage.ResourceSnackbar(message) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun showMessage(message: SnackbarMessage) { | ||||||
|  |         messages.value = message | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | package be.ugent.sel.studeez.common.snackbar | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources | ||||||
|  | import androidx.annotation.StringRes | ||||||
|  | import be.ugent.sel.studeez.R.string as AppText | ||||||
|  | 
 | ||||||
|  | sealed class SnackbarMessage { | ||||||
|  |     class StringSnackbar(val message: String) : SnackbarMessage() | ||||||
|  |     class ResourceSnackbar(@StringRes val message: Int) : SnackbarMessage() | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         fun SnackbarMessage.toMessage(resources: Resources): String { | ||||||
|  |             return when (this) { | ||||||
|  |                 is StringSnackbar -> this.message | ||||||
|  |                 is ResourceSnackbar -> resources.getString(this.message) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         fun Throwable.toSnackbarMessage(): SnackbarMessage { | ||||||
|  |             val message = this.message.orEmpty() | ||||||
|  |             return if (message.isNotBlank()) StringSnackbar(message) | ||||||
|  |             else ResourceSnackbar(AppText.generic_error) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/be/ugent/sel/studeez/di/DatabaseModule.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | package be.ugent.sel.studeez.di | ||||||
|  | 
 | ||||||
|  | import be.ugent.sel.studeez.domain.AccountDAO | ||||||
|  | import be.ugent.sel.studeez.domain.LogService | ||||||
|  | import be.ugent.sel.studeez.domain.implementation.FirebaseAccountDAO | ||||||
|  | import be.ugent.sel.studeez.domain.implementation.LogServiceImpl | ||||||
|  | import dagger.Binds | ||||||
|  | import dagger.Module | ||||||
|  | import dagger.hilt.InstallIn | ||||||
|  | import dagger.hilt.components.SingletonComponent | ||||||
|  | 
 | ||||||
|  | @Module | ||||||
|  | @InstallIn(SingletonComponent::class) | ||||||
|  | abstract class DatabaseModule { | ||||||
|  |     @Binds abstract fun provideAccountDAO(impl: FirebaseAccountDAO): AccountDAO | ||||||
|  | 
 | ||||||
|  |     @Binds abstract fun provideLogService(impl: LogServiceImpl): LogService | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								app/src/main/java/be/ugent/sel/studeez/di/FireBaseModule.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/src/main/java/be/ugent/sel/studeez/di/FireBaseModule.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | package be.ugent.sel.studeez.di | ||||||
|  | 
 | ||||||
|  | import com.google.firebase.auth.FirebaseAuth | ||||||
|  | import com.google.firebase.auth.ktx.auth | ||||||
|  | import com.google.firebase.firestore.FirebaseFirestore | ||||||
|  | import com.google.firebase.firestore.ktx.firestore | ||||||
|  | import com.google.firebase.ktx.Firebase | ||||||
|  | import dagger.Module | ||||||
|  | import dagger.Provides | ||||||
|  | import dagger.hilt.InstallIn | ||||||
|  | import dagger.hilt.components.SingletonComponent | ||||||
|  | 
 | ||||||
|  | @Module | ||||||
|  | @InstallIn(SingletonComponent::class) | ||||||
|  | object FirebaseModule { | ||||||
|  |     @Provides | ||||||
|  |     fun auth(): FirebaseAuth = Firebase.auth | ||||||
|  | 
 | ||||||
|  |     @Provides | ||||||
|  |     fun firestore(): FirebaseFirestore = Firebase.firestore | ||||||
|  | } | ||||||
|  | @ -25,10 +25,9 @@ interface AccountDAO { | ||||||
| 
 | 
 | ||||||
|     val currentUser: Flow<User> |     val currentUser: Flow<User> | ||||||
| 
 | 
 | ||||||
|     suspend fun authenticate(email: String, password: String) |     suspend fun signInWithEmailAndPassword(email: String, password: String) | ||||||
|     suspend fun sendRecoveryEmail(email: String) |     suspend fun sendRecoveryEmail(email: String) | ||||||
|     suspend fun createAnonymousAccount() |     suspend fun signUpWithEmailAndPassword(email: String, password: String) | ||||||
|     suspend fun linkAccount(email: String, password: String) |  | ||||||
|     suspend fun deleteAccount() |     suspend fun deleteAccount() | ||||||
|     suspend fun signOut() |     suspend fun signOut() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | package be.ugent.sel.studeez.domain | ||||||
|  | 
 | ||||||
|  | interface LogService { | ||||||
|  |     fun logNonFatalCrash(throwable: Throwable) | ||||||
|  | } | ||||||
|  | @ -47,7 +47,7 @@ class FirebaseAccountDAO @Inject constructor( | ||||||
|             awaitClose { auth.removeAuthStateListener(listener) } |             awaitClose { auth.removeAuthStateListener(listener) } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     override suspend fun authenticate(email: String, password: String) { |     override suspend fun signInWithEmailAndPassword(email: String, password: String) { | ||||||
|         auth.signInWithEmailAndPassword(email, password).await() |         auth.signInWithEmailAndPassword(email, password).await() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -55,31 +55,15 @@ class FirebaseAccountDAO @Inject constructor( | ||||||
|         auth.sendPasswordResetEmail(email).await() |         auth.sendPasswordResetEmail(email).await() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun createAnonymousAccount() { |     override suspend fun signUpWithEmailAndPassword(email: String, password: String) { | ||||||
|         auth.signInAnonymously().await() |         auth.createUserWithEmailAndPassword(email, password).await() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun linkAccount(email: String, password: String): Unit = |  | ||||||
|         trace(LINK_ACCOUNT_TRACE) { |  | ||||||
|             val credential = EmailAuthProvider.getCredential(email, password) |  | ||||||
|             auth.currentUser!!.linkWithCredential(credential).await() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     override suspend fun deleteAccount() { |     override suspend fun deleteAccount() { | ||||||
|         auth.currentUser!!.delete().await() |         auth.currentUser!!.delete().await() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun signOut() { |     override suspend fun signOut() { | ||||||
|         if (auth.currentUser!!.isAnonymous) { |  | ||||||
|             auth.currentUser!!.delete() |  | ||||||
|         } |  | ||||||
|         auth.signOut() |         auth.signOut() | ||||||
| 
 |  | ||||||
|         // Sign the user back in anonymously. |  | ||||||
|         createAnonymousAccount() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         private const val LINK_ACCOUNT_TRACE = "linkAccount" |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | package be.ugent.sel.studeez.domain.implementation | ||||||
|  | 
 | ||||||
|  | import be.ugent.sel.studeez.domain.LogService | ||||||
|  | import com.google.firebase.crashlytics.ktx.crashlytics | ||||||
|  | import com.google.firebase.ktx.Firebase | ||||||
|  | import javax.inject.Inject | ||||||
|  | 
 | ||||||
|  | class LogServiceImpl @Inject constructor() : LogService { | ||||||
|  |     override fun logNonFatalCrash(throwable: Throwable) { | ||||||
|  |         Firebase.crashlytics.recordException(throwable) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,8 @@ | ||||||
|  | package be.ugent.sel.studeez.navigation | ||||||
|  | 
 | ||||||
|  | object StudeezDestinations { | ||||||
|  |     const val SPLASH_SCREEN = "splash" | ||||||
|  |     const val SIGN_UP_SCREEN = "signup" | ||||||
|  |     const val LOGIN_SCREEN = "login" | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | package be.ugent.sel.studeez.screens | ||||||
|  | 
 | ||||||
|  | import androidx.lifecycle.ViewModel | ||||||
|  | import androidx.lifecycle.viewModelScope | ||||||
|  | import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||||
|  | import be.ugent.sel.studeez.common.snackbar.SnackbarMessage.Companion.toSnackbarMessage | ||||||
|  | import be.ugent.sel.studeez.domain.LogService | ||||||
|  | import kotlinx.coroutines.CoroutineExceptionHandler | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | 
 | ||||||
|  | open class StudeezViewModel(private val logService: LogService) : ViewModel() { | ||||||
|  |     fun launchCatching(snackbar: Boolean = true, block: suspend CoroutineScope.() -> Unit) = | ||||||
|  |         viewModelScope.launch( | ||||||
|  |             CoroutineExceptionHandler { _, throwable -> | ||||||
|  |                 if (snackbar) { | ||||||
|  |                     SnackbarManager.showMessage(throwable.toSnackbarMessage()) | ||||||
|  |                 } | ||||||
|  |                 logService.logNonFatalCrash(throwable) | ||||||
|  |             }, | ||||||
|  |             block = block | ||||||
|  |         ) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | package be.ugent.sel.studeez.screens.splash | ||||||
|  | 
 | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.layout.Arrangement | ||||||
|  | import androidx.compose.foundation.layout.Column | ||||||
|  | import androidx.compose.foundation.layout.fillMaxHeight | ||||||
|  | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
|  | import androidx.compose.foundation.rememberScrollState | ||||||
|  | import androidx.compose.foundation.verticalScroll | ||||||
|  | import androidx.compose.material.CircularProgressIndicator | ||||||
|  | import androidx.compose.material.MaterialTheme | ||||||
|  | import androidx.compose.material.Text | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.LaunchedEffect | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.hilt.navigation.compose.hiltViewModel | ||||||
|  | import be.ugent.sel.studeez.common.composable.BasicButton | ||||||
|  | import be.ugent.sel.studeez.common.ext.basicButton | ||||||
|  | import kotlinx.coroutines.delay | ||||||
|  | import be.ugent.sel.studeez.R.string as AppText | ||||||
|  | 
 | ||||||
|  | private const val SPLASH_TIMEOUT = 1000L | ||||||
|  | 
 | ||||||
|  | @Composable | ||||||
|  | fun SplashScreen( | ||||||
|  |     openAndPopUp: (String, String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     viewModel: SplashViewModel = hiltViewModel() | ||||||
|  | ) { | ||||||
|  |     Column( | ||||||
|  |         modifier = | ||||||
|  |         modifier | ||||||
|  |             .fillMaxWidth() | ||||||
|  |             .fillMaxHeight() | ||||||
|  |             .background(color = MaterialTheme.colors.background) | ||||||
|  |             .verticalScroll(rememberScrollState()), | ||||||
|  |         verticalArrangement = Arrangement.Center, | ||||||
|  |         horizontalAlignment = Alignment.CenterHorizontally | ||||||
|  |     ) { | ||||||
|  |         if (viewModel.showError.value) { | ||||||
|  |             Text(text = stringResource(AppText.generic_error)) | ||||||
|  | 
 | ||||||
|  |             BasicButton(AppText.try_again, Modifier.basicButton()) { viewModel.onAppStart(openAndPopUp) } | ||||||
|  |         } else { | ||||||
|  |             CircularProgressIndicator(color = MaterialTheme.colors.onBackground) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     LaunchedEffect(true) { | ||||||
|  |         delay(SPLASH_TIMEOUT) | ||||||
|  |         viewModel.onAppStart(openAndPopUp) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | package be.ugent.sel.studeez.screens.splash | ||||||
|  | 
 | ||||||
|  | import androidx.compose.runtime.mutableStateOf | ||||||
|  | import be.ugent.sel.studeez.domain.AccountDAO | ||||||
|  | import be.ugent.sel.studeez.domain.LogService | ||||||
|  | import be.ugent.sel.studeez.screens.StudeezViewModel | ||||||
|  | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
|  | import javax.inject.Inject | ||||||
|  | 
 | ||||||
|  | @HiltViewModel | ||||||
|  | class SplashViewModel @Inject constructor( | ||||||
|  |     private val accountDAO: AccountDAO, | ||||||
|  |     logService: LogService | ||||||
|  | ) : StudeezViewModel(logService) { | ||||||
|  |     val showError = mutableStateOf(false) | ||||||
|  | 
 | ||||||
|  |     fun onAppStart(openAndPopUp: (String, String) -> Unit) { | ||||||
|  | 
 | ||||||
|  |         showError.value = false | ||||||
|  |         if (accountDAO.hasUser) { | ||||||
|  |             // openAndPopUp( <homeScreen>, SPLASH_SCREEN) | ||||||
|  |         } else{ | ||||||
|  |             // openAndPopUp(<login>, SPLASH_SCREEN) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_visibility_off.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/drawable/ic_visibility_off.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportWidth="24" | ||||||
|  |     android:viewportHeight="24" | ||||||
|  |     android:tint="?attr/colorControlNormal"> | ||||||
|  |   <path | ||||||
|  |       android:fillColor="@android:color/white" | ||||||
|  |       android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/> | ||||||
|  | </vector> | ||||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_visibility_on.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/drawable/ic_visibility_on.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportWidth="24" | ||||||
|  |     android:viewportHeight="24" | ||||||
|  |     android:tint="?attr/colorControlNormal"> | ||||||
|  |   <path | ||||||
|  |       android:fillColor="@android:color/white" | ||||||
|  |       android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/> | ||||||
|  | </vector> | ||||||
|  | @ -1,3 +1,26 @@ | ||||||
| <resources> | <resources> | ||||||
|  |     <!-- Common --> | ||||||
|     <string name="app_name">Studeez</string> |     <string name="app_name">Studeez</string> | ||||||
|  |     <string name="email">Email</string> | ||||||
|  |     <string name="password">Password</string> | ||||||
|  |     <string name="repeat_password">Repeat password</string> | ||||||
|  |     <string name="generic_error">Something wrong happened. Please try again.</string> | ||||||
|  |     <string name="email_error">Please insert a valid email.</string> | ||||||
|  |     <string name="cancel">Cancel</string> | ||||||
|  |     <string name="try_again">Try again</string> | ||||||
|  | 
 | ||||||
|  |     <!-- SignUpScreen --> | ||||||
|  |     <string name="create_account">Create account</string> | ||||||
|  |     <string name="password_error">Your password should have at least six characters and include one digit, one lower case letter and one upper case letter.</string> | ||||||
|  |     <string name="password_match_error">Passwords do not match.</string> | ||||||
|  |     <string name="already_user">Already have an account? Log in.</string> | ||||||
|  | 
 | ||||||
|  |     <!-- LoginScreen --> | ||||||
|  |     <string name="not_already_user">Don\'t have an account yet? Sign up.</string> | ||||||
|  |     <string name="sign_in">Sign in</string> | ||||||
|  |     <string name="login_details">Enter your login details</string> | ||||||
|  |     <string name="forgot_password">Forgot password? Click to get recovery email</string> | ||||||
|  |     <string name="recovery_email_sent">Check your inbox for the recovery email.</string> | ||||||
|  |     <string name="empty_password_error">Password cannot be empty.</string> | ||||||
|  | 
 | ||||||
| </resources> | </resources> | ||||||
		Reference in a new issue
	
	 lbarraga
						lbarraga