add user creation screen to onboarding navhost

This commit is contained in:
Stefano Sansone
2024-06-23 21:48:00 +02:00
parent f25acf0628
commit 6165dfe203
15 changed files with 292 additions and 187 deletions

View File

@ -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)

View File

@ -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
)
}
}
}

View File

@ -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<Boolean> get() = _navigateToCreateUser.asStateFlow()
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> get() = _errorMessage.asStateFlow()
@ -79,8 +82,8 @@ class LoginViewModel @Inject constructor(
var usernameValidationErrorMessage by mutableStateOf<String?>(null)
private set
var pendingEmailUserCreds by mutableStateOf<PendingEmailUserCreds?>(null)
private set
private val _pendingEmailUserCreds = MutableStateFlow<PendingEmailUserCreds?>(null)
val pendingEmailUserCreds: StateFlow<PendingEmailUserCreds?> get() = _pendingEmailUserCreds.asStateFlow()
val hasAuthTokenState: StateFlow<Boolean> =
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))
}

View File

@ -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

View File

@ -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
)
}
}
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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()

View File

@ -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()) {

View File

@ -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()) {

View File

@ -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(

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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")

View File

@ -49,7 +49,7 @@
<!-- Email Sign Up -->
<string name="email_signup_verification_message">We\'ve sent a verification email to %1$s. Please verify your email and then tap the button below.</string>
<string name="email_signup_check_status">Check Status</string>
<string name="email_signup_action_use_different_email">Use a different email?</string>
<string name="email_signup_action_use_different_email">Use a different email? Back</string>
<string name="email_signup_loading">Loading...</string>
<string name="email_signup_action_back">Return to Social Login</string>
<string name="email_signup_action_already_have_account">Already have an account?</string>