diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/designsystem/theme/Colors.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/designsystem/theme/Colors.kt index 9c44ed78a..3623e9210 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/designsystem/theme/Colors.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/designsystem/theme/Colors.kt @@ -3,3 +3,4 @@ package app.omnivore.omnivore.core.designsystem.theme import androidx.compose.ui.graphics.Color val OmnivoreBrand = Color(0xFFFCEBA8) +val Success = Color(0xFF3C763D) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingScreen.kt index a02a40a0b..909ffa9ac 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingScreen.kt @@ -45,12 +45,13 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowInsetsControllerCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import app.omnivore.omnivore.R import app.omnivore.omnivore.core.designsystem.theme.OmnivoreBrand import app.omnivore.omnivore.feature.onboarding.auth.AuthProviderScreen +import app.omnivore.omnivore.feature.onboarding.auth.CreateUserScreen +import app.omnivore.omnivore.feature.onboarding.auth.EmailConfirmationScreen import app.omnivore.omnivore.feature.onboarding.auth.EmailSignInScreen import app.omnivore.omnivore.feature.onboarding.auth.EmailSignUpScreen import app.omnivore.omnivore.feature.onboarding.auth.SelfHostedScreen @@ -61,43 +62,57 @@ import app.omnivore.omnivore.navigation.Routes @OptIn(ExperimentalMaterial3Api::class) @Composable fun OnboardingScreen( - navController: NavHostController, - viewModel: LoginViewModel = hiltViewModel() + viewModel: OnboardingViewModel = hiltViewModel() ) { val activity = LocalContext.current as ComponentActivity - val welcomeNavController = rememberNavController() + val onboardingNavController = rememberNavController() val snackBarHostState = remember { SnackbarHostState() } - val currentRoute by welcomeNavController.currentBackStackEntryFlow.collectAsState( - initial = welcomeNavController.currentBackStackEntry + val currentRoute by onboardingNavController.currentBackStackEntryFlow.collectAsState( + initial = onboardingNavController.currentBackStackEntry ) val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle() + val navigateToCreateUser by viewModel.navigateToCreateUser.collectAsStateWithLifecycle() + val pendingEmailUserCreds by viewModel.pendingEmailUserCreds.collectAsStateWithLifecycle() - OmnivoreTheme(darkTheme = false) { - - LaunchedEffect(key1 = errorMessage) { - errorMessage?.let { message -> - val result = snackBarHostState.showSnackbar( - message = message, - actionLabel = "Dismiss", - duration = SnackbarDuration.Indefinite - ) - when (result) { - SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage() - else -> {} - } + LaunchedEffect(key1 = errorMessage) { + errorMessage?.let { message -> + val result = snackBarHostState.showSnackbar( + message = message, + actionLabel = "Dismiss", + duration = SnackbarDuration.Indefinite + ) + when (result) { + SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage() + else -> {} } } + } + LaunchedEffect(navigateToCreateUser) { + if (navigateToCreateUser) { + onboardingNavController.navigate(Routes.CreateUser.route) + viewModel.onNavigateToCreateUserHandled() + } + } + + LaunchedEffect(pendingEmailUserCreds) { + if (pendingEmailUserCreds != null) { + onboardingNavController.navigate(Routes.EmailConfirmation.route) + viewModel.onNavigateToEmailConfirmationHandled() + } + } + + OmnivoreTheme(darkTheme = false) { Scaffold( topBar = { TopAppBar( title = { }, navigationIcon = { if (currentRoute?.destination?.route != Routes.AuthProvider.route) { - IconButton(onClick = { welcomeNavController.popBackStack() }) { + IconButton(onClick = { onboardingNavController.popBackStack() }) { Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") } } @@ -126,26 +141,38 @@ fun OnboardingScreen( } item { OmnivoreNavHost( - navController = welcomeNavController, + navController = onboardingNavController, startDestination = Routes.AuthProvider.route ) { composable(Routes.AuthProvider.route) { AuthProviderScreen( - navController = navController, - welcomeNavController = welcomeNavController + welcomeNavController = onboardingNavController, + viewModel = viewModel ) } composable(Routes.EmailSignIn.route) { EmailSignInScreen( - navController = navController, - welcomeNavController = welcomeNavController + onboardingNavController = onboardingNavController, + viewModel = viewModel ) } composable(Routes.EmailSignUp.route) { - EmailSignUpScreen() + EmailSignUpScreen(viewModel = viewModel) + } + composable(Routes.EmailConfirmation.route) { + EmailConfirmationScreen( + viewModel = viewModel, + onboardingNavController = onboardingNavController + ) } composable(Routes.SelfHosting.route){ - SelfHostedScreen() + SelfHostedScreen(viewModel = viewModel) + } + composable(Routes.CreateUser.route){ + CreateUserScreen( + viewModel = viewModel, + onboardingNavController = onboardingNavController + ) } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingViewModel.kt index 7468123f7..008aaf4ac 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/OnboardingViewModel.kt @@ -58,7 +58,7 @@ data class PendingEmailUserCreds( ) @HiltViewModel -class LoginViewModel @Inject constructor( +class OnboardingViewModel @Inject constructor( private val datastoreRepository: DatastoreRepository, private val eventTracker: EventTracker, private val networker: Networker, @@ -70,6 +70,9 @@ class LoginViewModel @Inject constructor( var isLoading by mutableStateOf(false) private set + private val _navigateToCreateUser = MutableStateFlow(false) + val navigateToCreateUser: StateFlow get() = _navigateToCreateUser.asStateFlow() + private val _errorMessage = MutableStateFlow(null) val errorMessage: StateFlow get() = _errorMessage.asStateFlow() @@ -79,8 +82,8 @@ class LoginViewModel @Inject constructor( var usernameValidationErrorMessage by mutableStateOf(null) private set - var pendingEmailUserCreds by mutableStateOf(null) - private set + private val _pendingEmailUserCreds = MutableStateFlow(null) + val pendingEmailUserCreds: StateFlow get() = _pendingEmailUserCreds.asStateFlow() val hasAuthTokenState: StateFlow = datastoreRepository.hasAuthTokenFlow.distinctUntilChanged().stateIn( @@ -109,6 +112,14 @@ class LoginViewModel @Inject constructor( } } + fun onNavigateToCreateUserHandled() { + _navigateToCreateUser.value = false + } + + fun onNavigateToEmailConfirmationHandled() { + _pendingEmailUserCreds.value = null + } + fun resetSelfHostingDetails(context: Context) { viewModelScope.launch { datastoreRepository.clearValue(omnivoreSelfHostedApiServer) @@ -123,7 +134,7 @@ class LoginViewModel @Inject constructor( private fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) { resetState() - pendingEmailUserCreds = pendingCreds + setPendingEmailUserCreds(pendingCreds) } fun cancelNewUserSignUp() { @@ -131,7 +142,6 @@ class LoginViewModel @Inject constructor( viewModelScope.launch { datastoreRepository.clearValue(omnivorePendingUserToken) } - resetState() } fun registerUser() { @@ -143,6 +153,14 @@ class LoginViewModel @Inject constructor( } } + private fun setPendingEmailUserCreds(pendingCreds: PendingEmailUserCreds? = null) { + _pendingEmailUserCreds.value = pendingCreds + } + + private fun resetPendingEmailUserCreds() { + _pendingEmailUserCreds.value = null + } + private fun setErrorMessage(message: String) { _errorMessage.value = message } @@ -157,7 +175,7 @@ class LoginViewModel @Inject constructor( resetErrorMessage() hasValidUsername = false usernameValidationErrorMessage = null - pendingEmailUserCreds = null + resetPendingEmailUserCreds() } fun validateUsername(potentialUsername: String) { @@ -280,7 +298,7 @@ class LoginViewModel @Inject constructor( if (result.errorBody() != null) { setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg)) } else { - pendingEmailUserCreds = PendingEmailUserCreds(email, password) + setPendingEmailUserCreds(PendingEmailUserCreds(email, password)) } } } @@ -410,7 +428,7 @@ class LoginViewModel @Inject constructor( datastoreRepository.putString( omnivorePendingUserToken, result.body()?.pendingUserToken!! ) - // TODO go to pending user + _navigateToCreateUser.value = true // TODO go to pending user } else { setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg)) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/AuthProviderScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/AuthProviderScreen.kt index 2c39e523e..1bc1e64e2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/AuthProviderScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/AuthProviderScreen.kt @@ -21,10 +21,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import app.omnivore.omnivore.R -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import app.omnivore.omnivore.feature.onboarding.auth.provider.AppleAuthButton import app.omnivore.omnivore.feature.onboarding.auth.provider.GoogleAuthButton import app.omnivore.omnivore.navigation.Routes @@ -32,9 +31,8 @@ import com.google.android.gms.common.GoogleApiAvailability @Composable fun AuthProviderScreen( - navController: NavHostController, welcomeNavController: NavHostController, - viewModel: LoginViewModel = hiltViewModel() + viewModel: OnboardingViewModel ) { val isGoogleAuthAvailable: Boolean = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(LocalContext.current) == 0 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/CreateUserProfileScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/CreateUserProfileScreen.kt index 269c1b7e8..4d18ed80e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/CreateUserProfileScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/CreateUserProfileScreen.kt @@ -8,17 +8,19 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,34 +32,39 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController import app.omnivore.omnivore.R -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel @Composable -fun CreateUserProfileScreen(viewModel: LoginViewModel) { +fun CreateUserScreen( + viewModel: OnboardingViewModel, + onboardingNavController: NavHostController +) { var name by rememberSaveable { mutableStateOf("") } var username by rememberSaveable { mutableStateOf("") } Row( - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(bottom = 64.dp) ) { Spacer(modifier = Modifier.weight(1.0F)) Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( text = stringResource(R.string.create_user_profile_title), - style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 8.dp) ) - UserProfileFields(name = name, + UserProfileFields( + name = name, username = username, usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage, showUsernameAsAvailable = viewModel.hasValidUsername, @@ -66,16 +73,21 @@ fun CreateUserProfileScreen(viewModel: LoginViewModel) { username = it viewModel.validateUsername(it) }, - onSubmit = { viewModel.submitProfile(username = username, name = name) }) + onSubmit = { viewModel.submitProfile(username = username, name = name) }, + isLoading = viewModel.isLoading + ) - // TODO: add a activity indicator (maybe after a delay?) - if (viewModel.isLoading) { - Text(stringResource(R.string.create_user_profile_loading)) + TextButton( + onClick = { + viewModel.cancelNewUserSignUp() + onboardingNavController.popBackStack() + } + ) { + Text( + text = stringResource(R.string.create_user_profile_action_cancel), + textDecoration = TextDecoration.Underline + ) } - - ClickableText(text = AnnotatedString(stringResource(R.string.create_user_profile_action_cancel)), - style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)), - onClick = { viewModel.cancelNewUserSignUp() }) } Spacer(modifier = Modifier.weight(1.0F)) } @@ -89,7 +101,8 @@ fun UserProfileFields( showUsernameAsAvailable: Boolean, onNameChange: (String) -> Unit, onUsernameChange: (String) -> Unit, - onSubmit: () -> Unit + onSubmit: () -> Unit, + isLoading: Boolean ) { val context = LocalContext.current val focusManager = LocalFocusManager.current @@ -97,48 +110,51 @@ fun UserProfileFields( Column( modifier = Modifier .fillMaxWidth() - .height(300.dp), - verticalArrangement = Arrangement.spacedBy(25.dp), + .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { OutlinedTextField( value = name, + onValueChange = onNameChange, + modifier = Modifier.fillMaxWidth(), placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_name)) }, label = { Text(stringResource(R.string.create_user_profile_field_label_name)) }, - onValueChange = onNameChange, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) ) - Column( - verticalArrangement = Arrangement.spacedBy(5.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - OutlinedTextField(value = username, - placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_username)) }, - label = { Text(stringResource(R.string.create_user_profile_field_label_username)) }, - onValueChange = onUsernameChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - trailingIcon = { - if (showUsernameAsAvailable) { - Icon( - imageVector = Icons.Filled.CheckCircle, contentDescription = null - ) - } - }) - - if (usernameValidationErrorMessage != null) { - Text( - text = usernameValidationErrorMessage, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) + OutlinedTextField( + value = username, + onValueChange = onUsernameChange, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp), + placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_username)) }, + label = { Text(stringResource(R.string.create_user_profile_field_label_username)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + trailingIcon = { + if (showUsernameAsAvailable) { + Icon( + imageVector = Icons.Filled.CheckCircle, contentDescription = null + ) + } + }, + isError = usernameValidationErrorMessage != null, + supportingText = { + if (usernameValidationErrorMessage != null) { + Text( + text = usernameValidationErrorMessage, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + } } - } + ) - Button( + OutlinedButton( + modifier = Modifier.fillMaxWidth(), onClick = { if (name.isNotBlank() && username.isNotBlank()) { onSubmit() @@ -158,6 +174,16 @@ fun UserProfileFields( text = stringResource(R.string.create_user_profile_action_submit), modifier = Modifier.padding(horizontal = 100.dp) ) + if (isLoading) { + Spacer(modifier = Modifier.width(16.dp)) + CircularProgressIndicator( + modifier = Modifier + .height(16.dp) + .width(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary + ) + } } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailConfirmationScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailConfirmationScreen.kt new file mode 100644 index 000000000..4bdd7e929 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailConfirmationScreen.kt @@ -0,0 +1,89 @@ +package app.omnivore.omnivore.feature.onboarding.auth + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import app.omnivore.omnivore.R +import app.omnivore.omnivore.core.designsystem.theme.Success +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel + +@Composable +fun EmailConfirmationScreen( + viewModel: OnboardingViewModel, + onboardingNavController: NavHostController +) { + val email = viewModel.pendingEmailUserCreds.value?.email ?: "" + val password = viewModel.pendingEmailUserCreds.value?.password ?: "" + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(bottom = 64.dp) + ) { + Spacer(modifier = Modifier.weight(1.0F)) + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.email_signup_verification_message, email), + color = Success, + style = MaterialTheme.typography.titleMedium + ) + + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + onClick = { + viewModel.login(email, password) + }, colors = ButtonDefaults.buttonColors( + contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234) + ) + ) { + Text( + text = stringResource(R.string.email_signup_check_status).uppercase() + ) + if (viewModel.isLoading) { + Spacer(modifier = Modifier.width(16.dp)) + CircularProgressIndicator( + modifier = Modifier + .height(16.dp) + .width(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary + ) + } + } + + TextButton( + onClick = { + viewModel.resetState() + onboardingNavController.popBackStack() + } + ){ + Text( + text = stringResource(R.string.email_signup_action_use_different_email), + textDecoration = TextDecoration.Underline + ) + } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignInScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignInScreen.kt index da2c0e922..4102859e9 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignInScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignInScreen.kt @@ -1,6 +1,5 @@ package app.omnivore.omnivore.feature.onboarding.auth -import android.annotation.SuppressLint import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -12,10 +11,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -37,34 +36,30 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import app.omnivore.omnivore.R import app.omnivore.omnivore.core.designsystem.component.DividerWithText -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import app.omnivore.omnivore.feature.theme.OmnivoreTheme -import app.omnivore.omnivore.utils.AuthUtils.autofill import app.omnivore.omnivore.navigation.Routes +import app.omnivore.omnivore.utils.AuthUtils.autofill import app.omnivore.omnivore.utils.FORGOT_PASSWORD_URL -@SuppressLint("CoroutineCreationDuringComposition") @Composable fun EmailSignInScreen( - navController: NavHostController, - welcomeNavController: NavHostController + onboardingNavController: NavHostController, + viewModel: OnboardingViewModel ) { OmnivoreTheme(darkTheme = false) { - EmailSignInContent(navController, welcomeNavController) + EmailSignInContent(onboardingNavController, viewModel) } } @Composable fun EmailSignInContent( - navController: NavHostController, - welcomeNavController: NavHostController, - viewModel: LoginViewModel = hiltViewModel() + onboardingNavController: NavHostController, + viewModel: OnboardingViewModel ) { - val uriHandler = LocalUriHandler.current var email by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } @@ -84,7 +79,7 @@ fun EmailSignInContent( onPasswordChange = { password = it }, onLoginClick = { viewModel.login(email, password) }, onCreateAccountClick = { - welcomeNavController.navigate(Routes.EmailSignUp.route) + onboardingNavController.navigate(Routes.EmailSignUp.route) viewModel.resetState() }, isLoading = viewModel.isLoading @@ -162,7 +157,7 @@ fun LoginFields( } } - Button( + OutlinedButton( modifier = Modifier.fillMaxWidth(), enabled = email.isNotBlank() && password.isNotBlank(), onClick = { @@ -197,12 +192,9 @@ fun LoginFields( DividerWithText(text = "or") - Button( + OutlinedButton( modifier = Modifier.fillMaxWidth(), - onClick = { onCreateAccountClick() }, - colors = ButtonDefaults.buttonColors( - contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234) - ) + onClick = { onCreateAccountClick() } ) { Text( text = "Create Account".uppercase() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignUpScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignUpScreen.kt index 8b92e377b..f6be9d011 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignUpScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/EmailSignUpScreen.kt @@ -1,6 +1,5 @@ package app.omnivore.omnivore.feature.onboarding.auth -import android.annotation.SuppressLint import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -10,16 +9,15 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -35,67 +33,25 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import app.omnivore.omnivore.R -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import app.omnivore.omnivore.utils.AuthUtils.autofill @Composable fun EmailSignUpScreen( - viewModel: LoginViewModel = hiltViewModel() + viewModel: OnboardingViewModel ) { - if (viewModel.pendingEmailUserCreds != null) { - val email = viewModel.pendingEmailUserCreds?.email ?: "" - val password = viewModel.pendingEmailUserCreds?.password ?: "" - - Row( - horizontalArrangement = Arrangement.Center - ) { - Spacer(modifier = Modifier.weight(1.0F)) - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.email_signup_verification_message, email), - style = MaterialTheme.typography.titleMedium - ) - - Button( - onClick = { - viewModel.login(email, password) - }, colors = ButtonDefaults.buttonColors( - contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234) - ) - ) { - Text( - text = stringResource(R.string.email_signup_check_status), - modifier = Modifier.padding(horizontal = 100.dp) - ) - } - - ClickableText( - text = AnnotatedString(stringResource(R.string.email_signup_action_use_different_email)), - style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)), - onClick = { viewModel.resetState() } - ) - } - } - } else { - EmailSignUpForm(viewModel = viewModel) - } + EmailSignUpForm(viewModel = viewModel) } -@SuppressLint("CoroutineCreationDuringComposition") @Composable -fun EmailSignUpForm(viewModel: LoginViewModel) { +fun EmailSignUpForm( + viewModel: OnboardingViewModel +) { var email by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } var name by rememberSaveable { mutableStateOf("") } @@ -224,7 +180,7 @@ fun EmailSignUpFields( } ) - Button( + OutlinedButton( modifier = Modifier.fillMaxWidth(), onClick = { if (email.isNotBlank() && password.isNotBlank() && username.isNotBlank() && name.isNotBlank()) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/SelfHostedScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/SelfHostedScreen.kt index da174a133..44b7a41c0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/SelfHostedScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/SelfHostedScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme @@ -42,12 +41,12 @@ import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import app.omnivore.omnivore.R import app.omnivore.omnivore.core.designsystem.component.DividerWithText -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import app.omnivore.omnivore.utils.SELF_HOSTING_URL @Composable fun SelfHostedScreen( - viewModel: LoginViewModel = hiltViewModel() + viewModel: OnboardingViewModel = hiltViewModel() ) { var apiServer by rememberSaveable { mutableStateOf("") } var webServer by rememberSaveable { mutableStateOf("") } @@ -138,7 +137,7 @@ fun SelfHostedFields( keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) ) - Button( + OutlinedButton( modifier = Modifier.fillMaxWidth(), onClick = { if (apiServer.isNotBlank() && webServer.isNotBlank()) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/AppleAuthButton.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/AppleAuthButton.kt index 54b5005db..d1c77a19a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/AppleAuthButton.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/AppleAuthButton.kt @@ -30,12 +30,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import app.omnivore.omnivore.R -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import app.omnivore.omnivore.utils.AppleConstants import java.net.URLEncoder @Composable -fun AppleAuthButton(viewModel: LoginViewModel) { +fun AppleAuthButton(viewModel: OnboardingViewModel) { val showDialog = remember { mutableStateOf(false) } OutlinedButton( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/GoogleAuthButton.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/GoogleAuthButton.kt index db59802b3..e97cb2d40 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/GoogleAuthButton.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/onboarding/auth/provider/GoogleAuthButton.kt @@ -25,14 +25,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.omnivore.omnivore.BuildConfig import app.omnivore.omnivore.R -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.tasks.Task @Composable -fun GoogleAuthButton(viewModel: LoginViewModel) { +fun GoogleAuthButton(viewModel: OnboardingViewModel) { val context = LocalContext.current val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/profile/ProfileScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/profile/ProfileScreen.kt index 9282ca60e..b3f5c8c57 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/profile/ProfileScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/profile/ProfileScreen.kt @@ -18,7 +18,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import app.omnivore.omnivore.R import app.omnivore.omnivore.core.designsystem.component.TextPreferenceWidget -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import app.omnivore.omnivore.navigation.Routes internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/releases" @@ -27,7 +27,7 @@ internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/relea @Composable internal fun SettingsScreen( navController: NavHostController, - loginViewModel: LoginViewModel = hiltViewModel() + onboardingViewModel: OnboardingViewModel = hiltViewModel() ) { Scaffold(topBar = { TopAppBar( @@ -38,7 +38,7 @@ internal fun SettingsScreen( ) }) { paddingValues -> SettingsViewContent( - loginViewModel = loginViewModel, + onboardingViewModel = onboardingViewModel, navController = navController, paddingValues = paddingValues ) @@ -47,7 +47,7 @@ internal fun SettingsScreen( @Composable fun SettingsViewContent( - loginViewModel: LoginViewModel, + onboardingViewModel: OnboardingViewModel, navController: NavHostController, paddingValues: PaddingValues ) { @@ -94,7 +94,7 @@ fun SettingsViewContent( if (showLogoutDialog.value) { LogoutDialog { performLogout -> if (performLogout) { - loginViewModel.logout() + onboardingViewModel.logout() } showLogoutDialog.value = false } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt index eab690fd5..3d64ef757 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt @@ -38,7 +38,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import app.omnivore.omnivore.core.designsystem.theme.OmnivoreBrand -import app.omnivore.omnivore.feature.onboarding.LoginViewModel +import app.omnivore.omnivore.feature.onboarding.OnboardingViewModel import app.omnivore.omnivore.feature.onboarding.OnboardingScreen import app.omnivore.omnivore.feature.following.FollowingScreen import app.omnivore.omnivore.feature.library.LibraryView @@ -54,14 +54,14 @@ import app.omnivore.omnivore.navigation.TopLevelDestination @Composable fun RootView( - loginViewModel: LoginViewModel = hiltViewModel() + onboardingViewModel: OnboardingViewModel = hiltViewModel() ) { val snackbarHostState = remember { SnackbarHostState() } val navController = rememberNavController() - val followingTabActive by loginViewModel.followingTabActiveState.collectAsStateWithLifecycle() - val hasAuthToken by loginViewModel.hasAuthTokenState.collectAsStateWithLifecycle() + val followingTabActive by onboardingViewModel.followingTabActiveState.collectAsStateWithLifecycle() + val hasAuthToken by onboardingViewModel.hasAuthTokenState.collectAsStateWithLifecycle() val destinations = if (followingTabActive) { TopLevelDestination.entries @@ -69,7 +69,9 @@ fun RootView( TopLevelDestination.entries.filter { it.route != Routes.Following.route } } - Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = { + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { if (navController.currentBackStackEntryAsState().value?.destination?.route in TopLevelDestination.entries.map { it.route }) { OmnivoreBottomBar( navController, @@ -97,7 +99,7 @@ fun RootView( ) LaunchedEffect(hasAuthToken) { if (hasAuthToken) { - loginViewModel.registerUser() + onboardingViewModel.registerUser() } } } @@ -119,7 +121,7 @@ fun PrimaryNavigator( ) { composable(Routes.Welcome.route) { - OnboardingScreen(navController = navController) + OnboardingScreen() } navigation( @@ -217,8 +219,3 @@ private fun OmnivoreBottomBar( } } } - -private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) = - this?.hierarchy?.any { - it.route?.contains(destination.name, true) ?: false - } ?: false diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt index 5a434ec03..b22f31e14 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt @@ -5,7 +5,9 @@ sealed class Routes(val route: String) { data object Welcome : Routes("Welcome") data object EmailSignIn : Routes("EmailSignIn") data object EmailSignUp : Routes("EmailSignUp") + data object EmailConfirmation : Routes("EmailConfirmation") data object SelfHosting : Routes("SelfHosting") + data object CreateUser : Routes("CreateUser") data object AuthProvider : Routes("AuthProvider") data object Following : Routes("Following") data object Inbox : Routes("Inbox") diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index 3e0d3263e..4bb7e948f 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -49,7 +49,7 @@ We\'ve sent a verification email to %1$s. Please verify your email and then tap the button below. Check Status - Use a different email? + Use a different email? Back Loading... Return to Social Login Already have an account?