Create account: user feedback when account name is already taken (#701)

* Show snackbar when account can't be created without known reason; show error when account name is taken

* Don't crash on empty account name
This commit is contained in:
Ricki Hirner 2024-04-04 13:36:27 +02:00 committed by GitHub
parent 7b2f14d148
commit e638f5debc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 46 additions and 4 deletions

View file

@ -19,6 +19,7 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.RadioButton
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
@ -29,9 +30,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
@ -43,9 +46,11 @@ import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.ui.composable.Assistant
import at.bitfire.davdroid.ui.widget.ExceptionInfoDialog
import at.bitfire.vcard4android.GroupMethod
import kotlinx.coroutines.launch
@Composable
fun AccountDetailsPage(
snackbarHostState: SnackbarHostState,
loginInfo: LoginInfo,
foundConfig: DavResourceFinder.Configuration,
onBack: () -> Unit,
@ -54,6 +59,9 @@ fun AccountDetailsPage(
) {
BackHandler(onBack = onBack)
val context = LocalContext.current
val scope = rememberCoroutineScope()
val resultOrNull by model.createAccountResult.observeAsState()
var showExceptionInfo by remember { mutableStateOf(false) }
LaunchedEffect(resultOrNull) {
@ -73,9 +81,16 @@ fun AccountDetailsPage(
model.createAccountResult.value = null
}
)
// TODO else
}
else
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.login_account_not_created))
}
}
else -> {}
}
// reset result
model.createAccountResult.value = null
}
val suggestedAccountNames = foundConfig.calDAV?.emails ?: emptyList()
@ -86,6 +101,7 @@ fun AccountDetailsPage(
AccountDetailsPage_Content(
suggestedAccountNames = suggestedAccountNames,
accountName = accountName,
accountNameAlreadyExists = model.accountExists(accountName).observeAsState(false).value,
onUpdateAccountName = { accountName = it },
onCreateAccount = {
model.createAccount(
@ -106,6 +122,7 @@ fun AccountDetailsPage(
fun AccountDetailsPage_Content(
suggestedAccountNames: List<String>,
accountName: String,
accountNameAlreadyExists: Boolean,
onUpdateAccountName: (String) -> Unit = {},
groupMethod: GroupMethod,
groupMethodReadOnly: Boolean,
@ -114,7 +131,8 @@ fun AccountDetailsPage_Content(
) {
Assistant(
nextLabel = stringResource(R.string.login_create_account),
onNext = onCreateAccount
onNext = onCreateAccount,
nextEnabled = accountName.isNotBlank() && !accountNameAlreadyExists
) {
Column(Modifier.padding(8.dp)) {
var expanded by remember { mutableStateOf(false) }
@ -122,10 +140,15 @@ fun AccountDetailsPage_Content(
expanded = expanded,
onExpandedChange = { expanded = it }
) {
val accountNameLabel = if (accountNameAlreadyExists)
stringResource(R.string.login_account_name_already_taken)
else
stringResource(R.string.login_account_name)
OutlinedTextField(
value = accountName,
onValueChange = onUpdateAccountName,
label = { Text(stringResource(R.string.login_account_name)) },
label = { Text(accountNameLabel) },
isError = accountNameAlreadyExists,
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email
@ -227,6 +250,7 @@ fun AccountDetailsPage_Content_Preview() {
AccountDetailsPage_Content(
suggestedAccountNames = listOf("name1", "name2@example.com"),
accountName = "account@example.com",
accountNameAlreadyExists = false,
groupMethod = GroupMethod.GROUP_VCARDS,
groupMethodReadOnly = false
)
@ -238,6 +262,7 @@ fun AccountDetailsPage_Content_Preview_With_Apostrophe() {
AccountDetailsPage_Content(
suggestedAccountNames = listOf("name1", "name2@example.com"),
accountName = "account'example.com",
accountNameAlreadyExists = true,
groupMethod = GroupMethod.CATEGORIES,
groupMethodReadOnly = true
)

View file

@ -5,12 +5,15 @@
package at.bitfire.davdroid.ui.setup
import android.accounts.Account
import android.accounts.AccountManager
import android.app.Application
import android.content.ContentResolver
import android.provider.CalendarContract
import androidx.core.os.CancellationSignal
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.InvalidAccountException
@ -77,6 +80,19 @@ class LoginModel @Inject constructor(
}
fun accountExists(accountName: String): LiveData<Boolean> = liveData {
val accountType = context.getString(R.string.account_type)
val exists =
if (accountName.isEmpty())
false
else
AccountManager.get(context)
.getAccountsByType(accountType)
.contains(Account(accountName, accountType))
emit(exists)
}
interface CreateAccountResult {
class Success(val account: Account): CreateAccountResult
class Error(val exception: Exception?): CreateAccountResult

View file

@ -145,6 +145,7 @@ fun LoginScreen(
foundConfig?.let {
val context = LocalContext.current
AccountDetailsPage(
snackbarHostState = snackbarHostState,
loginInfo = loginInfo,
foundConfig = it,
onBack = { phase = LoginActivity.Phase.LOGIN_TYPE },