diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt index 61a2499..5ab89b0 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt @@ -16,4 +16,6 @@ interface AuthManager { fun cancelLoginFlow() suspend fun getAccessToken(): String + + fun logout(ctx: Context) } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt index 0554e44..370de2d 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt @@ -176,4 +176,15 @@ constructor( expires = calculateExpiration(tokenResponse.expiresIn), ) } + + override fun logout(ctx: Context) { + try { + authStorage.clearAuthData() + authData = null + state = AuthState.NOT_AUTHENTICATED + } catch (e: Exception) { + Log.e("SpotifyAuth", "Error during logout", e) + throw FeelBeatException(ErrorCode.AUTHENTICATION_LOGOUT_ERROR, e) + } + } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/AuthStorage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/AuthStorage.kt index c2e385e..758081f 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/AuthStorage.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/AuthStorage.kt @@ -6,4 +6,6 @@ interface AuthStorage { fun storeAuthData(data: AuthData) fun retrieveAuthData(): AuthData? + + fun clearAuthData() } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt index afcce35..f306afd 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt @@ -35,4 +35,8 @@ class PreferencesAuthStorage @Inject constructor(@ApplicationContext ctx: Contex return AuthData(accessToken = accessToken, refreshToken = refreshToken, expires = expires) } + + override fun clearAuthData() { + preferences.edit().clear().apply() + } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/error/ErrorCode.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/error/ErrorCode.kt index bb8a207..73e8213 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/error/ErrorCode.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/error/ErrorCode.kt @@ -4,4 +4,5 @@ enum class ErrorCode { AUTHORIZATION_SERVER_UNREACHABLE, AUTHORIZATION_ACCESS_TOKEN_ERROR, AUTHORIZATION_REFRESH_TOKEN_ERROR, + AUTHENTICATION_LOGOUT_ERROR, } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt index afed8be..36ba656 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt @@ -43,9 +43,6 @@ fun FeelBeatApp( composable(route = FeelBeatRoute.ACCEPT_GAME.name) { AcceptGameScreen(navController = navController) } - composable(route = FeelBeatRoute.ACCOUNT_SETTINGS.name) { - // AccountSettingsScreen(parentNavController = navController) - } composable(route = FeelBeatRoute.GAME_RESULT.name) { GameResultScreen(navController = navController) } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt index b111f24..e0caa14 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt @@ -2,13 +2,13 @@ package com.github.feelbeatapp.androidclient.ui.home import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -23,34 +23,57 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.github.feelbeatapp.androidclient.R import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute import com.github.feelbeatapp.androidclient.ui.state.Room +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( - viewModel: HomeViewModel = HomeViewModel(), + viewModel: HomeViewModel = hiltViewModel(), modifier: Modifier = Modifier, navController: NavController, ) { val title = stringResource(R.string.feel_beat) val rooms by viewModel.rooms.collectAsState() val selectedRoom by viewModel.selectedRoom.collectAsState() + val ctx = LocalContext.current + + var isBottomSheetVisible by remember { mutableStateOf(false) } + + if (isBottomSheetVisible) { + ModalBottomSheet(onDismissRequest = { isBottomSheetVisible = false }) { + UserAccountBottomSheetContent( + onLogoutClick = { + viewModel.logout(ctx) + isBottomSheetVisible = false + navController.navigate(FeelBeatRoute.LOGIN.name) + } + ) + } + } - Scaffold(topBar = { HomeTopBar(title, navController) }) { innerPadding -> + Scaffold(topBar = { HomeTopBar(title) { isBottomSheetVisible = true } }) { innerPadding -> Column(modifier = modifier.padding(innerPadding).fillMaxSize()) { Text( text = stringResource(R.string.current_games), @@ -77,7 +100,6 @@ fun HomeScreen( Box( modifier = Modifier.align(Alignment.BottomEnd) - .offset(x = (-15).dp) .size(60.dp) .background( MaterialTheme.colorScheme.primary, @@ -103,7 +125,7 @@ fun HomeScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeTopBar(title: String, navController: NavController) { +fun HomeTopBar(title: String, onAccountClick: () -> Unit) { CenterAlignedTopAppBar( title = { Text(title) }, colors = @@ -112,7 +134,7 @@ fun HomeTopBar(title: String, navController: NavController) { titleContentColor = MaterialTheme.colorScheme.primary, ), actions = { - IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCOUNT_SETTINGS.name) }) { + IconButton(onClick = onAccountClick) { Icon( imageVector = Icons.Outlined.Person, contentDescription = stringResource(R.string.account), @@ -122,6 +144,42 @@ fun HomeTopBar(title: String, navController: NavController) { ) } +@Composable +fun UserAccountBottomSheetContent(onLogoutClick: () -> Unit) { + Column( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Box( + modifier = + Modifier.size(80.dp).background(Color.Gray, shape = MaterialTheme.shapes.large), + contentAlignment = Alignment.Center, + ) { + Text( + text = "U", // Placeholder for user's avatar + style = MaterialTheme.typography.headlineMedium, + color = Color.White, + ) + } + Text(text = "User Name", style = MaterialTheme.typography.titleMedium) + Box( + modifier = + Modifier.fillMaxWidth() + .background(MaterialTheme.colorScheme.primary, MaterialTheme.shapes.medium) + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = "log out", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding(vertical = 4.dp).clickable { onLogoutClick() }, + ) + } + } +} + @Composable fun RoomItem(room: Room, isSelected: Boolean, onClick: () -> Unit) { Card( diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt index e0d403f..112186a 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt @@ -1,15 +1,19 @@ package com.github.feelbeatapp.androidclient.ui.home +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.github.feelbeatapp.androidclient.auth.AuthManager import com.github.feelbeatapp.androidclient.ui.state.Room +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -class HomeViewModel : ViewModel() { - +@HiltViewModel +class HomeViewModel @Inject constructor(private val authManager: AuthManager) : ViewModel() { private val _rooms = MutableStateFlow>(emptyList()) val rooms: StateFlow> = _rooms.asStateFlow() @@ -20,6 +24,10 @@ class HomeViewModel : ViewModel() { loadRooms() } + fun logout(ctx: Context) { + authManager.logout(ctx) + } + @SuppressWarnings("MagicNumber") private fun loadRooms() { viewModelScope.launch { diff --git a/detekt.yml b/detekt.yml index 16e4cd6..90c8d37 100644 --- a/detekt.yml +++ b/detekt.yml @@ -12,8 +12,7 @@ complexity: LongParameterList: ignoreDefaultParameters: true TooManyFunctions: - ignoreAnnotated: - - 'Preview' + active: false style: ThrowsCount: