Biometric authentication implemented and integrated in dashboard

This commit is contained in:
2025-12-30 20:46:37 +01:00
parent d07d369546
commit 66e3bbb004
21 changed files with 540 additions and 250 deletions

View File

@@ -4,39 +4,24 @@ import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
@@ -48,20 +33,16 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.lifecycleScope
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.status.SessionStatus
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.tinsae.clocked.biometric.GlobalAuthenticator
import net.tinsae.clocked.components.ShowLoadingScreen
import net.tinsae.clocked.dashboard.DashboardScreen
import net.tinsae.clocked.data.Locale
@@ -71,11 +52,10 @@ import net.tinsae.clocked.history.HistoryScreen
import net.tinsae.clocked.settings.SettingsScreen
import net.tinsae.clocked.settings.SettingsViewModel
import net.tinsae.clocked.ui.theme.ClockedTheme
import net.tinsae.clocked.util.SupabaseClient
import java.text.SimpleDateFormat
import java.util.Date
class MainActivity : ComponentActivity() {
class MainActivity : AppCompatActivity() {
private val settingsViewModel: SettingsViewModel by viewModels()
private val authViewModel: AuthViewModel by viewModels()
@@ -120,6 +100,7 @@ class MainActivity : ComponentActivity() {
setContent {
// The single ViewModel instances from the Activity are passed down.
AppEntry(settingsViewModel, authViewModel)
GlobalAuthenticator()
}
}
}
@@ -128,19 +109,7 @@ class MainActivity : ComponentActivity() {
fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) {
val settingsState by settingsViewModel.uiState.collectAsState()
val sessionStatus by authViewModel.sessionStatus.collectAsState()
/*LaunchedEffect(Unit) {
SupabaseClient.client.auth.sessionStatus.collect { status ->
if (status is SessionStatus.Authenticated &&
SupabaseClient.client.auth.currentSessionOrNull() != null
) {
LogRepository.fetchLogs()
} else {
return@collect
}
}
}*/
// Trigger fetching of logs when the session status changes.
LaunchedEffect(sessionStatus) {
if (sessionStatus is SessionStatus.Authenticated){
LogRepository.fetchLogs()
@@ -169,11 +138,9 @@ fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel)
ClockedApp(settingsViewModel, authViewModel)
}
is SessionStatus.NotAuthenticated -> {
else -> {
LoginScreen(authViewModel)
}
else -> {}
}
}
}
@@ -194,12 +161,16 @@ fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewMode
NavigationSuiteScaffold(
modifier = Modifier.fillMaxSize(),
navigationSuiteItems = {
AppDestinations.entries.forEach { destination ->
item(
icon = { Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.label))
icon = {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.label),
modifier = Modifier.padding(1.dp).size(24.dp)
)
},
alwaysShowLabel = false,
label = { Text(stringResource(destination.label)) },
@@ -211,52 +182,40 @@ fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewMode
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
/*topBar = {
TopBar(
appName = stringResource(id = R.string.app_name, currentDestination.label),
modifier = Modifier.padding(horizontal = 16.dp),
isForContainer = true,
actions = {
IconButton(
onClick = { authViewModel.logout() }, modifier = Modifier.padding(10.dp),
topBar = {
Surface(
color = colorScheme.surfaceContainer,
shadowElevation = 2.dp, // Adds a subtle shadow to lift the bar
modifier = Modifier.fillMaxWidth()
// No padding needed here on the Surface itself
) {
Row(
// Apply safe area padding to the Row to push content down
modifier = Modifier
.fillMaxWidth()
.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top).asPaddingValues()),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(start = 16.dp), // Vertical padding is handled by Row's alignment
text = stringResource(R.string.app_name),
style = typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = { authViewModel.logout() }, modifier = Modifier.padding(10.dp)) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Logout,
contentDescription = "Logout" // Use string resource
)
) {
Icon(imageVector = Icons.AutoMirrored.Filled.Logout, contentDescription = stringResource(R.string.logout))
}
}
}
},
) { innerPadding ->
val modifier = Modifier.fillMaxWidth().padding(innerPadding)
when (currentDestination) {
AppDestinations.HOME -> DashboardScreen( modifier = modifier)
AppDestinations.HISTORY -> HistoryScreen(modifier = modifier)
AppDestinations.SETTING -> SettingsScreen(
modifier = Modifier,
viewModel = settingsViewModel,
authViewModel = authViewModel
)
}*/
) { innerPadding ->
val modifier = Modifier
.fillMaxWidth()
.padding(innerPadding)
when (currentDestination) {
AppDestinations.HOME -> DashboardScreen(modifier = modifier)
AppDestinations.HISTORY -> HistoryScreen(modifier = modifier)
AppDestinations.SETTING -> SettingsScreen(
modifier = modifier,
viewModel = settingsViewModel
)
AppDestinations.LOGOUT -> {
authViewModel.logout()
}
}
}
}
}
}
@@ -267,12 +226,5 @@ enum class AppDestinations(
HOME(R.string.nav_home, Icons.Default.Home),
HISTORY(R.string.nav_history, Icons.AutoMirrored.Filled.List),
SETTING(R.string.nav_settings, Icons.Default.Settings),
}
@Preview(showBackground = true)
@Composable
fun AppEntryPreview() {
ClockedTheme {
AppEntry(SettingsViewModel(), AuthViewModel())
}
LOGOUT(R.string.logout, Icons.AutoMirrored.Filled.Logout)
}