refactor onboarding

This commit is contained in:
Stefano Sansone
2024-06-23 19:53:19 +02:00
parent 1c9626eabf
commit f25acf0628
29 changed files with 1444 additions and 1198 deletions

View File

@ -3,10 +3,11 @@ import java.util.Properties
plugins {
alias(libs.plugins.android.application)
kotlin("android")
alias(libs.plugins.org.jetbrains.kotlin.android)
id("dagger.hilt.android.plugin")
alias(libs.plugins.ksp)
alias(libs.plugins.apollo)
alias(libs.plugins.compose.compiler)
}
val keystorePropertiesFile = rootProject.file("app/external/keystore.properties")
@ -90,9 +91,7 @@ android {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
}
packaging {
resources {
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")

View File

@ -0,0 +1,43 @@
package app.omnivore.omnivore.core.designsystem.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
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.unit.dp
@Composable
fun DividerWithText(text: String) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
HorizontalDivider(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp),
thickness = 1.dp,
color = Color.Black
)
Text(
text = text,
color = Color.Black,
)
HorizontalDivider(
modifier = Modifier
.weight(1f)
.width(50.dp)
.padding(start = 8.dp),
thickness = 1.dp,
color = Color.Black
)
}
}

View File

@ -0,0 +1,5 @@
package app.omnivore.omnivore.core.designsystem.theme
import androidx.compose.ui.graphics.Color
val OmnivoreBrand = Color(0xFFFCEBA8)

View File

@ -1,96 +0,0 @@
package app.omnivore.omnivore.feature.auth
import android.annotation.SuppressLint
import android.net.Uri
import android.view.ViewGroup
import android.webkit.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import app.omnivore.omnivore.utils.AppleConstants
import app.omnivore.omnivore.R
import java.net.URLEncoder
import java.util.*
@Composable
fun AppleAuthButton(viewModel: LoginViewModel) {
val showDialog = remember { mutableStateOf(false) }
LoadingButtonWithIcon(
text = stringResource(R.string.apple_auth_text),
loadingText = stringResource(R.string.apple_auth_loading),
isLoading = viewModel.isLoading,
icon = painterResource(id = R.drawable.ic_logo_apple),
modifier = Modifier.padding(vertical = 6.dp),
onClick = { showDialog.value = true }
)
if (showDialog.value) {
AppleAuthDialog(onDismiss = { token ->
if (token != null ) {
viewModel.handleAppleToken(token)
}
showDialog.value = false
})
}
}
@Composable
fun AppleAuthDialog(onDismiss: (String?) -> Unit) {
Dialog(onDismissRequest = { onDismiss(null) }) {
Surface(
shape = RoundedCornerShape(16.dp),
color = Color.White
) {
AppleAuthWebView(onDismiss)
}
}
}
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun AppleAuthWebView(onDismiss: (String?) -> Unit) {
val url = AppleConstants.authUrl +
"?client_id=" + AppleConstants.clientId +
"&redirect_uri=" + URLEncoder.encode(AppleConstants.redirectURI, "utf8") +
"&response_type=code%20id_token" +
"&scope=" + AppleConstants.scope +
"&response_mode=form_post" +
"&state=android:login"
// Adding a WebView inside AndroidView
// with layout as full screen
AndroidView(factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
if (request?.url.toString().contains("android-apple-token")) {
val uri = Uri.parse(request!!.url.toString())
val token = uri.getQueryParameter("token")
onDismiss(token)
}
return true
}
}
settings.javaScriptEnabled = true
loadUrl(url)
}
}, update = {
it.loadUrl(url)
})
}

View File

@ -1,159 +0,0 @@
package app.omnivore.omnivore.feature.auth
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.layout.*
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.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.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 app.omnivore.omnivore.R
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun CreateUserProfileView(viewModel: LoginViewModel) {
var name by rememberSaveable { mutableStateOf("") }
var username by rememberSaveable { mutableStateOf("") }
Row(
horizontalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.create_user_profile_title),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
UserProfileFields(
name = name,
username = username,
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
showUsernameAsAvailable = viewModel.hasValidUsername,
onNameChange = { name = it },
onUsernameChange = {
username = it
viewModel.validateUsername(it)
},
onSubmit = { viewModel.submitProfile(username = username, name = name) }
)
// TODO: add a activity indicator (maybe after a delay?)
if (viewModel.isLoading) {
Text(stringResource(R.string.create_user_profile_loading))
}
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))
}
}
@Composable
fun UserProfileFields(
name: String,
username: String,
usernameValidationErrorMessage: String?,
showUsernameAsAvailable: Boolean,
onNameChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onSubmit: () -> Unit
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
verticalArrangement = Arrangement.spacedBy(25.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = name,
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
)
}
}
Button(
onClick = {
if (name.isNotBlank() && username.isNotBlank()) {
onSubmit()
focusManager.clearFocus()
} else {
Toast.makeText(
context,
context.getString(R.string.create_user_profile_error_msg),
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
containerColor = Color(0xffffd234)
)
) {
Text(
text = stringResource(R.string.create_user_profile_action_submit),
modifier = Modifier.padding(horizontal = 100.dp)
)
}
}
}

View File

@ -1,171 +0,0 @@
package app.omnivore.omnivore.feature.auth
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalUriHandler
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.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import app.omnivore.omnivore.BuildConfig
import app.omnivore.omnivore.R
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun EmailLoginView(viewModel: LoginViewModel) {
val uriHandler = LocalUriHandler.current
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
Row(
horizontalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LoginFields(
email,
password,
onEmailChange = { email = it },
onPasswordChange = { password = it },
onLoginClick = { viewModel.login(email, password) }
)
// TODO: add a activity indicator (maybe after a delay?)
if (viewModel.isLoading) {
Text(stringResource(R.string.email_login_loading))
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ClickableText(
text = AnnotatedString(stringResource(R.string.email_login_action_back)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showSocialLogin() }
)
ClickableText(
text = AnnotatedString(stringResource(R.string.email_login_action_no_account)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showEmailSignUp() }
)
ClickableText(
text = AnnotatedString(stringResource(R.string.email_login_action_forgot_password)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = {
val uri = "${BuildConfig.OMNIVORE_WEB_URL}/auth/forgot-password"
uriHandler.openUri(uri)
}
)
}
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun LoginFields(
email: String,
password: String,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onLoginClick: () -> Unit
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
verticalArrangement = Arrangement.spacedBy(25.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
modifier = Modifier.autofill(
autofillTypes = listOf(
AutofillType.EmailAddress,
),
onFill = { onEmailChange(it) }
),
value = email,
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_email)) },
label = { Text(stringResource(R.string.email_login_field_label_email)) },
onValueChange = onEmailChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Email,
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(
modifier = Modifier.autofill(
autofillTypes = listOf(
AutofillType.Password,
),
onFill = { onPasswordChange(it) }
),
value = password,
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_password)) },
label = { Text(stringResource(R.string.email_login_field_label_password)) },
onValueChange = onPasswordChange,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Password,
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
Button(
onClick = {
if (email.isNotBlank() && password.isNotBlank()) {
onLoginClick()
focusManager.clearFocus()
} else {
Toast.makeText(
context,
context.getString(R.string.email_login_error_msg),
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
containerColor = Color(0xffffd234)
)
) {
Text(
text = stringResource(R.string.email_login_action_login),
modifier = Modifier.padding(horizontal = 100.dp)
)
}
}
}

View File

@ -1,264 +0,0 @@
package app.omnivore.omnivore.feature.auth
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
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 app.omnivore.omnivore.R
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
@Composable
fun EmailSignUpView(viewModel: LoginViewModel) {
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.showEmailSignUp() }
)
}
}
} else {
EmailSignUpForm(viewModel = viewModel)
}
}
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun EmailSignUpForm(viewModel: LoginViewModel) {
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
var name by rememberSaveable { mutableStateOf("") }
var username by rememberSaveable { mutableStateOf("") }
Row(
horizontalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailSignUpFields(
email = email,
password = password,
name = name,
username = username,
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
showUsernameAsAvailable = viewModel.hasValidUsername,
onEmailChange = { email = it },
onPasswordChange = { password = it },
onNameChange = { name = it },
onUsernameChange = {
username = it
viewModel.validateUsername(it)
},
onSubmit = {
viewModel.submitEmailSignUp(
email = email,
password = password,
username = username,
name = name
)
}
)
// TODO: add a activity indicator (maybe after a delay?)
if (viewModel.isLoading) {
Text(stringResource(R.string.email_signup_loading))
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ClickableText(
text = AnnotatedString(stringResource(R.string.email_signup_action_back)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showSocialLogin() }
)
ClickableText(
text = AnnotatedString(stringResource(R.string.email_signup_action_already_have_account)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showEmailSignIn() }
)
}
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun EmailSignUpFields(
email: String,
password: String,
name: String,
username: String,
usernameValidationErrorMessage: String?,
showUsernameAsAvailable: Boolean,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onNameChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onSubmit: () -> Unit,
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(25.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
modifier = Modifier.autofill(
autofillTypes = listOf(
AutofillType.EmailAddress,
),
onFill = { onEmailChange(it) }
),
value = email,
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_email)) },
label = { Text(stringResource(R.string.email_signup_field_label_email)) },
onValueChange = onEmailChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(
modifier = Modifier.autofill(
autofillTypes = listOf(
AutofillType.Password,
),
onFill = { onPasswordChange(it) }
),
value = password,
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_password)) },
label = { Text(stringResource(R.string.email_signup_field_label_password)) },
onValueChange = onPasswordChange,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(
value = name,
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_name)) },
label = { Text(stringResource(R.string.email_signup_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.email_signup_field_placeholder_username)) },
label = { Text(stringResource(R.string.email_signup_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
)
}
}
Button(onClick = {
if (email.isNotBlank() && password.isNotBlank() && username.isNotBlank() && name.isNotBlank()) {
onSubmit()
focusManager.clearFocus()
} else {
Toast.makeText(
context,
context.getString(R.string.email_signup_error_msg),
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
containerColor = Color(0xffffd234)
)
) {
Text(
text = stringResource(R.string.email_signup_action_sign_up),
modifier = Modifier.padding(horizontal = 100.dp)
)
}
}
}

View File

@ -1,62 +0,0 @@
package app.omnivore.omnivore.feature.auth
import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import app.omnivore.omnivore.BuildConfig
import app.omnivore.omnivore.R
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) {
val context = LocalContext.current
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(BuildConfig.OMNIVORE_GAUTH_SERVER_CLIENT_ID)
.requestEmail()
.build()
val startForResult =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
if (result.data != null) {
val task: Task<GoogleSignInAccount> = GoogleSignIn.getSignedInAccountFromIntent(intent)
viewModel.handleGoogleAuthTask(task)
}
} else {
viewModel.showGoogleErrorMessage()
}
}
LoadingButtonWithIcon(
text = stringResource(R.string.google_auth_text),
loadingText = stringResource(R.string.google_auth_loading),
isLoading = viewModel.isLoading,
icon = painterResource(id = R.drawable.ic_logo_google),
onClick = {
val googleSignIn = GoogleSignIn.getClient(context, signInOptions)
googleSignIn.silentSignIn()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
viewModel.handleGoogleAuthTask(task)
} else {
startForResult.launch(googleSignIn.signInIntent)
}
}
.addOnFailureListener {
startForResult.launch(googleSignIn.signInIntent)
}
}
)
}

View File

@ -1,69 +0,0 @@
package app.omnivore.omnivore.feature.auth
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
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.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
@Composable
fun LoadingButtonWithIcon(
modifier: Modifier = Modifier,
text: String,
loadingText: String,
icon: Painter,
isLoading: Boolean = false,
shape: Shape = Shapes().medium,
borderColor: Color = Color.LightGray,
backgroundColor: Color = MaterialTheme.colorScheme.surface,
progressIndicatorColor: Color = MaterialTheme.colorScheme.primary,
onClick: () -> Unit
) {
Surface(
modifier = modifier.clickable(
enabled = !isLoading,
onClick = onClick
),
shape = shape,
border = BorderStroke(width = 1.dp, color = borderColor),
color = backgroundColor
) {
Row(
modifier = Modifier
.padding(
start = 12.dp,
end = 16.dp,
top = 12.dp,
bottom = 12.dp
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(
painter = icon,
contentDescription = "SignInButton",
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = if (isLoading) loadingText else text)
if (isLoading) {
Spacer(modifier = Modifier.width(16.dp))
CircularProgressIndicator(
modifier = Modifier
.height(16.dp)
.width(16.dp),
strokeWidth = 2.dp,
color = progressIndicatorColor
)
}
}
}
}

View File

@ -1,174 +0,0 @@
package app.omnivore.omnivore.feature.auth
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import app.omnivore.omnivore.R
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
import com.google.android.gms.common.GoogleApiAvailability
@Composable
fun WelcomeScreen(viewModel: LoginViewModel) {
OmnivoreTheme(darkTheme = false) {
Surface(
modifier = Modifier.fillMaxSize()
) {
WelcomeScreenContent(viewModel = viewModel)
}
}
}
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun WelcomeScreenContent(viewModel: LoginViewModel) {
val registrationState: RegistrationState by viewModel.registrationStateLiveData.observeAsState(
RegistrationState.SocialLogin
)
val snackBarHostState = remember { SnackbarHostState() }
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(hostState = snackBarHostState) },
containerColor = Color(0xFFFCEBA8)
) { paddingValues ->
Column(
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.Start,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.padding(paddingValues)
) {
Spacer(modifier = Modifier.height(50.dp))
Image(
painter = painterResource(id = R.drawable.ic_omnivore_name_logo),
contentDescription = "Omnivore Icon with Name"
)
Spacer(modifier = Modifier.height(50.dp))
when (registrationState) {
RegistrationState.EmailSignIn -> {
EmailLoginView(viewModel = viewModel)
}
RegistrationState.EmailSignUp -> {
EmailSignUpView(viewModel = viewModel)
}
RegistrationState.SelfHosted -> {
SelfHostedView(viewModel = viewModel)
}
RegistrationState.SocialLogin -> {
Text(
text = stringResource(id = R.string.welcome_title),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineLarge
)
Text(
text = stringResource(id = R.string.welcome_subtitle),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleSmall
)
MoreInfoButton()
Spacer(modifier = Modifier.height(50.dp))
AuthProviderView(viewModel = viewModel)
}
RegistrationState.PendingUser -> {
CreateUserProfileView(viewModel = viewModel)
}
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
LaunchedEffect(viewModel.errorMessage) {
viewModel.errorMessage?.let { message ->
val result = snackBarHostState.showSnackbar(
message,
actionLabel = "Dismiss",
duration = SnackbarDuration.Indefinite
)
when (result) {
SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage()
else -> {}
}
}
}
}
@Composable
fun AuthProviderView(viewModel: LoginViewModel) {
val isGoogleAuthAvailable: Boolean =
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(LocalContext.current) == 0
Row(
horizontalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isGoogleAuthAvailable) {
GoogleAuthButton(viewModel)
}
AppleAuthButton(viewModel)
ClickableText(text = AnnotatedString(stringResource(R.string.welcome_screen_action_continue_with_email)),
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showEmailSignIn() })
Spacer(modifier = Modifier.weight(1.0F))
ClickableText(
text = AnnotatedString(stringResource(R.string.welcome_screen_action_self_hosting_options)),
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showSelfHostedSettings() },
modifier = Modifier.padding(vertical = 10.dp)
)
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
@Composable
fun MoreInfoButton() {
val context = LocalContext.current
val intent = remember { Intent(Intent.ACTION_VIEW, Uri.parse("https://omnivore.app/about")) }
ClickableText(
text = AnnotatedString(
stringResource(id = R.string.learn_more),
),
style = MaterialTheme.typography.titleSmall.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = {
context.startActivity(intent)
},
modifier = Modifier.padding(vertical = 6.dp)
)
}

View File

@ -0,0 +1,213 @@
package app.omnivore.omnivore.feature.onboarding
import android.content.Intent
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDecoration
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.EmailSignInScreen
import app.omnivore.omnivore.feature.onboarding.auth.EmailSignUpScreen
import app.omnivore.omnivore.feature.onboarding.auth.SelfHostedScreen
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
import app.omnivore.omnivore.navigation.OmnivoreNavHost
import app.omnivore.omnivore.navigation.Routes
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OnboardingScreen(
navController: NavHostController,
viewModel: LoginViewModel = hiltViewModel()
) {
val activity = LocalContext.current as ComponentActivity
val welcomeNavController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }
val currentRoute by welcomeNavController.currentBackStackEntryFlow.collectAsState(
initial = welcomeNavController.currentBackStackEntry
)
val errorMessage by viewModel.errorMessage.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 -> {}
}
}
}
Scaffold(
topBar = {
TopAppBar(
title = { },
navigationIcon = {
if (currentRoute?.destination?.route != Routes.AuthProvider.route) {
IconButton(onClick = { welcomeNavController.popBackStack() }) {
Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = OmnivoreBrand
)
)
},
modifier = Modifier
.fillMaxSize()
.imePadding(),
snackbarHost = { SnackbarHost(hostState = snackBarHostState) },
containerColor = OmnivoreBrand
) { paddingValues ->
LazyColumn(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.padding(paddingValues)
) {
item {
WelcomeHeader()
}
item {
OmnivoreNavHost(
navController = welcomeNavController,
startDestination = Routes.AuthProvider.route
) {
composable(Routes.AuthProvider.route) {
AuthProviderScreen(
navController = navController,
welcomeNavController = welcomeNavController
)
}
composable(Routes.EmailSignIn.route) {
EmailSignInScreen(
navController = navController,
welcomeNavController = welcomeNavController
)
}
composable(Routes.EmailSignUp.route) {
EmailSignUpScreen()
}
composable(Routes.SelfHosting.route){
SelfHostedScreen()
}
}
}
}
}
// Set the light status bar
DisposableEffect(Unit) {
val windowInsetsController = WindowInsetsControllerCompat(activity.window, activity.window.decorView)
val originalAppearanceLightStatusBars = windowInsetsController.isAppearanceLightStatusBars
val originalStatusBarColor = activity.window.statusBarColor
// Set light status bar
windowInsetsController.isAppearanceLightStatusBars = true
activity.window.statusBarColor = Color.Transparent.toArgb()
onDispose {
// Restore original status bar settings
windowInsetsController.isAppearanceLightStatusBars = originalAppearanceLightStatusBars
activity.window.statusBarColor = originalStatusBarColor
}
}
}
}
@Composable
fun WelcomeHeader() {
Column(
modifier = Modifier.padding(bottom = 64.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Image(
painter = painterResource(id = R.drawable.ic_omnivore_name_logo),
contentDescription = "Omnivore Icon with Name"
)
Text(
text = stringResource(id = R.string.welcome_title),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineLarge
)
Text(
text = stringResource(id = R.string.welcome_subtitle),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleSmall
)
MoreInfoButton()
}
}
@Composable
fun MoreInfoButton() {
val context = LocalContext.current
val intent = remember { Intent(Intent.ACTION_VIEW, Uri.parse("https://omnivore.app/about")) }
ClickableText(
text = AnnotatedString(
stringResource(id = R.string.learn_more),
),
style = MaterialTheme.typography.titleSmall.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = {
context.startActivity(intent)
},
modifier = Modifier.padding(vertical = 6.dp)
)
}

View File

@ -1,11 +1,10 @@
package app.omnivore.omnivore.feature.auth
package app.omnivore.omnivore.feature.onboarding
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.omnivore.omnivore.BuildConfig
@ -43,8 +42,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import io.intercom.android.sdk.Intercom
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -52,10 +53,6 @@ import kotlinx.coroutines.runBlocking
import java.util.regex.Pattern
import javax.inject.Inject
enum class RegistrationState {
SocialLogin, EmailSignIn, EmailSignUp, PendingUser, SelfHosted
}
data class PendingEmailUserCreds(
val email: String, val password: String
)
@ -73,8 +70,8 @@ class LoginViewModel @Inject constructor(
var isLoading by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> get() = _errorMessage.asStateFlow()
var hasValidUsername by mutableStateOf(false)
private set
@ -92,8 +89,6 @@ class LoginViewModel @Inject constructor(
initialValue = true
)
val registrationStateLiveData = MutableLiveData(RegistrationState.SocialLogin)
val followingTabActiveState: StateFlow<Boolean> = datastoreRepository.getBoolean(
followingTabActive
).stateIn(
@ -124,29 +119,11 @@ class LoginViewModel @Inject constructor(
Toast.LENGTH_SHORT
).show()
}
}
fun showSocialLogin() {
resetState()
registrationStateLiveData.value = RegistrationState.SocialLogin
}
fun showEmailSignIn() {
resetState()
registrationStateLiveData.value = RegistrationState.EmailSignIn
}
fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
private fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
resetState()
pendingEmailUserCreds = pendingCreds
registrationStateLiveData.value = RegistrationState.EmailSignUp
}
fun showSelfHostedSettings() {
resetState()
registrationStateLiveData.value = RegistrationState.SelfHosted
}
fun cancelNewUserSignUp() {
@ -154,7 +131,7 @@ class LoginViewModel @Inject constructor(
viewModelScope.launch {
datastoreRepository.clearValue(omnivorePendingUserToken)
}
showSocialLogin()
resetState()
}
fun registerUser() {
@ -166,10 +143,18 @@ class LoginViewModel @Inject constructor(
}
}
private fun resetState() {
private fun setErrorMessage(message: String) {
_errorMessage.value = message
}
fun resetErrorMessage() {
_errorMessage.value = null
}
fun resetState() {
validateUsernameJob = null
isLoading = false
errorMessage = null
resetErrorMessage()
hasValidUsername = false
usernameValidationErrorMessage = null
pendingEmailUserCreds = null
@ -240,7 +225,7 @@ class LoginViewModel @Inject constructor(
RetrofitHelper.getInstance(networker).create(EmailLoginSubmit::class.java)
isLoading = true
errorMessage = null
resetErrorMessage()
val result = emailLogin.submitEmailLogin(
EmailLoginCredentials(email = email, password = password)
@ -260,9 +245,7 @@ class LoginViewModel @Inject constructor(
if (result.body()?.authToken != null) {
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
} else {
errorMessage = resourceProvider.getString(
R.string.login_view_model_something_went_wrong_error_msg
)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
}
if (result.body()?.authCookieString != null) {
@ -284,7 +267,7 @@ class LoginViewModel @Inject constructor(
RetrofitHelper.getInstance(networker).create(CreateEmailAccountSubmit::class.java)
isLoading = true
errorMessage = null
resetErrorMessage()
val params = EmailSignUpParams(
email = email, password = password, name = name, username = username
@ -295,9 +278,7 @@ class LoginViewModel @Inject constructor(
isLoading = false
if (result.errorBody() != null) {
errorMessage = resourceProvider.getString(
R.string.login_view_model_something_went_wrong_two_error_msg
)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
} else {
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
}
@ -314,7 +295,7 @@ class LoginViewModel @Inject constructor(
RetrofitHelper.getInstance(networker).create(CreateAccountSubmit::class.java)
isLoading = true
errorMessage = null
resetErrorMessage()
val pendingUserToken = getPendingAuthToken() ?: ""
@ -330,9 +311,7 @@ class LoginViewModel @Inject constructor(
if (result.body()?.authToken != null) {
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
} else {
errorMessage = resourceProvider.getString(
R.string.login_view_model_something_went_wrong_error_msg
)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
}
if (result.body()?.authCookieString != null) {
@ -358,12 +337,8 @@ class LoginViewModel @Inject constructor(
}
}
fun resetErrorMessage() {
errorMessage = null
}
fun showGoogleErrorMessage() {
errorMessage = resourceProvider.getString(R.string.login_view_model_google_auth_error_msg)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_google_auth_error_msg))
}
fun handleGoogleAuthTask(task: Task<GoogleSignInAccount>) {
@ -372,9 +347,7 @@ class LoginViewModel @Inject constructor(
// If token is missing then set the error message
if (googleIdToken.isEmpty()) {
errorMessage = resourceProvider.getString(
R.string.login_view_model_missing_auth_token_error_msg
)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_missing_auth_token_error_msg))
return
}
@ -390,7 +363,7 @@ class LoginViewModel @Inject constructor(
RetrofitHelper.getInstance(networker).create(AuthProviderLoginSubmit::class.java)
isLoading = true
errorMessage = null
resetErrorMessage()
val result = login.submitAuthProviderLogin(params)
@ -413,15 +386,11 @@ class LoginViewModel @Inject constructor(
418 -> {
// Show pending email state
errorMessage = resourceProvider.getString(
R.string.login_view_model_something_went_wrong_two_error_msg
)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
}
else -> {
errorMessage = resourceProvider.getString(
R.string.login_view_model_something_went_wrong_two_error_msg
)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
}
}
}
@ -430,7 +399,7 @@ class LoginViewModel @Inject constructor(
private suspend fun submitAuthProviderPayloadForPendingToken(params: SignInParams) {
isLoading = true
errorMessage = null
resetErrorMessage()
val request = RetrofitHelper.getInstance(networker).create(PendingUserSubmit::class.java)
val result = request.submitPendingUser(params)
@ -441,11 +410,9 @@ class LoginViewModel @Inject constructor(
datastoreRepository.putString(
omnivorePendingUserToken, result.body()?.pendingUserToken!!
)
registrationStateLiveData.value = RegistrationState.PendingUser
// TODO go to pending user
} else {
errorMessage = resourceProvider.getString(
R.string.login_view_model_something_went_wrong_two_error_msg
)
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
}
}
}

View File

@ -0,0 +1,93 @@
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
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.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.auth.provider.AppleAuthButton
import app.omnivore.omnivore.feature.onboarding.auth.provider.GoogleAuthButton
import app.omnivore.omnivore.navigation.Routes
import com.google.android.gms.common.GoogleApiAvailability
@Composable
fun AuthProviderScreen(
navController: NavHostController,
welcomeNavController: NavHostController,
viewModel: LoginViewModel = hiltViewModel()
) {
val isGoogleAuthAvailable: Boolean =
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(LocalContext.current) == 0
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(bottom = 64.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.width(500.dp)
) {
if (isGoogleAuthAvailable) {
GoogleAuthButton(viewModel)
}
AppleAuthButton(viewModel)
OutlinedButton(
onClick = {
welcomeNavController.navigate(Routes.EmailSignIn.route)
viewModel.resetState()
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp),
shape = RoundedCornerShape(6.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Text(text = stringResource(R.string.welcome_screen_action_continue_with_email), modifier = Modifier.padding(vertical = 6.dp))
}
Spacer(modifier = Modifier.weight(1.0F))
TextButton(
onClick = {
viewModel.resetState()
welcomeNavController.navigate(Routes.SelfHosting.route)
},
modifier = Modifier.padding(vertical = 10.dp),
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.onSurface
)
){
Text(
text = stringResource(R.string.welcome_screen_action_self_hosting_options),
textDecoration = TextDecoration.Underline
)
}
}
}
}

View File

@ -0,0 +1,163 @@
package app.omnivore.omnivore.feature.onboarding.auth
import android.widget.Toast
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.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.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.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 app.omnivore.omnivore.R
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
@Composable
fun CreateUserProfileScreen(viewModel: LoginViewModel) {
var name by rememberSaveable { mutableStateOf("") }
var username by rememberSaveable { mutableStateOf("") }
Row(
horizontalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.create_user_profile_title),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
UserProfileFields(name = name,
username = username,
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
showUsernameAsAvailable = viewModel.hasValidUsername,
onNameChange = { name = it },
onUsernameChange = {
username = it
viewModel.validateUsername(it)
},
onSubmit = { viewModel.submitProfile(username = username, name = name) })
// TODO: add a activity indicator (maybe after a delay?)
if (viewModel.isLoading) {
Text(stringResource(R.string.create_user_profile_loading))
}
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))
}
}
@Composable
fun UserProfileFields(
name: String,
username: String,
usernameValidationErrorMessage: String?,
showUsernameAsAvailable: Boolean,
onNameChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onSubmit: () -> Unit
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
verticalArrangement = Arrangement.spacedBy(25.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = name,
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
)
}
}
Button(
onClick = {
if (name.isNotBlank() && username.isNotBlank()) {
onSubmit()
focusManager.clearFocus()
} else {
Toast.makeText(
context,
context.getString(R.string.create_user_profile_error_msg),
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
)
) {
Text(
text = stringResource(R.string.create_user_profile_action_submit),
modifier = Modifier.padding(horizontal = 100.dp)
)
}
}
}

View File

@ -0,0 +1,212 @@
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
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.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.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
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.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.theme.OmnivoreTheme
import app.omnivore.omnivore.utils.AuthUtils.autofill
import app.omnivore.omnivore.navigation.Routes
import app.omnivore.omnivore.utils.FORGOT_PASSWORD_URL
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun EmailSignInScreen(
navController: NavHostController,
welcomeNavController: NavHostController
) {
OmnivoreTheme(darkTheme = false) {
EmailSignInContent(navController, welcomeNavController)
}
}
@Composable
fun EmailSignInContent(
navController: NavHostController,
welcomeNavController: NavHostController,
viewModel: LoginViewModel = hiltViewModel()
) {
val uriHandler = LocalUriHandler.current
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(bottom = 64.dp)
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LoginFields(
email,
password,
onEmailChange = { email = it },
onPasswordChange = { password = it },
onLoginClick = { viewModel.login(email, password) },
onCreateAccountClick = {
welcomeNavController.navigate(Routes.EmailSignUp.route)
viewModel.resetState()
},
isLoading = viewModel.isLoading
)
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun LoginFields(
email: String,
password: String,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onLoginClick: () -> Unit,
onCreateAccountClick: () -> Unit,
isLoading: Boolean
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(modifier = Modifier
.autofill(autofillTypes = listOf(
AutofillType.EmailAddress,
), onFill = { onEmailChange(it) })
.fillMaxWidth(),
value = email,
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_email)) },
label = { Text(stringResource(R.string.email_login_field_label_email)) },
onValueChange = onEmailChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Email,
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(modifier = Modifier
.autofill(autofillTypes = listOf(
AutofillType.Password,
), onFill = { onPasswordChange(it) })
.fillMaxWidth(),
value = password,
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_password)) },
label = { Text(stringResource(R.string.email_login_field_label_password)) },
onValueChange = onPasswordChange,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Password,
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
TextButton(
onClick = {
val uri = FORGOT_PASSWORD_URL
uriHandler.openUri(uri)
}
) {
Text(text = stringResource(R.string.forgot_password))
}
}
Button(
modifier = Modifier.fillMaxWidth(),
enabled = email.isNotBlank() && password.isNotBlank(),
onClick = {
if (email.isNotBlank() && password.isNotBlank()) {
onLoginClick()
focusManager.clearFocus()
} else {
Toast.makeText(
context,
context.getString(R.string.email_login_error_msg),
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
)
) {
Text(
text = stringResource(R.string.email_login_action_login).uppercase()
)
if (isLoading) {
Spacer(modifier = Modifier.width(16.dp))
CircularProgressIndicator(
modifier = Modifier
.height(16.dp)
.width(16.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.primary
)
}
}
DividerWithText(text = "or")
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onCreateAccountClick() },
colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
)
) {
Text(
text = "Create Account".uppercase()
)
}
}
}

View File

@ -0,0 +1,259 @@
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
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.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.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
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.utils.AuthUtils.autofill
@Composable
fun EmailSignUpScreen(
viewModel: LoginViewModel = hiltViewModel()
) {
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)
}
}
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun EmailSignUpForm(viewModel: LoginViewModel) {
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
var name by rememberSaveable { mutableStateOf("") }
var username by rememberSaveable { mutableStateOf("") }
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(bottom = 64.dp)
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailSignUpFields(
email = email,
password = password,
name = name,
username = username,
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
showUsernameAsAvailable = viewModel.hasValidUsername,
onEmailChange = { email = it },
onPasswordChange = { password = it },
onNameChange = { name = it },
onUsernameChange = {
username = it
viewModel.validateUsername(it)
},
onSubmit = {
viewModel.submitEmailSignUp(
email = email, password = password, username = username, name = name
)
},
isLoading = viewModel.isLoading
)
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun EmailSignUpFields(
email: String,
password: String,
name: String,
username: String,
usernameValidationErrorMessage: String?,
showUsernameAsAvailable: Boolean,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onNameChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onSubmit: () -> Unit,
isLoading: Boolean
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
modifier = Modifier
.autofill(
autofillTypes = listOf(AutofillType.EmailAddress),
onFill = { onEmailChange(it) }
)
.fillMaxWidth(),
value = email,
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_email)) },
label = { Text(stringResource(R.string.email_signup_field_label_email)) },
onValueChange = onEmailChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(modifier = Modifier.autofill(autofillTypes = listOf(
AutofillType.Password,
), onFill = { onPasswordChange(it) }).fillMaxWidth(),
value = password,
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_password)) },
label = { Text(stringResource(R.string.email_signup_field_label_password)) },
onValueChange = onPasswordChange,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = name,
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_name)) },
label = { Text(stringResource(R.string.email_signup_field_label_name)) },
onValueChange = onNameChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp),
value = username,
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_username)) },
label = { Text(stringResource(R.string.email_signup_field_label_username)) },
onValueChange = onUsernameChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
isError = usernameValidationErrorMessage != null,
trailingIcon = {
if (showUsernameAsAvailable) {
Icon(
imageVector = Icons.Filled.CheckCircle, contentDescription = null
)
}
},
supportingText = {
if (usernameValidationErrorMessage != null) {
Text(
text = usernameValidationErrorMessage,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Left
)
}
}
)
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
if (email.isNotBlank() && password.isNotBlank() && username.isNotBlank() && name.isNotBlank()) {
onSubmit()
focusManager.clearFocus()
} else {
Toast.makeText(
context,
context.getString(R.string.email_signup_error_msg),
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
)
) {
Text(
text = stringResource(R.string.email_signup_action_sign_up).uppercase()
)
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

@ -1,16 +1,31 @@
package app.omnivore.omnivore.feature.auth
package app.omnivore.omnivore.feature.onboarding.auth
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.layout.*
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.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -24,20 +39,27 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
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.utils.SELF_HOSTING_URL
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun SelfHostedView(viewModel: LoginViewModel) {
fun SelfHostedScreen(
viewModel: LoginViewModel = hiltViewModel()
) {
var apiServer by rememberSaveable { mutableStateOf("") }
var webServer by rememberSaveable { mutableStateOf("") }
val context = LocalContext.current
Row(
horizontalArrangement = Arrangement.Center
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(bottom = 64.dp)
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
@ -48,51 +70,25 @@ fun SelfHostedView(viewModel: LoginViewModel) {
onWebServerChange = { webServer = it },
onSaveClick = {
viewModel.setSelfHostingDetails(context, apiServer, webServer)
}
},
onResetClick = { viewModel.resetSelfHostingDetails(context) },
isLoading = viewModel.isLoading
)
// TODO: add a activity indicator (maybe after a delay?)
if (viewModel.isLoading) {
Text(stringResource(R.string.self_hosted_view_loading))
}
Row(
horizontalArrangement = Arrangement.Center,
Column(
modifier = Modifier.padding(top = 16.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ClickableText(
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_reset)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.resetSelfHostingDetails(context) },
modifier = Modifier.align(Alignment.CenterHorizontally)
)
ClickableText(
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_back)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showSocialLogin() },
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.weight(1.0F))
// Text("Omnivore is a free and open-source software project and allows self hosting. \n\n" +
// "If you have chosen to deploy your own server instance, fill in the above fields to " +
// "your private self-hosted instance.\n\n"
// )
ClickableText(
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_learn_more)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = {
val uri = Uri.parse("https://docs.omnivore.app/self-hosting/self-hosting.html")
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(context, browserIntent, null)
},
modifier = Modifier.padding(vertical = 10.dp)
)
}
ClickableText(
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_learn_more)),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = {
val uri = Uri.parse(SELF_HOSTING_URL)
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(context, browserIntent, null)
},
modifier = Modifier.padding(vertical = 10.dp)
)
}
}
Spacer(modifier = Modifier.weight(1.0F))
@ -105,19 +101,19 @@ fun SelfHostedFields(
webServer: String,
onAPIServerChange: (String) -> Unit,
onWebServerChange: (String) -> Unit,
onSaveClick: () -> Unit
onSaveClick: () -> Unit,
onResetClick: () -> Unit,
isLoading: Boolean
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
verticalArrangement = Arrangement.spacedBy(25.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = apiServer,
placeholder = { Text(text = "https://api-prod.omnivore.app/") },
label = { Text(stringResource(R.string.self_hosted_view_field_api_url_label)) },
@ -130,6 +126,7 @@ fun SelfHostedFields(
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp),
value = webServer,
placeholder = { Text(text = "https://omnivore.app/") },
label = { Text(stringResource(R.string.self_hosted_view_field_web_url_label)) },
@ -141,7 +138,9 @@ fun SelfHostedFields(
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
Button(onClick = {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
if (apiServer.isNotBlank() && webServer.isNotBlank()) {
onSaveClick()
focusManager.clearFocus()
@ -158,8 +157,28 @@ fun SelfHostedFields(
)
) {
Text(
text = stringResource(R.string.self_hosted_view_action_save),
modifier = Modifier.padding(horizontal = 100.dp)
text = stringResource(R.string.self_hosted_view_action_save).uppercase(),
)
if (isLoading) {
Spacer(modifier = Modifier.width(16.dp))
CircularProgressIndicator(
modifier = Modifier
.height(16.dp)
.width(16.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.primary
)
}
}
DividerWithText(text = "or")
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = { onResetClick() }
) {
Text(
text = "Reset".uppercase()
)
}
}

View File

@ -0,0 +1,128 @@
package app.omnivore.omnivore.feature.onboarding.auth.provider
import android.annotation.SuppressLint
import android.net.Uri
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.Image
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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.utils.AppleConstants
import java.net.URLEncoder
@Composable
fun AppleAuthButton(viewModel: LoginViewModel) {
val showDialog = remember { mutableStateOf(false) }
OutlinedButton(
onClick = {
showDialog.value = true
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp),
shape = RoundedCornerShape(6.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Image(
painter = painterResource(id = R.drawable.ic_logo_apple),
contentDescription = "",
modifier = Modifier.padding(end = 10.dp)
)
Text(text = stringResource(R.string.apple_auth_text), modifier = Modifier.padding(vertical = 6.dp))
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
)
}
}
if (showDialog.value) {
AppleAuthDialog(onDismiss = { token ->
if (token != null) {
viewModel.handleAppleToken(token)
}
showDialog.value = false
})
}
}
@Composable
fun AppleAuthDialog(onDismiss: (String?) -> Unit) {
Dialog(onDismissRequest = { onDismiss(null) }) {
Surface(
shape = RoundedCornerShape(16.dp), color = Color.White
) {
AppleAuthWebView(onDismiss)
}
}
}
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun AppleAuthWebView(onDismiss: (String?) -> Unit) {
val url =
AppleConstants.authUrl + "?client_id=" + AppleConstants.clientId + "&redirect_uri=" + URLEncoder.encode(
AppleConstants.redirectURI,
"utf8"
) + "&response_type=code%20id_token" + "&scope=" + AppleConstants.scope + "&response_mode=form_post" + "&state=android:login"
// Adding a WebView inside AndroidView
// with layout as full screen
AndroidView(factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?, request: WebResourceRequest?
): Boolean {
if (request?.url.toString().contains("android-apple-token")) {
val uri = Uri.parse(request!!.url.toString())
val token = uri.getQueryParameter("token")
onDismiss(token)
}
return true
}
}
settings.javaScriptEnabled = true
loadUrl(url)
}
}, update = {
it.loadUrl(url)
})
}

View File

@ -0,0 +1,95 @@
package app.omnivore.omnivore.feature.onboarding.auth.provider
import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
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.foundation.shape.RoundedCornerShape
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.omnivore.omnivore.BuildConfig
import app.omnivore.omnivore.R
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
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) {
val context = LocalContext.current
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(BuildConfig.OMNIVORE_GAUTH_SERVER_CLIENT_ID).requestEmail().build()
val startForResult =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
if (result.data != null) {
val task: Task<GoogleSignInAccount> =
GoogleSignIn.getSignedInAccountFromIntent(intent)
viewModel.handleGoogleAuthTask(task)
}
} else {
viewModel.showGoogleErrorMessage()
}
}
OutlinedButton(
onClick = {
val googleSignIn = GoogleSignIn.getClient(context, signInOptions)
googleSignIn.silentSignIn().addOnCompleteListener { task ->
if (task.isSuccessful) {
viewModel.handleGoogleAuthTask(task)
} else {
startForResult.launch(googleSignIn.signInIntent)
}
}.addOnFailureListener {
startForResult.launch(googleSignIn.signInIntent)
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp),
shape = RoundedCornerShape(6.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Image(
painter = painterResource(id = R.drawable.ic_logo_google),
contentDescription = "",
modifier = Modifier.padding(end = 10.dp)
)
Text(text = stringResource(R.string.google_auth_text), modifier = Modifier.padding(vertical = 6.dp))
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
)
}
}
}

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.auth.LoginViewModel
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
import app.omnivore.omnivore.navigation.Routes
internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/releases"

View File

@ -1,68 +1,78 @@
package app.omnivore.omnivore.feature.reader
import android.util.Log
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.entities.SavedItem
import com.google.gson.Gson
enum class WebFont(val displayText: String, val rawValue: String) {
INTER("Inter", "Inter"),
SYSTEM("System Default", "system-ui"),
OPEN_DYSLEXIC("Open Dyslexic", "OpenDyslexic"),
MERRIWEATHER("Merriweather", "Merriweather"),
LORA("Lora", "Lora"),
OPEN_SANS("Open Sans", "Open Sans"),
ROBOTO("Roboto", "Roboto"),
CRIMSON_TEXT("Crimson Text", "Crimson Text"),
SOURCE_SERIF_PRO("Source Serif Pro", "Source Serif Pro"),
NEWSREADER("Newsreader", "Newsreader"),
LEXEND("Lexend", "Lexend"),
LXGWWENKAI("LXGW WenKai", "LXGWWenKai"),
ATKINSON_HYPERLEGIBLE("Atkinson Hyperlegible", "AtkinsonHyperlegible"),
SOURCE_SANS_PRO("Source Sans Pro", "SourceSansPro"),
IBM_PLEX_SANS("IBM Plex Sans", "IBMPlexSans"),
LITERATA("Literata", "Literata"),
FRAUNCES("Fraunces", "Fraunces"),
INTER("Inter", "Inter"), SYSTEM("System Default", "system-ui"), OPEN_DYSLEXIC(
"Open Dyslexic",
"OpenDyslexic"
),
MERRIWEATHER("Merriweather", "Merriweather"), LORA("Lora", "Lora"), OPEN_SANS(
"Open Sans",
"Open Sans"
),
ROBOTO("Roboto", "Roboto"), CRIMSON_TEXT(
"Crimson Text",
"Crimson Text"
),
SOURCE_SERIF_PRO("Source Serif Pro", "Source Serif Pro"), NEWSREADER(
"Newsreader",
"Newsreader"
),
LEXEND("Lexend", "Lexend"), LXGWWENKAI(
"LXGW WenKai",
"LXGWWenKai"
),
ATKINSON_HYPERLEGIBLE(
"Atkinson Hyperlegible",
"AtkinsonHyperlegible"
),
SOURCE_SANS_PRO("Source Sans Pro", "SourceSansPro"), IBM_PLEX_SANS(
"IBM Plex Sans",
"IBMPlexSans"
),
LITERATA("Literata", "Literata"), FRAUNCES("Fraunces", "Fraunces"),
}
enum class ArticleContentStatus(val rawValue: String) {
FAILED("FAILED"),
PROCESSING("PROCESSING"),
SUCCEEDED("SUCCEEDED"),
UNKNOWN("UNKNOWN")
FAILED("FAILED"), PROCESSING("PROCESSING"), SUCCEEDED("SUCCEEDED"), UNKNOWN("UNKNOWN")
}
data class ArticleContent(
val title: String,
val htmlContent: String,
val highlights: List<Highlight>,
val contentStatus: String, // ArticleContentStatus,
val labelsJSONString: String
val title: String,
val htmlContent: String,
val highlights: List<Highlight>,
val contentStatus: String, // ArticleContentStatus,
val labelsJSONString: String
) {
fun highlightsJSONString(): String {
return Gson().toJson(highlights)
}
fun highlightsJSONString(): String {
return Gson().toJson(highlights)
}
}
data class WebReaderContent(
val preferences: WebPreferences,
val item: SavedItem,
val articleContent: ArticleContent,
val preferences: WebPreferences,
val item: SavedItem,
val articleContent: ArticleContent,
) {
fun styledContent(): String {
val savedAt = "\"${item.savedAt}\""
val createdAt = "\"${item.createdAt}\""
val publishedAt = if (item.publishDate != null) "\"${item.publishDate}\"" else "undefined"
fun styledContent(): String {
val savedAt = "\"${item.savedAt}\""
val createdAt = "\"${item.createdAt}\""
val publishedAt = if (item.publishDate != null) "\"${item.publishDate}\"" else "undefined"
val textFontSize = preferences.textFontSize
val highlightCssFilePath = "highlight${if (preferences.themeKey == "Dark" || preferences.themeKey == "Black") "-dark" else ""}.css"
val textFontSize = preferences.textFontSize
val highlightCssFilePath =
"highlight${if (preferences.themeKey == "Dark" || preferences.themeKey == "Black") "-dark" else ""}.css"
Log.d("theme", "current theme is: ${preferences.themeKey}")
Log.d("theme", "current theme is: ${preferences.themeKey}")
Log.d("sync", "HIGHLIGHTS JSON: ${articleContent.highlightsJSONString()}")
Log.d("sync", "HIGHLIGHTS JSON: ${articleContent.highlightsJSONString()}")
return """
return """
<!DOCTYPE html>
<html>
<head>
@ -118,5 +128,5 @@ data class WebReaderContent(
</body>
</html>
"""
}
}
}

View File

@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@ -34,15 +33,13 @@ import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXIn
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXOut
import app.omnivore.omnivore.feature.auth.LoginViewModel
import app.omnivore.omnivore.feature.auth.WelcomeScreen
import app.omnivore.omnivore.core.designsystem.theme.OmnivoreBrand
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
import app.omnivore.omnivore.feature.onboarding.OnboardingScreen
import app.omnivore.omnivore.feature.following.FollowingScreen
import app.omnivore.omnivore.feature.library.LibraryView
import app.omnivore.omnivore.feature.library.SearchView
@ -51,6 +48,7 @@ import app.omnivore.omnivore.feature.profile.about.AboutScreen
import app.omnivore.omnivore.feature.profile.account.AccountScreen
import app.omnivore.omnivore.feature.profile.filters.FiltersScreen
import app.omnivore.omnivore.feature.web.WebViewScreen
import app.omnivore.omnivore.navigation.OmnivoreNavHost
import app.omnivore.omnivore.navigation.Routes
import app.omnivore.omnivore.navigation.TopLevelDestination
@ -81,7 +79,7 @@ fun RootView(
}
}) { padding ->
Box(
modifier = if (!hasAuthToken) Modifier.background(Color(0xFFFCEBA8)) else Modifier
modifier = if (!hasAuthToken) Modifier.background(OmnivoreBrand) else Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
@ -95,8 +93,7 @@ fun RootView(
PrimaryNavigator(
navController = navController,
snackbarHostState = snackbarHostState,
startDestination = startDestination,
loginViewModel = loginViewModel
startDestination = startDestination
)
LaunchedEffect(hasAuthToken) {
if (hasAuthToken) {
@ -107,33 +104,32 @@ fun RootView(
}
}
private const val INITIAL_OFFSET_FACTOR = 0.10f
@Composable
fun PrimaryNavigator(
navController: NavHostController,
snackbarHostState: SnackbarHostState,
startDestination: String,
loginViewModel: LoginViewModel
startDestination: String
) {
NavHost(navController = navController,
startDestination = startDestination,
enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) },
exitTransition = { materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
popEnterTransition = { materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
popExitTransition = { materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) }) {
OmnivoreNavHost(
navController = navController,
startDestination = startDestination
) {
composable(Routes.Welcome.route) {
WelcomeScreen(viewModel = loginViewModel)
OnboardingScreen(navController = navController)
}
navigation(startDestination = Routes.Inbox.route,
navigation(
startDestination = Routes.Inbox.route,
route = Routes.Home.route,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None },
popExitTransition = { ExitTransition.None }) {
popExitTransition = { ExitTransition.None }
) {
composable(Routes.Inbox.route) {
LibraryView(navController = navController)

View File

@ -0,0 +1,28 @@
package app.omnivore.omnivore.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXIn
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXOut
private const val INITIAL_OFFSET_FACTOR = 0.10f
@Composable
fun OmnivoreNavHost(
navController: NavHostController,
startDestination: String,
builder: NavGraphBuilder.() -> Unit
) {
return NavHost(
navController = navController,
startDestination = startDestination,
enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) },
exitTransition = { materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
popEnterTransition = { materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
popExitTransition = { materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) }
) {
builder()
}
}

View File

@ -3,6 +3,10 @@ package app.omnivore.omnivore.navigation
sealed class Routes(val route: String) {
data object Home : Routes("Home")
data object Welcome : Routes("Welcome")
data object EmailSignIn : Routes("EmailSignIn")
data object EmailSignUp : Routes("EmailSignUp")
data object SelfHosting : Routes("SelfHosting")
data object AuthProvider : Routes("AuthProvider")
data object Following : Routes("Following")
data object Inbox : Routes("Inbox")
data object Settings : Routes("Settings")

View File

@ -1,4 +1,4 @@
package app.omnivore.omnivore.feature.auth
package app.omnivore.omnivore.utils
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier

View File

@ -13,3 +13,6 @@ object AppleConstants {
const val scope = "name%20email"
const val authUrl = "https://appleid.apple.com/auth/authorize"
}
const val FORGOT_PASSWORD_URL = "${BuildConfig.OMNIVORE_WEB_URL}/auth/forgot-password"
const val SELF_HOSTING_URL = "https://docs.omnivore.app/self-hosting/self-hosting.html"

View File

@ -268,4 +268,5 @@
<string name="edit_info_sheet_action_cancel">Cancel</string>
<string name="edit_info_sheet_error">Error while editing article!</string>
<string name="edit_info_sheet_success">Article info successfully updated!</string>
<string name="forgot_password">Forgot Password?</string>
</resources>

View File

@ -15,6 +15,8 @@ buildscript {
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.org.jetbrains.kotlin.android) apply false
}
task<Delete>("clean") {

View File

@ -2,14 +2,13 @@
accompanistFlowLayout = "0.34.0"
androidGradlePlugin = "8.3.2"
androidxActivity = "1.9.0"
androidxAppCompat = "1.6.1"
androidxComposeBom = "2024.04.01"
androidxComposeCompiler = "1.5.9"
androidxCore = "1.13.0"
androidxDataStore = "1.1.0"
androidxAppCompat = "1.7.0"
androidxComposeBom = "2024.06.00"
androidxCore = "1.13.1"
androidxDataStore = "1.1.1"
androidxEspresso = "3.5.1"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.7.0"
androidxLifecycle = "2.8.2"
androidxNavigation = "2.7.7"
androidxSecurity = "1.0.0"
androidxTestExt = "1.1.5"
@ -19,14 +18,14 @@ coil = "2.6.0"
composeMarkdown = "0.3.3"
coreSplashscreen = "1.0.1"
gson = "2.10.1"
hilt = "2.51"
hilt = "2.51.1"
intercom = "15.8.2"
junit4 = "4.13.2"
kotlin = "1.9.22"
ksp = "1.9.22-1.0.18"
kotlin = "2.0.0"
ksp = "2.0.0-1.0.21"
kotlinxCoroutines = "1.8.0"
playServices = "18.4.0"
playServicesAuth = "21.1.0"
playServices = "18.5.0"
playServicesAuth = "21.2.0"
posthog = "2.0.3"
pspdfkit = "8.9.1"
retrofit = "2.11.0"
@ -88,6 +87,8 @@ hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltWo
hilt-work-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltWork"}
[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
apollo = { id = "com.apollographql.apollo3", version.ref = "apollo" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }