diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/data/repository/UserAuthenticationRepositoryImpl.kt b/app/src/main/java/com/oguzdogdu/walliescompose/data/repository/UserAuthenticationRepositoryImpl.kt index e5dada2c..66579718 100644 --- a/app/src/main/java/com/oguzdogdu/walliescompose/data/repository/UserAuthenticationRepositoryImpl.kt +++ b/app/src/main/java/com/oguzdogdu/walliescompose/data/repository/UserAuthenticationRepositoryImpl.kt @@ -1,8 +1,11 @@ package com.oguzdogdu.walliescompose.data.repository import com.google.android.gms.tasks.Task +import com.google.firebase.auth.AuthCredential import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.EmailAuthProvider import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.GoogleAuthProvider import com.google.firebase.firestore.FirebaseFirestore import com.oguzdogdu.walliescompose.data.common.Constants.COLLECTION_PATH @@ -95,6 +98,21 @@ class UserAuthenticationRepositoryImpl @Inject constructor( } }.toResource() + override suspend fun changeEmail(email: String?, password: String?) : Flow?>> = flow { + val credential = + auth.currentUser?.email?.let { password?.let { password -> + EmailAuthProvider.getCredential(it, + password + ) + } } + credential?.let { + auth.currentUser?.reauthenticate(it)?.await() + emit(auth.currentUser?.updateEmail(email.orEmpty())) + firebaseFirestore.collection(COLLECTION_PATH).document(auth.currentUser!!.uid) + .update(EMAIL, email) + } + }.toResource() + override suspend fun signOut() = auth.signOut() override suspend fun changeProfilePhoto(photo: String?) { diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/domain/repository/UserAuthenticationRepository.kt b/app/src/main/java/com/oguzdogdu/walliescompose/domain/repository/UserAuthenticationRepository.kt index e250eaab..2bc9c48b 100644 --- a/app/src/main/java/com/oguzdogdu/walliescompose/domain/repository/UserAuthenticationRepository.kt +++ b/app/src/main/java/com/oguzdogdu/walliescompose/domain/repository/UserAuthenticationRepository.kt @@ -1,6 +1,7 @@ package com.oguzdogdu.walliescompose.domain.repository import com.google.android.gms.tasks.Task +import com.google.firebase.auth.AuthCredential import com.google.firebase.auth.AuthResult import com.oguzdogdu.walliescompose.domain.model.auth.User import com.oguzdogdu.walliescompose.domain.wrapper.Resource @@ -16,4 +17,5 @@ interface UserAuthenticationRepository { suspend fun signOut() suspend fun changeProfilePhoto(photo: String?) suspend fun updatePassword(password: String?): Flow?>> + suspend fun changeEmail(email:String?,password: String?): Flow?>> } \ No newline at end of file diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenNavigation.kt b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenNavigation.kt index 869a6379..f2e0beb7 100644 --- a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenNavigation.kt +++ b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenNavigation.kt @@ -16,8 +16,10 @@ fun NavController.navigateToAuthenticatedUserScreen( fun NavGraphBuilder.authenticatedUserScreen( navigateBack: () -> Unit, navigateToLogin: () -> Unit, - navigateToChangePassword: () -> Unit -) { + navigateToChangePassword: () -> Unit, + navigateToChangeEmail: () -> Unit, + + ) { composable( AuthenticatedUserScreenNavigationRoute ) { @@ -30,6 +32,8 @@ fun NavGraphBuilder.authenticatedUserScreen( }, navigateToChangePassword = { navigateToChangePassword.invoke() + }, navigateToChangeEmail = { + navigateToChangeEmail.invoke() } ) } diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenRoute.kt b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenRoute.kt index 41ab84d6..9cb9faf8 100644 --- a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenRoute.kt +++ b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/AuthenticatedUserScreenRoute.kt @@ -80,8 +80,9 @@ fun AuthenticatedUserScreenRoute( viewModel: AuthenticatedUserViewModel = hiltViewModel(), navigateBack:() -> Unit, navigateToLogin: () -> Unit, - navigateToChangePassword: () -> Unit -) { + navigateToChangePassword: () -> Unit, + navigateToChangeEmail: () -> Unit, + ) { val userState by viewModel.userState.collectAsStateWithLifecycle() val dialogState by viewModel.changeProfilePhotoBottomSheetOpenStat.collectAsStateWithLifecycle() @@ -167,6 +168,8 @@ fun AuthenticatedUserScreenRoute( viewModel.handleUiEvents(AuthenticatedUserEvent.OpenChangeProfileBottomSheet(dialog)) }, onChangePasswordClick = { navigateToChangePassword.invoke() + }, onChangeEmailClick = { + navigateToChangeEmail.invoke() },showDialog = dialogState) } } @@ -183,6 +186,7 @@ fun AuthenticatedUserScreenContent( onChangeProfilePhotoButtonClick: () -> Unit, dismissDialog: (Boolean) -> Unit, onChangePasswordClick: () -> Unit, + onChangeEmailClick: () -> Unit, showDialog: Boolean ) { var isGoogleSign by remember { @@ -220,9 +224,15 @@ fun AuthenticatedUserScreenContent( onChangeProfilePhotoClick.invoke(it) } ) - EditProfileInformationContent(modifier = modifier, onChangePasswordClick = { - onChangePasswordClick.invoke() - }) + EditProfileInformationContent( + modifier = modifier, + onChangePasswordClick = { + onChangePasswordClick.invoke() + }, + onChangeEmailClick = { + onChangeEmailClick.invoke() + } + ) } ChangeProfilePhotoDialog(userInfos = userScreenState, modifier = modifier, profilePhotoUri = profilePhotoUri ,isOpen = showDialog, onDismiss = { dismissDialog.invoke(false) @@ -399,7 +409,11 @@ fun AuthenticatedUserWelcomeCard( } @Composable -fun EditProfileInformationContent(modifier: Modifier,onChangePasswordClick: () -> Unit) { +fun EditProfileInformationContent( + modifier: Modifier, + onChangePasswordClick: () -> Unit, + onChangeEmailClick: () -> Unit +) { val scope = rememberCoroutineScope() val profileOptionsList = immutableListOf( MenuRow( @@ -438,7 +452,7 @@ fun EditProfileInformationContent(modifier: Modifier,onChangePasswordClick: () - }, openEditEmail = { - + onChangeEmailClick.invoke() }, openChangePassword = { onChangePasswordClick.invoke() } diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenNavigation.kt b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenNavigation.kt new file mode 100644 index 00000000..9a1b8c82 --- /dev/null +++ b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenNavigation.kt @@ -0,0 +1,26 @@ +package com.oguzdogdu.walliescompose.features.authenticateduser.changeemail + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable + +const val ChangeEmailScreenNavigationRoute = "change_email_screen_route" + +fun NavController.navigateToChangeEmailScreen( + navOptions: NavOptions? = null, +) { + this.navigate(ChangeEmailScreenNavigationRoute, navOptions) +} + +fun NavGraphBuilder.changeEmailScreen( + navigateBack: () -> Unit, +) { + composable( + ChangeEmailScreenNavigationRoute + ) { + ChangeEmailScreenRoute(navigateBack = { + navigateBack.invoke() + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenRoute.kt b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenRoute.kt new file mode 100644 index 00000000..f9f9e032 --- /dev/null +++ b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenRoute.kt @@ -0,0 +1,287 @@ +package com.oguzdogdu.walliescompose.features.authenticateduser.changeemail + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Clear +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ShapeDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +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.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.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.oguzdogdu.walliescompose.R +import com.oguzdogdu.walliescompose.ui.theme.medium + +@Composable +fun ChangeEmailScreenRoute( + modifier: Modifier = Modifier, + viewModel: ChangeEmailViewModel = hiltViewModel(), + navigateBack: () -> Unit +) { + + val stateOfEmail by viewModel.emailState.collectAsStateWithLifecycle() + + Scaffold(modifier = modifier + .fillMaxSize(), topBar = { + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { navigateBack.invoke() }, + modifier = modifier.wrapContentSize() + ) { + Icon( + painter = painterResource(id = R.drawable.back), + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = modifier.wrapContentSize() + ) + } + + Text( + modifier = modifier, + text = stringResource(id = R.string.edit_email), + color = MaterialTheme.colorScheme.onPrimaryContainer, + fontSize = 16.sp, + fontFamily = medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start + ) + } + }) { + Column( + modifier = modifier + .padding(it) + .fillMaxSize() + ) { + ChangeEmailScreenContent(modifier = modifier, onPasswordChange = { password -> + viewModel.setPassword(password) + viewModel.handleUIEvent(ChangeUserEmailEvent.ButtonState) + }, state = stateOfEmail, onEmailChange = {email -> + viewModel.setEmail(email) + viewModel.handleUIEvent(ChangeUserEmailEvent.ButtonState) + },onLoginButtonClick = { + viewModel.handleUIEvent(ChangeUserEmailEvent.ChangedEmail) + }) + } + } +} + +@Composable +fun ChangeEmailScreenContent( + modifier: Modifier, + state: ChangeEmailScreenState, + onEmailChange: (String) -> Unit, + onPasswordChange: (String) -> Unit, + onLoginButtonClick: () -> Unit, +) { + val context = LocalContext.current + + var password by remember { + mutableStateOf("") + } + var email by remember { + mutableStateOf("") + } + var buttonEnabled by remember { mutableStateOf(false) } + + var loading by remember { + mutableStateOf(false) + } + + LaunchedEffect(state) { + when (state) { + is ChangeEmailScreenState.ButtonEnabled -> { + buttonEnabled = state.isEnabled + } + + is ChangeEmailScreenState.Loading -> { + loading = state.isLoading + } + + is ChangeEmailScreenState.ChangedEmailError -> { + Toast.makeText(context,state.errorMessage,Toast.LENGTH_SHORT).show() + } + + is ChangeEmailScreenState.ChangedEmail -> { + Toast.makeText(context,state.emailChanged,Toast.LENGTH_SHORT).show() + } + + is ChangeEmailScreenState.Start -> { + + } + } + } + + Box(modifier = modifier.fillMaxSize()) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .wrapContentHeight(), + verticalArrangement = Arrangement.SpaceBetween + ) { + + Text( + text = stringResource(R.string.email), + fontSize = 16.sp, + fontFamily = medium, + modifier = modifier + ) + + Spacer(modifier = modifier.size(8.dp)) + + TextField(modifier = modifier + .fillMaxWidth(), + value = email, + onValueChange = { + email = it + onEmailChange.invoke(it) + }, + shape = ShapeDefaults.Medium, + colors = TextFieldDefaults.colors( + focusedTextColor = MaterialTheme.colorScheme.onBackground, + disabledTextColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ), + singleLine = true, + trailingIcon = { + if (email.isNotEmpty()) { + IconButton( + onClick = { email = "" }, modifier = modifier.wrapContentSize() + ) { + Icon( + imageVector = Icons.Rounded.Clear, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = modifier.wrapContentSize() + ) + } + } + }) + + Text( + text = stringResource(R.string.password), + fontSize = 16.sp, + fontFamily = medium, + modifier = modifier + ) + + Spacer(modifier = modifier.size(8.dp)) + + TextField(modifier = modifier + .fillMaxWidth(), + value = password, + onValueChange = { + password = it + onPasswordChange.invoke(it) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, imeAction = ImeAction.Search + ), + shape = ShapeDefaults.Medium, + colors = TextFieldDefaults.colors( + focusedTextColor = MaterialTheme.colorScheme.onBackground, + disabledTextColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ), + singleLine = true, + trailingIcon = { + if (password.isNotEmpty()) { + IconButton( + onClick = { password = "" }, modifier = modifier.wrapContentSize() + ) { + Icon( + imageVector = Icons.Rounded.Clear, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = modifier.wrapContentSize() + ) + } + } + }) + } + + Button( + onClick = { + onLoginButtonClick.invoke() + }, + enabled = buttonEnabled, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .align(Alignment.BottomCenter) + , + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ), + shape = RoundedCornerShape(16.dp), + contentPadding = PaddingValues(16.dp) + ) { + when { + loading -> { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.primary, + modifier = modifier.size(16.dp), + strokeWidth = 2.dp + ) + } + else -> { + Text( + text = stringResource(R.string.send_info), + fontSize = 14.sp, + fontFamily = medium, + color = Color.White + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenState.kt b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenState.kt new file mode 100644 index 00000000..099d8175 --- /dev/null +++ b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailScreenState.kt @@ -0,0 +1,9 @@ +package com.oguzdogdu.walliescompose.features.authenticateduser.changeemail + +sealed class ChangeEmailScreenState { + data object Start : ChangeEmailScreenState() + data class Loading(val isLoading:Boolean) : ChangeEmailScreenState() + data class ChangedEmailError(val errorMessage: String?) : ChangeEmailScreenState() + data class ChangedEmail(val emailChanged:String) : ChangeEmailScreenState() + data class ButtonEnabled(val isEnabled: Boolean) : ChangeEmailScreenState() +} diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailViewModel.kt b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailViewModel.kt new file mode 100644 index 00000000..4cb46319 --- /dev/null +++ b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeEmailViewModel.kt @@ -0,0 +1,89 @@ +package com.oguzdogdu.walliescompose.features.authenticateduser.changeemail + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.oguzdogdu.walliescompose.domain.repository.UserAuthenticationRepository +import com.oguzdogdu.walliescompose.domain.wrapper.onFailure +import com.oguzdogdu.walliescompose.domain.wrapper.onSuccess +import com.oguzdogdu.walliescompose.util.FieldValidators +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ChangeEmailViewModel @Inject constructor( + private val authenticationRepository: UserAuthenticationRepository +) : ViewModel() { + + private val _emailState: MutableStateFlow = MutableStateFlow( + ChangeEmailScreenState.Start + ) + val emailState = _emailState.asStateFlow() + + private var userEmail = MutableStateFlow("") + + private var userPassword = MutableStateFlow("") + + fun handleUIEvent(event: ChangeUserEmailEvent) { + when (event) { + is ChangeUserEmailEvent.ChangedEmail -> { + changeEmail(email = userEmail.value, password = userPassword.value) + } + + is ChangeUserEmailEvent.ButtonState -> checkButtonState() + } + } + + fun setEmail(email: String?) { + email?.let { + userEmail.value = it + } + } + + fun setPassword(password: String?) { + password?.let { + userPassword.value = it + } + } + + private fun checkButtonState() { + viewModelScope.launch { + val buttonState = + FieldValidators.isValidEmail(email = userEmail.value) && FieldValidators.isValidPasswordCheck( + input = userPassword.value + ) + _emailState.update { ChangeEmailScreenState.ButtonEnabled(isEnabled = buttonState) } + } + } + + private fun changeEmail(email: String?, password: String?) { + viewModelScope.launch { + _emailState.update { + ChangeEmailScreenState.Loading(true) + } + + authenticationRepository.changeEmail(email = email, password = password) + .collect { result -> + result.onFailure { error -> + _emailState.update { + ChangeEmailScreenState.ChangedEmailError(error) + } + } + + result.onSuccess { auth -> + _emailState.update { + ChangeEmailScreenState.ChangedEmail("Email Changed") + } + } + } + delay(1000) + _emailState.update { + ChangeEmailScreenState.Loading(false) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeUserEmailEvent.kt b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeUserEmailEvent.kt new file mode 100644 index 00000000..28e987e6 --- /dev/null +++ b/app/src/main/java/com/oguzdogdu/walliescompose/features/authenticateduser/changeemail/ChangeUserEmailEvent.kt @@ -0,0 +1,6 @@ +package com.oguzdogdu.walliescompose.features.authenticateduser.changeemail + +sealed class ChangeUserEmailEvent { + data object ButtonState : ChangeUserEmailEvent() + data object ChangedEmail : ChangeUserEmailEvent() +} diff --git a/app/src/main/java/com/oguzdogdu/walliescompose/navigation/WalliesNavHost.kt b/app/src/main/java/com/oguzdogdu/walliescompose/navigation/WalliesNavHost.kt index 1d752bd6..5ceb1128 100644 --- a/app/src/main/java/com/oguzdogdu/walliescompose/navigation/WalliesNavHost.kt +++ b/app/src/main/java/com/oguzdogdu/walliescompose/navigation/WalliesNavHost.kt @@ -14,6 +14,8 @@ import androidx.navigation.compose.navigation import com.oguzdogdu.walliescompose.features.appstate.MainAppState import com.oguzdogdu.walliescompose.features.authenticateduser.AuthenticatedUserScreenNavigationRoute import com.oguzdogdu.walliescompose.features.authenticateduser.authenticatedUserScreen +import com.oguzdogdu.walliescompose.features.authenticateduser.changeemail.changeEmailScreen +import com.oguzdogdu.walliescompose.features.authenticateduser.changeemail.navigateToChangeEmailScreen import com.oguzdogdu.walliescompose.features.authenticateduser.changepassword.changePasswordScreen import com.oguzdogdu.walliescompose.features.authenticateduser.changepassword.navigateToChangePasswordScreen import com.oguzdogdu.walliescompose.features.authenticateduser.navigateToAuthenticatedUserScreen @@ -237,10 +239,15 @@ fun WalliesNavHost( } }, navigateToChangePassword = { navController.navigateToChangePasswordScreen() + }, navigateToChangeEmail = { + navController.navigateToChangeEmailScreen() }) changePasswordScreen(navigateBack = { navController.popBackStack() }) + changeEmailScreen(navigateBack = { + navController.popBackStack() + }) } } } \ No newline at end of file