Merge pull request #1178 from omnivore-app/feature/android-sign-up

Android Sign Up
This commit is contained in:
Satindar Dhillon 2022-09-16 09:55:27 -07:00 committed by GitHub
commit 1ab1db4819
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 729 additions and 43 deletions

View file

@ -0,0 +1,3 @@
query ValidateUsername($username: String!) {
validateUsername(username: $username)
}

View file

@ -9,6 +9,7 @@ object Constants {
object DatastoreKeys {
const val omnivoreAuthToken = "omnivoreAuthToken"
const val omnivoreAuthCookieString = "omnivoreAuthCookieString"
const val omnivorePendingUserToken = "omnivorePendingUserToken"
}
object AppleConstants {

View file

@ -16,6 +16,7 @@ interface DatastoreRepository {
suspend fun putInt(key: String, value: Int)
suspend fun getString(key: String): String?
suspend fun getInt(key: String): Int?
suspend fun clearValue(key: String)
}
class OmnivoreDatastore @Inject constructor(
@ -55,6 +56,11 @@ class OmnivoreDatastore @Inject constructor(
context.dataStore.edit { it.clear() }
}
override suspend fun clearValue(key: String) {
val preferencesKey = stringPreferencesKey(key)
context.dataStore.edit { it.remove(preferencesKey) }
}
override val hasAuthTokenFlow: Flow<Boolean> = context
.dataStore.data.map { preferences ->
val key = stringPreferencesKey(DatastoreKeys.omnivoreAuthToken)

View file

@ -12,12 +12,23 @@ data class AuthPayload(
val authToken: String
)
data class PendingUserAuthPayload(
val pendingUserToken: String,
)
data class SignInParams(
val token: String,
val provider: String, // APPLE or GOOGLE
val source: String = "ANDROID"
)
data class EmailSignUpParams(
val email: String,
val password: String,
val username: String,
val name: String
)
data class EmailAuthPayload(
val authCookieString: String?,
val authToken: String?,
@ -29,6 +40,16 @@ data class EmailLoginCredentials(
val password: String
)
data class CreateAccountParams(
val pendingUserToken: String,
val userProfile: UserProfile
)
data class UserProfile(
val username: String,
val name: String
)
interface EmailLoginSubmit {
@Headers("Content-Type: application/json")
@POST("/api/mobile-auth/email-sign-in")
@ -41,6 +62,24 @@ interface AuthProviderLoginSubmit {
suspend fun submitAuthProviderLogin(@Body params: SignInParams): Response<AuthPayload>
}
interface PendingUserSubmit {
@Headers("Content-Type: application/json")
@POST("/api/mobile-auth/sign-up")
suspend fun submitPendingUser(@Body params: SignInParams): Response<PendingUserAuthPayload>
}
interface CreateAccountSubmit {
@Headers("Content-Type: application/json")
@POST("/api/mobile-auth/create-account")
suspend fun submitCreateAccount(@Body params: CreateAccountParams): Response<AuthPayload>
}
interface CreateEmailAccountSubmit {
@Headers("Content-Type: application/json")
@POST("/api/mobile-auth/email-sign-up")
suspend fun submitCreateEmailAccount(@Body params: EmailSignUpParams): Response<Unit>
}
object RetrofitHelper {
fun getInstance(): Retrofit {
return Retrofit.Builder().baseUrl(Constants.apiURL)

View file

@ -0,0 +1,162 @@
package app.omnivore.omnivore.ui.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.material.icons.filled.Search
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
import org.intellij.lang.annotations.JdkConstants
@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 = "Create Your Profile",
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("Loading...")
}
ClickableText(
text = AnnotatedString("Cancel Sign Up"),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.cancelNewUserSignUp() }
)
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
@OptIn(ExperimentalMaterial3Api::class)
@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(text = "Name") },
label = { Text(text = "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(text = "Username") },
label = { Text(text = "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,
"Please enter a valid name and username.",
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
containerColor = Color(0xffffd234)
)
) {
Text(
text = "Submit",
modifier = Modifier.padding(horizontal = 100.dp)
)
}
}
}

View file

@ -1,6 +1,10 @@
package app.omnivore.omnivore.ui.auth
import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText
@ -15,16 +19,20 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalUriHandler
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.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import app.omnivore.omnivore.BuildConfig
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun EmailLoginView(viewModel: LoginViewModel, onAuthProviderButtonTap: () -> Unit) {
fun EmailLoginView(viewModel: LoginViewModel) {
val uriHandler = LocalUriHandler.current
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
@ -49,12 +57,33 @@ fun EmailLoginView(viewModel: LoginViewModel, onAuthProviderButtonTap: () -> Uni
Text("Loading...")
}
ClickableText(
text = AnnotatedString("Return to Social Login"),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { onAuthProviderButtonTap() }
)
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ClickableText(
text = AnnotatedString("Return to Social Login"),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showSocialLogin() }
)
ClickableText(
text = AnnotatedString("Don't have an account?"),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showEmailSignUp() }
)
ClickableText(
text = AnnotatedString("Forgot your 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))
}

View file

@ -0,0 +1,249 @@
package app.omnivore.omnivore.ui.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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
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
@Composable
fun EmailSignUpView(viewModel: LoginViewModel) {
if (viewModel.pendingEmailUserCreds != null) {
val email = viewModel.pendingEmailUserCreds?.email ?: ""
val password = viewModel.pendingEmailUserCreds?.password ?: ""
val verificationMessage = "We've sent a verification email to ${email}. Please verify your email and then tap the button below."
Row(
horizontalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = verificationMessage,
style = MaterialTheme.typography.titleMedium
)
Button(onClick = {
viewModel.login(email, password)
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
containerColor = Color(0xffffd234)
)
) {
Text(
text = "Check Status",
modifier = Modifier.padding(horizontal = 100.dp)
)
}
ClickableText(
text = AnnotatedString("Use a 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("Loading...")
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ClickableText(
text = AnnotatedString("Return to Social Login"),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showSocialLogin() }
)
ClickableText(
text = AnnotatedString("Already have an account?"),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { viewModel.showEmailSignIn() }
)
}
}
Spacer(modifier = Modifier.weight(1.0F))
}
}
@OptIn(ExperimentalMaterial3Api::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(
value = email,
placeholder = { Text(text = "user@email.com") },
label = { Text(text = "Email") },
onValueChange = onEmailChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(
value = password,
placeholder = { Text(text = "Password") },
label = { Text(text = "Password") },
onValueChange = onPasswordChange,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
OutlinedTextField(
value = name,
placeholder = { Text(text = "Name") },
label = { Text(text = "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(text = "Username") },
label = { Text(text = "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,
"Please complete all fields.",
Toast.LENGTH_SHORT
).show()
}
}, colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
containerColor = Color(0xffffd234)
)
) {
Text(
text = "Sign Up",
modifier = Modifier.padding(horizontal = 100.dp)
)
}
}
}

View file

@ -1,46 +1,151 @@
package app.omnivore.omnivore.ui.auth
import android.content.ContentValues
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import androidx.lifecycle.viewmodel.compose.viewModel
import app.omnivore.omnivore.*
import app.omnivore.omnivore.graphql.generated.SearchQuery
import app.omnivore.omnivore.graphql.generated.ValidateUsernameQuery
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.api.Optional
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Task
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.regex.Pattern
import javax.inject.Inject
enum class RegistrationState {
AuthProviderButtons,
EmailSignIn
SocialLogin,
EmailSignIn,
EmailSignUp,
PendingUser
}
data class PendingEmailUserCreds(
val email: String,
val password: String
)
@HiltViewModel
class LoginViewModel @Inject constructor(
private val datastoreRepo: DatastoreRepository
): ViewModel() {
private var validateUsernameJob: Job? = null
var isLoading by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var hasValidUsername by mutableStateOf<Boolean>(false)
private set
var usernameValidationErrorMessage by mutableStateOf<String?>(null)
private set
var pendingEmailUserCreds by mutableStateOf<PendingEmailUserCreds?>(null)
private set
val hasAuthTokenLiveData: LiveData<Boolean> = datastoreRepo
.hasAuthTokenFlow
.distinctUntilChanged()
.asLiveData()
val registrationStateLiveData = MutableLiveData(RegistrationState.SocialLogin)
fun getAuthCookieString(): String? = runBlocking {
datastoreRepo.getString(DatastoreKeys.omnivoreAuthCookieString)
}
fun showSocialLogin() {
resetState()
registrationStateLiveData.value = RegistrationState.SocialLogin
}
fun showEmailSignIn() {
resetState()
registrationStateLiveData.value = RegistrationState.EmailSignIn
}
fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
resetState()
pendingEmailUserCreds = pendingCreds
registrationStateLiveData.value = RegistrationState.EmailSignUp
}
fun cancelNewUserSignUp() {
resetState()
viewModelScope.launch {
datastoreRepo.clearValue(DatastoreKeys.omnivorePendingUserToken)
}
showSocialLogin()
}
private fun resetState() {
validateUsernameJob = null
isLoading = false
errorMessage = null
hasValidUsername = false
usernameValidationErrorMessage = null
pendingEmailUserCreds = null
}
fun validateUsername(potentialUsername: String) {
validateUsernameJob?.cancel()
validateUsernameJob = viewModelScope.launch {
delay(500)
// Check the username requirements first
if (potentialUsername.isEmpty()) {
usernameValidationErrorMessage = null
hasValidUsername = false
return@launch
}
if (potentialUsername.length < 4 || potentialUsername.length > 15) {
usernameValidationErrorMessage = "Username must be between 4 and 15 characters long."
hasValidUsername = false
return@launch
}
val isValidPattern = Pattern.compile("^[a-z0-9][a-z0-9_]+[a-z0-9]$")
.matcher(potentialUsername)
.matches()
if (!isValidPattern) {
usernameValidationErrorMessage = "Username can contain only letters and numbers"
hasValidUsername = false
return@launch
}
val apolloClient = ApolloClient.Builder()
.serverUrl("${Constants.apiURL}/api/graphql")
.build()
val response = apolloClient.query(
ValidateUsernameQuery(username = potentialUsername)
).execute()
if (response.data?.validateUsername == true) {
usernameValidationErrorMessage = null
hasValidUsername = true
} else {
hasValidUsername = false
usernameValidationErrorMessage = "This username is not available."
}
}
}
fun login(email: String, password: String) {
val emailLogin = RetrofitHelper.getInstance().create(EmailLoginSubmit::class.java)
@ -55,7 +160,7 @@ class LoginViewModel @Inject constructor(
isLoading = false
if (result.body()?.pendingEmailVerification == true) {
errorMessage = "Email needs verification"
showEmailSignUp(pendingCreds = PendingEmailUserCreds(email = email, password = password))
return@launch
}
@ -73,6 +178,74 @@ class LoginViewModel @Inject constructor(
}
}
fun submitEmailSignUp(
email: String,
password: String,
username: String,
name: String,
) {
viewModelScope.launch {
val request = RetrofitHelper.getInstance().create(CreateEmailAccountSubmit::class.java)
isLoading = true
errorMessage = null
val params = EmailSignUpParams(
email = email,
password = password,
name = name,
username = username
)
val result = request.submitCreateEmailAccount(params)
isLoading = false
if (result.errorBody() != null) {
errorMessage = "Something went wrong. Please check your and try again"
} else {
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
}
}
}
private fun getPendingAuthToken(): String? = runBlocking {
datastoreRepo.getString(DatastoreKeys.omnivorePendingUserToken)
}
fun submitProfile(username: String, name: String) {
viewModelScope.launch {
val request = RetrofitHelper.getInstance().create(CreateAccountSubmit::class.java)
isLoading = true
errorMessage = null
val pendingUserToken = getPendingAuthToken() ?: ""
val userProfile = UserProfile(name = name, username = username)
val params = CreateAccountParams(
pendingUserToken = pendingUserToken,
userProfile = userProfile
)
val result = request.submitCreateAccount(params)
isLoading = false
if (result.body()?.authToken != null) {
datastoreRepo.putString(DatastoreKeys.omnivoreAuthToken, result.body()?.authToken!!)
} else {
errorMessage = "Something went wrong. Please check your email/password and try again"
}
if (result.body()?.authCookieString != null) {
datastoreRepo.putString(
DatastoreKeys.omnivoreAuthCookieString, result.body()?.authCookieString!!
)
}
}
}
fun handleAppleToken(authToken: String) {
submitAuthProviderPayload(
params = SignInParams(token = authToken, provider = "APPLE")
@ -121,15 +294,46 @@ class LoginViewModel @Inject constructor(
if (result.body()?.authToken != null) {
datastoreRepo.putString(DatastoreKeys.omnivoreAuthToken, result.body()?.authToken!!)
} else {
errorMessage = "Something went wrong. Please check your credentials and try again"
}
if (result.body()?.authCookieString != null) {
datastoreRepo.putString(
DatastoreKeys.omnivoreAuthCookieString, result.body()?.authCookieString!!
)
if (result.body()?.authCookieString != null) {
datastoreRepo.putString(
DatastoreKeys.omnivoreAuthCookieString, result.body()?.authCookieString!!
)
}
} else {
when (result.code()) {
401, 403 -> {
// This is a new user so they should go through the new user flow
submitAuthProviderPayloadForPendingToken(params = params)
}
418 -> {
// Show pending email state
errorMessage = "Something went wrong. Please check your credentials and try again"
}
else -> {
errorMessage = "Something went wrong. Please check your credentials and try again"
}
}
}
}
}
private suspend fun submitAuthProviderPayloadForPendingToken(params: SignInParams) {
isLoading = true
errorMessage = null
val request = RetrofitHelper.getInstance().create(PendingUserSubmit::class.java)
val result = request.submitPendingUser(params)
isLoading = false
if (result.body()?.pendingUserToken != null) {
datastoreRepo.putString(
DatastoreKeys.omnivorePendingUserToken, result.body()?.pendingUserToken!!
)
registrationStateLiveData.value = RegistrationState.PendingUser
} else {
errorMessage = "Something went wrong. Please check your credentials and try again"
}
}
}

View file

@ -9,6 +9,7 @@ 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.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -35,11 +36,9 @@ fun WelcomeScreen(viewModel: LoginViewModel) {
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun WelcomeScreenContent(viewModel: LoginViewModel) {
var registrationState by rememberSaveable { mutableStateOf(RegistrationState.AuthProviderButtons) }
val onRegistrationStateChange = { state: RegistrationState ->
registrationState = state
}
val registrationState: RegistrationState by viewModel
.registrationStateLiveData
.observeAsState(RegistrationState.SocialLogin)
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
@ -62,14 +61,12 @@ fun WelcomeScreenContent(viewModel: LoginViewModel) {
when(registrationState) {
RegistrationState.EmailSignIn -> {
EmailLoginView(
viewModel = viewModel,
onAuthProviderButtonTap = {
onRegistrationStateChange(RegistrationState.AuthProviderButtons)
}
)
EmailLoginView(viewModel = viewModel)
}
RegistrationState.AuthProviderButtons -> {
RegistrationState.EmailSignUp -> {
EmailSignUpView(viewModel = viewModel)
}
RegistrationState.SocialLogin -> {
Text(
text = stringResource(id = R.string.welcome_title),
style = MaterialTheme.typography.headlineLarge
@ -84,10 +81,10 @@ fun WelcomeScreenContent(viewModel: LoginViewModel) {
Spacer(modifier = Modifier.height(50.dp))
AuthProviderView(
viewModel = viewModel,
onEmailButtonTap = { onRegistrationStateChange(RegistrationState.EmailSignIn) }
)
AuthProviderView(viewModel = viewModel)
}
RegistrationState.PendingUser -> {
CreateUserProfileView(viewModel = viewModel)
}
}
@ -112,10 +109,7 @@ fun WelcomeScreenContent(viewModel: LoginViewModel) {
}
@Composable
fun AuthProviderView(
viewModel: LoginViewModel,
onEmailButtonTap: () -> Unit
) {
fun AuthProviderView(viewModel: LoginViewModel) {
val isGoogleAuthAvailable: Boolean = GoogleApiAvailability
.getInstance()
.isGooglePlayServicesAvailable(LocalContext.current) == 0
@ -138,7 +132,7 @@ fun AuthProviderView(
text = AnnotatedString("Continue with Email"),
style = MaterialTheme.typography.titleMedium
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
onClick = { onEmailButtonTap() }
onClick = { viewModel.showEmailSignIn() }
)
}
Spacer(modifier = Modifier.weight(1.0F))

View file

@ -1,6 +1,5 @@
package app.omnivore.omnivore.ui.home
import android.util.Log
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

View file

@ -5,7 +5,7 @@
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<!-- Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->