refactor onboarding
This commit is contained in:
@ -3,10 +3,11 @@ import java.util.Properties
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
kotlin("android")
|
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||||
id("dagger.hilt.android.plugin")
|
id("dagger.hilt.android.plugin")
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.apollo)
|
alias(libs.plugins.apollo)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
}
|
}
|
||||||
|
|
||||||
val keystorePropertiesFile = rootProject.file("app/external/keystore.properties")
|
val keystorePropertiesFile = rootProject.file("app/external/keystore.properties")
|
||||||
@ -90,9 +91,7 @@ android {
|
|||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
|
|
||||||
}
|
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")
|
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package app.omnivore.omnivore.core.designsystem.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val OmnivoreBrand = Color(0xFFFCEBA8)
|
||||||
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,11 +1,10 @@
|
|||||||
package app.omnivore.omnivore.feature.auth
|
package app.omnivore.omnivore.feature.onboarding
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.omnivore.omnivore.BuildConfig
|
import app.omnivore.omnivore.BuildConfig
|
||||||
@ -43,8 +42,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import io.intercom.android.sdk.Intercom
|
import io.intercom.android.sdk.Intercom
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -52,10 +53,6 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
enum class RegistrationState {
|
|
||||||
SocialLogin, EmailSignIn, EmailSignUp, PendingUser, SelfHosted
|
|
||||||
}
|
|
||||||
|
|
||||||
data class PendingEmailUserCreds(
|
data class PendingEmailUserCreds(
|
||||||
val email: String, val password: String
|
val email: String, val password: String
|
||||||
)
|
)
|
||||||
@ -73,8 +70,8 @@ class LoginViewModel @Inject constructor(
|
|||||||
var isLoading by mutableStateOf(false)
|
var isLoading by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||||
private set
|
val errorMessage: StateFlow<String?> get() = _errorMessage.asStateFlow()
|
||||||
|
|
||||||
var hasValidUsername by mutableStateOf(false)
|
var hasValidUsername by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
@ -92,8 +89,6 @@ class LoginViewModel @Inject constructor(
|
|||||||
initialValue = true
|
initialValue = true
|
||||||
)
|
)
|
||||||
|
|
||||||
val registrationStateLiveData = MutableLiveData(RegistrationState.SocialLogin)
|
|
||||||
|
|
||||||
val followingTabActiveState: StateFlow<Boolean> = datastoreRepository.getBoolean(
|
val followingTabActiveState: StateFlow<Boolean> = datastoreRepository.getBoolean(
|
||||||
followingTabActive
|
followingTabActive
|
||||||
).stateIn(
|
).stateIn(
|
||||||
@ -124,29 +119,11 @@ class LoginViewModel @Inject constructor(
|
|||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSocialLogin() {
|
private fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
|
||||||
resetState()
|
|
||||||
registrationStateLiveData.value = RegistrationState.SocialLogin
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showEmailSignIn() {
|
|
||||||
resetState()
|
|
||||||
registrationStateLiveData.value = RegistrationState.EmailSignIn
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
|
|
||||||
resetState()
|
resetState()
|
||||||
pendingEmailUserCreds = pendingCreds
|
pendingEmailUserCreds = pendingCreds
|
||||||
registrationStateLiveData.value = RegistrationState.EmailSignUp
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSelfHostedSettings() {
|
|
||||||
resetState()
|
|
||||||
registrationStateLiveData.value = RegistrationState.SelfHosted
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelNewUserSignUp() {
|
fun cancelNewUserSignUp() {
|
||||||
@ -154,7 +131,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
datastoreRepository.clearValue(omnivorePendingUserToken)
|
datastoreRepository.clearValue(omnivorePendingUserToken)
|
||||||
}
|
}
|
||||||
showSocialLogin()
|
resetState()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerUser() {
|
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
|
validateUsernameJob = null
|
||||||
isLoading = false
|
isLoading = false
|
||||||
errorMessage = null
|
resetErrorMessage()
|
||||||
hasValidUsername = false
|
hasValidUsername = false
|
||||||
usernameValidationErrorMessage = null
|
usernameValidationErrorMessage = null
|
||||||
pendingEmailUserCreds = null
|
pendingEmailUserCreds = null
|
||||||
@ -240,7 +225,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
RetrofitHelper.getInstance(networker).create(EmailLoginSubmit::class.java)
|
RetrofitHelper.getInstance(networker).create(EmailLoginSubmit::class.java)
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
resetErrorMessage()
|
||||||
|
|
||||||
val result = emailLogin.submitEmailLogin(
|
val result = emailLogin.submitEmailLogin(
|
||||||
EmailLoginCredentials(email = email, password = password)
|
EmailLoginCredentials(email = email, password = password)
|
||||||
@ -260,9 +245,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
if (result.body()?.authToken != null) {
|
if (result.body()?.authToken != null) {
|
||||||
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||||
} else {
|
} else {
|
||||||
errorMessage = resourceProvider.getString(
|
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
|
||||||
R.string.login_view_model_something_went_wrong_error_msg
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.body()?.authCookieString != null) {
|
if (result.body()?.authCookieString != null) {
|
||||||
@ -284,7 +267,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
RetrofitHelper.getInstance(networker).create(CreateEmailAccountSubmit::class.java)
|
RetrofitHelper.getInstance(networker).create(CreateEmailAccountSubmit::class.java)
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
resetErrorMessage()
|
||||||
|
|
||||||
val params = EmailSignUpParams(
|
val params = EmailSignUpParams(
|
||||||
email = email, password = password, name = name, username = username
|
email = email, password = password, name = name, username = username
|
||||||
@ -295,9 +278,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
||||||
if (result.errorBody() != null) {
|
if (result.errorBody() != null) {
|
||||||
errorMessage = resourceProvider.getString(
|
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
|
||||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
|
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
|
||||||
}
|
}
|
||||||
@ -314,7 +295,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
RetrofitHelper.getInstance(networker).create(CreateAccountSubmit::class.java)
|
RetrofitHelper.getInstance(networker).create(CreateAccountSubmit::class.java)
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
resetErrorMessage()
|
||||||
|
|
||||||
val pendingUserToken = getPendingAuthToken() ?: ""
|
val pendingUserToken = getPendingAuthToken() ?: ""
|
||||||
|
|
||||||
@ -330,9 +311,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
if (result.body()?.authToken != null) {
|
if (result.body()?.authToken != null) {
|
||||||
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||||
} else {
|
} else {
|
||||||
errorMessage = resourceProvider.getString(
|
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
|
||||||
R.string.login_view_model_something_went_wrong_error_msg
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.body()?.authCookieString != null) {
|
if (result.body()?.authCookieString != null) {
|
||||||
@ -358,12 +337,8 @@ class LoginViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetErrorMessage() {
|
|
||||||
errorMessage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showGoogleErrorMessage() {
|
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>) {
|
fun handleGoogleAuthTask(task: Task<GoogleSignInAccount>) {
|
||||||
@ -372,9 +347,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
|
|
||||||
// If token is missing then set the error message
|
// If token is missing then set the error message
|
||||||
if (googleIdToken.isEmpty()) {
|
if (googleIdToken.isEmpty()) {
|
||||||
errorMessage = resourceProvider.getString(
|
setErrorMessage(resourceProvider.getString(R.string.login_view_model_missing_auth_token_error_msg))
|
||||||
R.string.login_view_model_missing_auth_token_error_msg
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,7 +363,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
RetrofitHelper.getInstance(networker).create(AuthProviderLoginSubmit::class.java)
|
RetrofitHelper.getInstance(networker).create(AuthProviderLoginSubmit::class.java)
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
resetErrorMessage()
|
||||||
|
|
||||||
val result = login.submitAuthProviderLogin(params)
|
val result = login.submitAuthProviderLogin(params)
|
||||||
|
|
||||||
@ -413,15 +386,11 @@ class LoginViewModel @Inject constructor(
|
|||||||
|
|
||||||
418 -> {
|
418 -> {
|
||||||
// Show pending email state
|
// Show pending email state
|
||||||
errorMessage = resourceProvider.getString(
|
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
|
||||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
errorMessage = resourceProvider.getString(
|
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
|
||||||
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) {
|
private suspend fun submitAuthProviderPayloadForPendingToken(params: SignInParams) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
resetErrorMessage()
|
||||||
|
|
||||||
val request = RetrofitHelper.getInstance(networker).create(PendingUserSubmit::class.java)
|
val request = RetrofitHelper.getInstance(networker).create(PendingUserSubmit::class.java)
|
||||||
val result = request.submitPendingUser(params)
|
val result = request.submitPendingUser(params)
|
||||||
@ -441,11 +410,9 @@ class LoginViewModel @Inject constructor(
|
|||||||
datastoreRepository.putString(
|
datastoreRepository.putString(
|
||||||
omnivorePendingUserToken, result.body()?.pendingUserToken!!
|
omnivorePendingUserToken, result.body()?.pendingUserToken!!
|
||||||
)
|
)
|
||||||
registrationStateLiveData.value = RegistrationState.PendingUser
|
// TODO go to pending user
|
||||||
} else {
|
} else {
|
||||||
errorMessage = resourceProvider.getString(
|
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
|
||||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
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.ClickableText
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.runtime.*
|
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.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import app.omnivore.omnivore.R
|
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
|
@Composable
|
||||||
fun SelfHostedView(viewModel: LoginViewModel) {
|
fun SelfHostedScreen(
|
||||||
|
viewModel: LoginViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
var apiServer by rememberSaveable { mutableStateOf("") }
|
var apiServer by rememberSaveable { mutableStateOf("") }
|
||||||
var webServer by rememberSaveable { mutableStateOf("") }
|
var webServer by rememberSaveable { mutableStateOf("") }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.padding(bottom = 64.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(1.0F))
|
Spacer(modifier = Modifier.weight(1.0F))
|
||||||
Column(
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
@ -48,51 +70,25 @@ fun SelfHostedView(viewModel: LoginViewModel) {
|
|||||||
onWebServerChange = { webServer = it },
|
onWebServerChange = { webServer = it },
|
||||||
onSaveClick = {
|
onSaveClick = {
|
||||||
viewModel.setSelfHostingDetails(context, apiServer, webServer)
|
viewModel.setSelfHostingDetails(context, apiServer, webServer)
|
||||||
}
|
},
|
||||||
|
onResetClick = { viewModel.resetSelfHostingDetails(context) },
|
||||||
|
isLoading = viewModel.isLoading
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: add a activity indicator (maybe after a delay?)
|
Column(
|
||||||
if (viewModel.isLoading) {
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
Text(stringResource(R.string.self_hosted_view_loading))
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
ClickableText(
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_learn_more)),
|
||||||
) {
|
style = MaterialTheme.typography.titleMedium
|
||||||
ClickableText(
|
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_reset)),
|
onClick = {
|
||||||
style = MaterialTheme.typography.titleMedium
|
val uri = Uri.parse(SELF_HOSTING_URL)
|
||||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
onClick = { viewModel.resetSelfHostingDetails(context) },
|
ContextCompat.startActivity(context, browserIntent, null)
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
},
|
||||||
)
|
modifier = Modifier.padding(vertical = 10.dp)
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1.0F))
|
Spacer(modifier = Modifier.weight(1.0F))
|
||||||
@ -105,19 +101,19 @@ fun SelfHostedFields(
|
|||||||
webServer: String,
|
webServer: String,
|
||||||
onAPIServerChange: (String) -> Unit,
|
onAPIServerChange: (String) -> Unit,
|
||||||
onWebServerChange: (String) -> Unit,
|
onWebServerChange: (String) -> Unit,
|
||||||
onSaveClick: () -> Unit
|
onSaveClick: () -> Unit,
|
||||||
|
onResetClick: () -> Unit,
|
||||||
|
isLoading: Boolean
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.height(300.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(25.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
value = apiServer,
|
value = apiServer,
|
||||||
placeholder = { Text(text = "https://api-prod.omnivore.app/") },
|
placeholder = { Text(text = "https://api-prod.omnivore.app/") },
|
||||||
label = { Text(stringResource(R.string.self_hosted_view_field_api_url_label)) },
|
label = { Text(stringResource(R.string.self_hosted_view_field_api_url_label)) },
|
||||||
@ -130,6 +126,7 @@ fun SelfHostedFields(
|
|||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp),
|
||||||
value = webServer,
|
value = webServer,
|
||||||
placeholder = { Text(text = "https://omnivore.app/") },
|
placeholder = { Text(text = "https://omnivore.app/") },
|
||||||
label = { Text(stringResource(R.string.self_hosted_view_field_web_url_label)) },
|
label = { Text(stringResource(R.string.self_hosted_view_field_web_url_label)) },
|
||||||
@ -141,7 +138,9 @@ fun SelfHostedFields(
|
|||||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||||
)
|
)
|
||||||
|
|
||||||
Button(onClick = {
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
if (apiServer.isNotBlank() && webServer.isNotBlank()) {
|
if (apiServer.isNotBlank() && webServer.isNotBlank()) {
|
||||||
onSaveClick()
|
onSaveClick()
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
@ -158,8 +157,28 @@ fun SelfHostedFields(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.self_hosted_view_action_save),
|
text = stringResource(R.string.self_hosted_view_action_save).uppercase(),
|
||||||
modifier = Modifier.padding(horizontal = 100.dp)
|
)
|
||||||
|
if (isLoading) {
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.width(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DividerWithText(text = "or")
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = { onResetClick() }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Reset".uppercase()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import app.omnivore.omnivore.R
|
import app.omnivore.omnivore.R
|
||||||
import app.omnivore.omnivore.core.designsystem.component.TextPreferenceWidget
|
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
|
import app.omnivore.omnivore.navigation.Routes
|
||||||
|
|
||||||
internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/releases"
|
internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/releases"
|
||||||
|
|||||||
@ -1,68 +1,78 @@
|
|||||||
package app.omnivore.omnivore.feature.reader
|
package app.omnivore.omnivore.feature.reader
|
||||||
|
|
||||||
import android.util.Log
|
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.Highlight
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
|
||||||
enum class WebFont(val displayText: String, val rawValue: String) {
|
enum class WebFont(val displayText: String, val rawValue: String) {
|
||||||
INTER("Inter", "Inter"),
|
INTER("Inter", "Inter"), SYSTEM("System Default", "system-ui"), OPEN_DYSLEXIC(
|
||||||
SYSTEM("System Default", "system-ui"),
|
"Open Dyslexic",
|
||||||
OPEN_DYSLEXIC("Open Dyslexic", "OpenDyslexic"),
|
"OpenDyslexic"
|
||||||
MERRIWEATHER("Merriweather", "Merriweather"),
|
),
|
||||||
LORA("Lora", "Lora"),
|
MERRIWEATHER("Merriweather", "Merriweather"), LORA("Lora", "Lora"), OPEN_SANS(
|
||||||
OPEN_SANS("Open Sans", "Open Sans"),
|
"Open Sans",
|
||||||
ROBOTO("Roboto", "Roboto"),
|
"Open Sans"
|
||||||
CRIMSON_TEXT("Crimson Text", "Crimson Text"),
|
),
|
||||||
SOURCE_SERIF_PRO("Source Serif Pro", "Source Serif Pro"),
|
ROBOTO("Roboto", "Roboto"), CRIMSON_TEXT(
|
||||||
NEWSREADER("Newsreader", "Newsreader"),
|
"Crimson Text",
|
||||||
LEXEND("Lexend", "Lexend"),
|
"Crimson Text"
|
||||||
LXGWWENKAI("LXGW WenKai", "LXGWWenKai"),
|
),
|
||||||
ATKINSON_HYPERLEGIBLE("Atkinson Hyperlegible", "AtkinsonHyperlegible"),
|
SOURCE_SERIF_PRO("Source Serif Pro", "Source Serif Pro"), NEWSREADER(
|
||||||
SOURCE_SANS_PRO("Source Sans Pro", "SourceSansPro"),
|
"Newsreader",
|
||||||
IBM_PLEX_SANS("IBM Plex Sans", "IBMPlexSans"),
|
"Newsreader"
|
||||||
LITERATA("Literata", "Literata"),
|
),
|
||||||
FRAUNCES("Fraunces", "Fraunces"),
|
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) {
|
enum class ArticleContentStatus(val rawValue: String) {
|
||||||
FAILED("FAILED"),
|
FAILED("FAILED"), PROCESSING("PROCESSING"), SUCCEEDED("SUCCEEDED"), UNKNOWN("UNKNOWN")
|
||||||
PROCESSING("PROCESSING"),
|
|
||||||
SUCCEEDED("SUCCEEDED"),
|
|
||||||
UNKNOWN("UNKNOWN")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ArticleContent(
|
data class ArticleContent(
|
||||||
val title: String,
|
val title: String,
|
||||||
val htmlContent: String,
|
val htmlContent: String,
|
||||||
val highlights: List<Highlight>,
|
val highlights: List<Highlight>,
|
||||||
val contentStatus: String, // ArticleContentStatus,
|
val contentStatus: String, // ArticleContentStatus,
|
||||||
val labelsJSONString: String
|
val labelsJSONString: String
|
||||||
) {
|
) {
|
||||||
fun highlightsJSONString(): String {
|
fun highlightsJSONString(): String {
|
||||||
return Gson().toJson(highlights)
|
return Gson().toJson(highlights)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class WebReaderContent(
|
data class WebReaderContent(
|
||||||
val preferences: WebPreferences,
|
val preferences: WebPreferences,
|
||||||
val item: SavedItem,
|
val item: SavedItem,
|
||||||
val articleContent: ArticleContent,
|
val articleContent: ArticleContent,
|
||||||
) {
|
) {
|
||||||
fun styledContent(): String {
|
fun styledContent(): String {
|
||||||
val savedAt = "\"${item.savedAt}\""
|
val savedAt = "\"${item.savedAt}\""
|
||||||
val createdAt = "\"${item.createdAt}\""
|
val createdAt = "\"${item.createdAt}\""
|
||||||
val publishedAt = if (item.publishDate != null) "\"${item.publishDate}\"" else "undefined"
|
val publishedAt = if (item.publishDate != null) "\"${item.publishDate}\"" else "undefined"
|
||||||
|
|
||||||
|
|
||||||
val textFontSize = preferences.textFontSize
|
val textFontSize = preferences.textFontSize
|
||||||
val highlightCssFilePath = "highlight${if (preferences.themeKey == "Dark" || preferences.themeKey == "Black") "-dark" else ""}.css"
|
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>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@ -118,5 +128,5 @@ data class WebReaderContent(
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
@ -34,15 +33,13 @@ import androidx.navigation.NavDestination
|
|||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.navigation
|
import androidx.navigation.compose.navigation
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXIn
|
import app.omnivore.omnivore.core.designsystem.theme.OmnivoreBrand
|
||||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXOut
|
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
import app.omnivore.omnivore.feature.onboarding.OnboardingScreen
|
||||||
import app.omnivore.omnivore.feature.auth.WelcomeScreen
|
|
||||||
import app.omnivore.omnivore.feature.following.FollowingScreen
|
import app.omnivore.omnivore.feature.following.FollowingScreen
|
||||||
import app.omnivore.omnivore.feature.library.LibraryView
|
import app.omnivore.omnivore.feature.library.LibraryView
|
||||||
import app.omnivore.omnivore.feature.library.SearchView
|
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.account.AccountScreen
|
||||||
import app.omnivore.omnivore.feature.profile.filters.FiltersScreen
|
import app.omnivore.omnivore.feature.profile.filters.FiltersScreen
|
||||||
import app.omnivore.omnivore.feature.web.WebViewScreen
|
import app.omnivore.omnivore.feature.web.WebViewScreen
|
||||||
|
import app.omnivore.omnivore.navigation.OmnivoreNavHost
|
||||||
import app.omnivore.omnivore.navigation.Routes
|
import app.omnivore.omnivore.navigation.Routes
|
||||||
import app.omnivore.omnivore.navigation.TopLevelDestination
|
import app.omnivore.omnivore.navigation.TopLevelDestination
|
||||||
|
|
||||||
@ -81,7 +79,7 @@ fun RootView(
|
|||||||
}
|
}
|
||||||
}) { padding ->
|
}) { padding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = if (!hasAuthToken) Modifier.background(Color(0xFFFCEBA8)) else Modifier
|
modifier = if (!hasAuthToken) Modifier.background(OmnivoreBrand) else Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
@ -95,8 +93,7 @@ fun RootView(
|
|||||||
PrimaryNavigator(
|
PrimaryNavigator(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
startDestination = startDestination,
|
startDestination = startDestination
|
||||||
loginViewModel = loginViewModel
|
|
||||||
)
|
)
|
||||||
LaunchedEffect(hasAuthToken) {
|
LaunchedEffect(hasAuthToken) {
|
||||||
if (hasAuthToken) {
|
if (hasAuthToken) {
|
||||||
@ -107,33 +104,32 @@ fun RootView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val INITIAL_OFFSET_FACTOR = 0.10f
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PrimaryNavigator(
|
fun PrimaryNavigator(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
startDestination: String,
|
startDestination: String
|
||||||
loginViewModel: LoginViewModel
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
NavHost(navController = navController,
|
OmnivoreNavHost(
|
||||||
startDestination = startDestination,
|
navController = navController,
|
||||||
enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
startDestination = startDestination
|
||||||
exitTransition = { materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
) {
|
||||||
popEnterTransition = { materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
|
||||||
popExitTransition = { materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) }) {
|
|
||||||
|
|
||||||
composable(Routes.Welcome.route) {
|
composable(Routes.Welcome.route) {
|
||||||
WelcomeScreen(viewModel = loginViewModel)
|
OnboardingScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation(startDestination = Routes.Inbox.route,
|
navigation(
|
||||||
|
startDestination = Routes.Inbox.route,
|
||||||
route = Routes.Home.route,
|
route = Routes.Home.route,
|
||||||
enterTransition = { EnterTransition.None },
|
enterTransition = { EnterTransition.None },
|
||||||
exitTransition = { ExitTransition.None },
|
exitTransition = { ExitTransition.None },
|
||||||
popEnterTransition = { EnterTransition.None },
|
popEnterTransition = { EnterTransition.None },
|
||||||
popExitTransition = { ExitTransition.None }) {
|
popExitTransition = { ExitTransition.None }
|
||||||
|
) {
|
||||||
|
|
||||||
composable(Routes.Inbox.route) {
|
composable(Routes.Inbox.route) {
|
||||||
LibraryView(navController = navController)
|
LibraryView(navController = navController)
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,10 @@ package app.omnivore.omnivore.navigation
|
|||||||
sealed class Routes(val route: String) {
|
sealed class Routes(val route: String) {
|
||||||
data object Home : Routes("Home")
|
data object Home : Routes("Home")
|
||||||
data object Welcome : Routes("Welcome")
|
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 Following : Routes("Following")
|
||||||
data object Inbox : Routes("Inbox")
|
data object Inbox : Routes("Inbox")
|
||||||
data object Settings : Routes("Settings")
|
data object Settings : Routes("Settings")
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package app.omnivore.omnivore.feature.auth
|
package app.omnivore.omnivore.utils
|
||||||
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -13,3 +13,6 @@ object AppleConstants {
|
|||||||
const val scope = "name%20email"
|
const val scope = "name%20email"
|
||||||
const val authUrl = "https://appleid.apple.com/auth/authorize"
|
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"
|
||||||
|
|||||||
@ -268,4 +268,5 @@
|
|||||||
<string name="edit_info_sheet_action_cancel">Cancel</string>
|
<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_error">Error while editing article!</string>
|
||||||
<string name="edit_info_sheet_success">Article info successfully updated!</string>
|
<string name="edit_info_sheet_success">Article info successfully updated!</string>
|
||||||
|
<string name="forgot_password">Forgot Password?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -15,6 +15,8 @@ buildscript {
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.ksp) 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") {
|
task<Delete>("clean") {
|
||||||
|
|||||||
@ -2,14 +2,13 @@
|
|||||||
accompanistFlowLayout = "0.34.0"
|
accompanistFlowLayout = "0.34.0"
|
||||||
androidGradlePlugin = "8.3.2"
|
androidGradlePlugin = "8.3.2"
|
||||||
androidxActivity = "1.9.0"
|
androidxActivity = "1.9.0"
|
||||||
androidxAppCompat = "1.6.1"
|
androidxAppCompat = "1.7.0"
|
||||||
androidxComposeBom = "2024.04.01"
|
androidxComposeBom = "2024.06.00"
|
||||||
androidxComposeCompiler = "1.5.9"
|
androidxCore = "1.13.1"
|
||||||
androidxCore = "1.13.0"
|
androidxDataStore = "1.1.1"
|
||||||
androidxDataStore = "1.1.0"
|
|
||||||
androidxEspresso = "3.5.1"
|
androidxEspresso = "3.5.1"
|
||||||
androidxHiltNavigationCompose = "1.2.0"
|
androidxHiltNavigationCompose = "1.2.0"
|
||||||
androidxLifecycle = "2.7.0"
|
androidxLifecycle = "2.8.2"
|
||||||
androidxNavigation = "2.7.7"
|
androidxNavigation = "2.7.7"
|
||||||
androidxSecurity = "1.0.0"
|
androidxSecurity = "1.0.0"
|
||||||
androidxTestExt = "1.1.5"
|
androidxTestExt = "1.1.5"
|
||||||
@ -19,14 +18,14 @@ coil = "2.6.0"
|
|||||||
composeMarkdown = "0.3.3"
|
composeMarkdown = "0.3.3"
|
||||||
coreSplashscreen = "1.0.1"
|
coreSplashscreen = "1.0.1"
|
||||||
gson = "2.10.1"
|
gson = "2.10.1"
|
||||||
hilt = "2.51"
|
hilt = "2.51.1"
|
||||||
intercom = "15.8.2"
|
intercom = "15.8.2"
|
||||||
junit4 = "4.13.2"
|
junit4 = "4.13.2"
|
||||||
kotlin = "1.9.22"
|
kotlin = "2.0.0"
|
||||||
ksp = "1.9.22-1.0.18"
|
ksp = "2.0.0-1.0.21"
|
||||||
kotlinxCoroutines = "1.8.0"
|
kotlinxCoroutines = "1.8.0"
|
||||||
playServices = "18.4.0"
|
playServices = "18.5.0"
|
||||||
playServicesAuth = "21.1.0"
|
playServicesAuth = "21.2.0"
|
||||||
posthog = "2.0.3"
|
posthog = "2.0.3"
|
||||||
pspdfkit = "8.9.1"
|
pspdfkit = "8.9.1"
|
||||||
retrofit = "2.11.0"
|
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"}
|
hilt-work-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltWork"}
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
apollo = { id = "com.apollographql.apollo3", version.ref = "apollo" }
|
apollo = { id = "com.apollographql.apollo3", version.ref = "apollo" }
|
||||||
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user