diff --git a/shared/src/commonMain/kotlin/data/repository/UserRepository.kt b/shared/src/commonMain/kotlin/data/repository/UserRepository.kt index cb57a90..c3e38cc 100644 --- a/shared/src/commonMain/kotlin/data/repository/UserRepository.kt +++ b/shared/src/commonMain/kotlin/data/repository/UserRepository.kt @@ -2,6 +2,7 @@ package data.repository import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.auth.auth +import domain.model.AuthProvider import domain.model.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -27,12 +28,26 @@ class UserRepository(private val backgroundScope: CoroutineContext = Dispatchers } suspend fun deleteAccount() = withContext(backgroundScope) { - val currentUser=Firebase.auth.currentUser - currentUser?.providerData?.forEach { + val currentUser = Firebase.auth.currentUser + currentUser?.providerData?.forEach { AppLogger.e("ProviderId: ${it.providerId}") } currentUser?.delete() } + fun getCurrentUserProviders(): List { + val currentUser = Firebase.auth.currentUser + return currentUser?.providerData?.mapNotNull { + val providerId = it.providerId + AppLogger.d("ProviderId: $providerId") + when (providerId) { + "google.com" -> AuthProvider.GOOGLE + "apple.com" -> AuthProvider.APPLE + "github.com" -> AuthProvider.GITHUB + else -> null + } + } ?: emptyList() + } + } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/presentation/components/AuthUiHelperButtonsAndFirebaseAuth.kt b/shared/src/commonMain/kotlin/presentation/components/AuthUiHelperButtonsAndFirebaseAuth.kt index 5fb264d..9a35741 100644 --- a/shared/src/commonMain/kotlin/presentation/components/AuthUiHelperButtonsAndFirebaseAuth.kt +++ b/shared/src/commonMain/kotlin/presentation/components/AuthUiHelperButtonsAndFirebaseAuth.kt @@ -16,6 +16,10 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -43,6 +47,7 @@ fun AuthUiHelperButtonsAndFirebaseAuth( authProviders: List = AuthProvider.values().asList(), onFirebaseResult: (Result) -> Unit, ) { + val isExistOnlyOneAuthProvider by remember { mutableStateOf(authProviders.size == 1) } Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(14.dp) @@ -54,6 +59,7 @@ fun AuthUiHelperButtonsAndFirebaseAuth( if (authProviders.contains(AuthProvider.GOOGLE)) { //Google Sign-In Button and authentication with Firebase GoogleButtonUiContainerFirebase(onResult = onFirebaseResult) { + LaunchedEffect(Unit) { if (isExistOnlyOneAuthProvider) this@GoogleButtonUiContainerFirebase.onClick() } GoogleSignInButton( modifier = Modifier.fillMaxWidth().height(height), text = Strings.btn_sign_in_with_google, @@ -67,6 +73,7 @@ fun AuthUiHelperButtonsAndFirebaseAuth( if (authProviders.contains(AuthProvider.APPLE)) { //Apple Sign-In Button and authentication with Firebase AppleButtonUiContainer(onResult = onFirebaseResult) { + LaunchedEffect(Unit) { if (isExistOnlyOneAuthProvider) this@AppleButtonUiContainer.onClick() } AppleSignInButton( modifier = Modifier.fillMaxWidth().height(height), text = Strings.btn_sign_in_with_apple, @@ -78,6 +85,7 @@ fun AuthUiHelperButtonsAndFirebaseAuth( if (authProviders.contains(AuthProvider.GITHUB)) { //Github Sign-In Button and authentication with Firebase GithubButtonUiContainer(onResult = onFirebaseResult) { + LaunchedEffect(Unit) { if (isExistOnlyOneAuthProvider) this@GithubButtonUiContainer.onClick() } GithubSignInButton( modifier = Modifier.fillMaxWidth().height(height), text = Strings.btn_sign_in_with_github, diff --git a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt index 509a7e7..605e431 100644 --- a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt +++ b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt @@ -21,13 +21,18 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,6 +44,7 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import dev.gitlive.firebase.auth.FirebaseUser +import domain.model.AuthProvider import domain.model.User import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource @@ -59,29 +65,48 @@ fun ProfileScreen(modifier: Modifier = Modifier, uiStateHolder: ProfileUiStateHo val uiState by uiStateHolder.profileScreenUiState.asState() if (uiState.reAuthenticateUserViewShown) { SocialLoginsBottomSheet( + isLoading = uiState.isDeleteInProgress, + authProviders = uiState.currentUserAuthProviderList, onDismiss = uiStateHolder::onDismissReAuthenticateView, onResult = uiStateHolder::onUserReAuthenticatedResult ) } - if (uiState.isLoading) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - MyAppCircularProgressIndicator() - } - } if (uiState.deleteUserDialogShown) { DeleteUserConfirmationDialog( onConfirm = uiStateHolder::onConfirmDeleteAccount, onDismiss = uiStateHolder::onDismissDeleteUserConfirmationDialog ) } - uiState.currentUser?.let { currentUser -> - ProfileScreen( - modifier = modifier.fillMaxSize(), - currentUser = currentUser, - onClickLogOut = uiStateHolder::onClickLogOut, - onClickDeleteAccount = uiStateHolder::onClickDeleteAccount - ) + + val snackbarHostState = remember { SnackbarHostState() } + Scaffold(modifier = modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } + ) { + LaunchedEffect(uiState.message){ + uiState.message?.let { message-> + snackbarHostState.showSnackbar(message) + uiStateHolder.onMessageIsShown() + } + } + uiState.currentUser?.let { currentUser -> + ProfileScreen( + modifier = Modifier.fillMaxSize(), + currentUser = currentUser, + onClickLogOut = uiStateHolder::onClickLogOut, + onClickDeleteAccount = uiStateHolder::onClickDeleteAccount + ) + } + + } + + + if (uiState.isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + MyAppCircularProgressIndicator() + } } } @@ -150,20 +175,22 @@ private fun ProfileScreen( ) ) - Text( - modifier = Modifier - .clip(ButtonDefaults.textShape) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(color = MaterialTheme.colorScheme.primary), - onClick = {} - ) - .padding(horizontal = 16.dp, vertical = 4.dp), - text = Strings.edit_profile, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.secondary - ) - ) + //TODO Implement Edit Profile + +// Text( +// modifier = Modifier +// .clip(ButtonDefaults.textShape) +// .clickable( +// interactionSource = remember { MutableInteractionSource() }, +// indication = rememberRipple(color = MaterialTheme.colorScheme.primary), +// onClick = {} +// ) +// .padding(horizontal = 16.dp, vertical = 4.dp), +// text = Strings.edit_profile, +// style = MaterialTheme.typography.bodySmall.copy( +// color = MaterialTheme.colorScheme.secondary +// ) +// ) BasicInfo( modifier = Modifier.fillMaxWidth().padding(top = 24.dp), @@ -221,7 +248,12 @@ private fun BasicInfo(modifier: Modifier = Modifier, currentUser: User) { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SocialLoginsBottomSheet(onDismiss: () -> Unit, onResult: (Result) -> Unit) { +fun SocialLoginsBottomSheet( + isLoading: Boolean = false, + authProviders: List, + onDismiss: () -> Unit, + onResult: (Result) -> Unit, +) { ModalBottomSheet( windowInsets = WindowInsets(0), @@ -230,7 +262,11 @@ fun SocialLoginsBottomSheet(onDismiss: () -> Unit, onResult: (Result = emptyList(), + val message: String? = null, ) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt index de1606c..971920e 100644 --- a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt +++ b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import presentation.theme.strings.Strings import util.UiStateHolder -import util.logging.AppLogger import util.uiStateHolderScope class ProfileUiStateHolder(private val userRepository: UserRepository) : UiStateHolder() { @@ -41,22 +40,47 @@ class ProfileUiStateHolder(private val userRepository: UserRepository) : UiState } fun onConfirmDeleteAccount() = uiStateHolderScope.launch { - _profileScreenUiState.update { it.copy(reAuthenticateUserViewShown = true, deleteUserDialogShown = false) } + _profileScreenUiState.update { + it.copy( + reAuthenticateUserViewShown = true, + deleteUserDialogShown = false, + currentUserAuthProviderList = userRepository.getCurrentUserProviders() + ) + } } fun onDismissReAuthenticateView() = uiStateHolderScope.launch { _profileScreenUiState.update { it.copy(reAuthenticateUserViewShown = false) } } + fun onMessageIsShown() = uiStateHolderScope.launch { + _profileScreenUiState.update { it.copy(message = null) } + } + fun onUserReAuthenticatedResult(result: Result) = uiStateHolderScope.launch { - _profileScreenUiState.update { it.copy(reAuthenticateUserViewShown = false) } - result.onSuccess {user-> - if (user!=null) userRepository.deleteAccount() - else{ - AppLogger.e("User is null") + _profileScreenUiState.update { it.copy(isDeleteInProgress = true) } + result.onSuccess { user -> + val userMessage = if (user != null) { + userRepository.deleteAccount() + Strings.msg_success_delete_user + } else + Strings.error_msg_no_signed_in_user + _profileScreenUiState.update { + it.copy( + isDeleteInProgress = false, + reAuthenticateUserViewShown = false, + message = userMessage + ) + } + + }.onFailure { error -> + _profileScreenUiState.update { + it.copy( + isDeleteInProgress = false, + reAuthenticateUserViewShown = false, + message = error.message + ) } - }.onFailure { - AppLogger.e(it.message) } } diff --git a/shared/src/commonMain/kotlin/presentation/screens/account/signin/SignInScreen.kt b/shared/src/commonMain/kotlin/presentation/screens/account/signin/SignInScreen.kt index f875ed6..b47c0a6 100644 --- a/shared/src/commonMain/kotlin/presentation/screens/account/signin/SignInScreen.kt +++ b/shared/src/commonMain/kotlin/presentation/screens/account/signin/SignInScreen.kt @@ -1,61 +1,42 @@ package presentation.screens.account.signin import androidx.compose.animation.core.tween -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -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.asPaddingValues 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.layout.size import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.mmk.kmpauth.core.KMPAuthInternalApi -import com.mmk.kmpauth.core.di.isAndroidPlatform -import com.mmk.kmpauth.firebase.apple.AppleButtonUiContainer -import com.mmk.kmpauth.firebase.github.GithubButtonUiContainer -import com.mmk.kmpauth.firebase.google.GoogleButtonUiContainerFirebase -import com.mmk.kmpauth.uihelper.apple.AppleSignInButton -import com.mmk.kmpauth.uihelper.google.GoogleButtonMode -import com.mmk.kmpauth.uihelper.google.GoogleSignInButton -import dev.gitlive.firebase.auth.FirebaseUser +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource import presentation.components.AuthUiHelperButtonsAndFirebaseAuth -import presentation.theme.Yellow_alpha_0 -import presentation.theme.Yellow_alpha_39 import presentation.theme.strings.Strings import util.logging.AppLogger @@ -68,52 +49,65 @@ fun SignInScreen( ) { val scrollState = rememberScrollState() LaunchedEffect(true) { - scrollState.animateScrollTo(scrollState.maxValue, tween(1500)) + scrollState.animateScrollTo(scrollState.maxValue, tween(500)) } val systemBarPaddingValues = WindowInsets.systemBars.asPaddingValues() - - Column( - modifier - .fillMaxSize() - .verticalScroll(scrollState) -// .background( -// Brush.verticalGradient( -// 0.8f to Yellow_alpha_0, -// 1.0f to Yellow_alpha_39, -// ) -// ) - .padding(start = 30.dp,end=30.dp, top = systemBarPaddingValues.calculateTopPadding()+30.dp, bottom = 30.dp), - horizontalAlignment = Alignment.CenterHorizontally, + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + Scaffold( + modifier = modifier.fillMaxSize(), + contentWindowInsets = WindowInsets( + top = systemBarPaddingValues.calculateTopPadding(), + bottom = 0.dp, + ), + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } ) { + Column( + Modifier + .fillMaxSize() + .padding( + start = 30.dp, + end = 30.dp, + top = systemBarPaddingValues.calculateTopPadding() + 30.dp, + bottom = 30.dp + ) + .verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + ) { - Image( - painter = painterResource("drawable/ic_logo.xml"), - contentDescription = null, - modifier = Modifier.padding(top = 4.dp).size(140.dp) - ) + Image( + painter = painterResource("drawable/ic_logo.xml"), + contentDescription = null, + modifier = Modifier.padding(top = 4.dp).size(140.dp) + ) - TitleText(modifier = Modifier.padding(top = 20.dp)) + TitleText(modifier = Modifier.padding(top = 20.dp)) - AuthUiHelperButtonsAndFirebaseAuth( - modifier = Modifier.padding(top = 32.dp).fillMaxWidth(), - onFirebaseResult = { result -> - result.onSuccess { - AppLogger.d("Successful sign in") - }.onFailure { - AppLogger.e("Error occurred while signing in") + AuthUiHelperButtonsAndFirebaseAuth( + modifier = Modifier.padding(top = 32.dp).fillMaxWidth(), + onFirebaseResult = { result -> + result.onSuccess { + AppLogger.d("Successful sign in") + }.onFailure { + coroutineScope.launch { snackbarHostState.showSnackbar(it.message?:"") } + AppLogger.e("Error occurred while signing in") + } } - } - ) + ) - AgreePrivacyPolicyTermsConditionsText( - modifier = Modifier.padding(top = 32.dp).fillMaxWidth(), - onClickPrivacyPolicy = onNavigatePrivacyPolicy, - onClickTermsService = onNavigateTermsConditions - ) + AgreePrivacyPolicyTermsConditionsText( + modifier = Modifier.padding(top = 32.dp).fillMaxWidth(), + onClickPrivacyPolicy = onNavigatePrivacyPolicy, + onClickTermsService = onNavigateTermsConditions + ) + } } + } @Composable diff --git a/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt b/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt index 129c5cc..3d49795 100644 --- a/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt +++ b/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt @@ -87,6 +87,8 @@ object Strings { const val and = "and" const val btn_get_started = "GET STARTED" const val btn_skip = "SKIP" + const val error_msg_no_signed_in_user = "Error: There is not any signed-in user" + const val msg_success_delete_user = "Your account data is deleted successfully" }