add user creation screen to onboarding navhost
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user