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