mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Convert AccountSettingsFragment from DSL to Compose.
This commit is contained in:
committed by
Jeffrey Starke
parent
71c34e17eb
commit
dcce8ea35a
@@ -640,6 +640,9 @@ dependencies {
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
testImplementation(testLibs.espresso.core)
|
||||
testImplementation(testLibs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
|
||||
"perfImplementation"(libs.androidx.compose.ui.test.manifest)
|
||||
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.account
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.text.InputType
|
||||
@@ -10,21 +9,44 @@ import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.core.app.DialogCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -36,13 +58,12 @@ import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
|
||||
class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) {
|
||||
class AccountSettingsFragment : ComposeFragment() {
|
||||
|
||||
lateinit var viewModel: AccountSettingsViewModel
|
||||
private val viewModel: AccountSettingsViewModel by viewModels()
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN && resultCode == CreateSvrPinActivity.RESULT_OK) {
|
||||
@@ -55,132 +76,15 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
viewModel.refreshState()
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
viewModel = ViewModelProvider(this)[AccountSettingsViewModel::class.java]
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val callbacks = remember { Callbacks() }
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: AccountSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.preferences_app_protection__signal_pin)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(if (state.hasPin || state.hasRestoredAep) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
if (state.hasPin) {
|
||||
startActivityForResult(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
|
||||
} else {
|
||||
startActivityForResult(CreateSvrPinActivity.getIntentForPinCreate(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_app_protection__pin_reminders),
|
||||
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__youll_be_asked_less_frequently),
|
||||
isChecked = state.hasPin && state.pinRemindersEnabled,
|
||||
isEnabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
setPinRemindersEnabled(!state.pinRemindersEnabled)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_app_protection__registration_lock),
|
||||
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__require_your_signal_pin),
|
||||
isChecked = state.registrationLockEnabled,
|
||||
isEnabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
setRegistrationLockEnabled(!state.registrationLockEnabled)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__advanced_pin_settings),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.AccountSettingsFragment__account)
|
||||
|
||||
if (SignalStore.account.isRegistered) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
|
||||
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
|
||||
isEnabled = state.canTransferWhileUnregistered || state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__request_account_data),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_exportAccountFragment)
|
||||
}
|
||||
)
|
||||
|
||||
if (!state.isNotDeprecatedOrUnregistered()) {
|
||||
if (state.clientDeprecated) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_account_update_signal),
|
||||
onClick = {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
|
||||
}
|
||||
)
|
||||
} else if (state.userUnregistered) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_account_reregister),
|
||||
onClick = {
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_account_delete_all_data, ContextCompat.getColor(requireContext(), R.color.signal_alert_primary)),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.preferences_account_delete_all_data_confirmation_title)
|
||||
.setMessage(R.string.preferences_account_delete_all_data_confirmation_message)
|
||||
.setPositiveButton(R.string.preferences_account_delete_all_data_confirmation_proceed) { _: DialogInterface, _: Int ->
|
||||
if (!ServiceUtil.getActivityManager(AppDependencies.application).clearApplicationUserData()) {
|
||||
Toast.makeText(requireContext(), R.string.preferences_account_delete_all_data_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.preferences_account_delete_all_data_confirmation_cancel, null)
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), if (state.isNotDeprecatedOrUnregistered()) R.color.signal_alert_primary else R.color.signal_alert_primary_50)),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
AccountSettingsScreen(
|
||||
state = state,
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
private fun setRegistrationLockEnabled(enabled: Boolean) {
|
||||
@@ -232,6 +136,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
changeKeyboard.setIconResource(PinKeyboardType.ALPHA_NUMERIC.iconResource)
|
||||
}
|
||||
|
||||
PinKeyboardType.ALPHA_NUMERIC -> {
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
changeKeyboard.setIconResource(PinKeyboardType.NUMERIC.iconResource)
|
||||
@@ -263,4 +168,323 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
viewModel.refreshState()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Callbacks : AccountSettingsScreenCallbacks {
|
||||
override fun onNavigationClick() {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onChangePinClick() {
|
||||
startActivityForResult(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreatePinClick() {
|
||||
startActivityForResult(CreateSvrPinActivity.getIntentForPinCreate(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
|
||||
}
|
||||
|
||||
override fun setPinRemindersEnabled(enabled: Boolean) {
|
||||
this@AccountSettingsFragment.setPinRemindersEnabled(enabled)
|
||||
}
|
||||
|
||||
override fun setRegistrationLockEnabled(enabled: Boolean) {
|
||||
this@AccountSettingsFragment.setRegistrationLockEnabled(enabled)
|
||||
}
|
||||
|
||||
override fun openAdvancedPinSettings() {
|
||||
findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
|
||||
}
|
||||
|
||||
override fun openChangeNumberFlow() {
|
||||
findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
|
||||
}
|
||||
|
||||
override fun openDeviceTransferFlow() {
|
||||
findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
|
||||
}
|
||||
|
||||
override fun openExportAccountDataFlow() {
|
||||
findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_exportAccountFragment)
|
||||
}
|
||||
|
||||
override fun openUpdateAppFlow() {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
|
||||
}
|
||||
|
||||
override fun openReRegistrationFlow() {
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
|
||||
override fun openDeleteAccountFlow() {
|
||||
findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
|
||||
}
|
||||
|
||||
override fun deleteAllData() {
|
||||
if (!ServiceUtil.getActivityManager(AppDependencies.application).clearApplicationUserData()) {
|
||||
Toast.makeText(requireContext(), R.string.preferences_account_delete_all_data_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@VisibleForTesting
|
||||
interface AccountSettingsScreenCallbacks {
|
||||
|
||||
fun onNavigationClick() = Unit
|
||||
fun onChangePinClick() = Unit
|
||||
fun onCreatePinClick() = Unit
|
||||
fun setPinRemindersEnabled(enabled: Boolean) = Unit
|
||||
fun setRegistrationLockEnabled(enabled: Boolean) = Unit
|
||||
fun openAdvancedPinSettings() = Unit
|
||||
fun openChangeNumberFlow() = Unit
|
||||
fun openDeviceTransferFlow() = Unit
|
||||
fun openExportAccountDataFlow() = Unit
|
||||
fun openUpdateAppFlow() = Unit
|
||||
fun openReRegistrationFlow() = Unit
|
||||
fun openDeleteAccountFlow() = Unit
|
||||
fun deleteAllData() = Unit
|
||||
|
||||
object Empty : AccountSettingsScreenCallbacks
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
object AccountSettingsTestTags {
|
||||
const val SCROLLER = "scroller"
|
||||
const val ROW_MODIFY_PIN = "row-modify-pin"
|
||||
const val ROW_PIN_REMINDER = "row-pin-reminder"
|
||||
const val ROW_REGISTRATION_LOCK = "row-registration-lock"
|
||||
const val ROW_ADVANCED_PIN_SETTINGS = "row-advanced-pin-settings"
|
||||
const val ROW_CHANGE_PHONE_NUMBER = "row-change-phone-number"
|
||||
const val ROW_TRANSFER_ACCOUNT = "row-transfer-account"
|
||||
const val ROW_REQUEST_ACCOUNT_DATA = "row-request-account-data"
|
||||
const val ROW_UPDATE_SIGNAL = "row-update-signal"
|
||||
const val ROW_RE_REGISTER = "row-re-register"
|
||||
const val ROW_DELETE_ALL_DATA = "row-delete-all-data"
|
||||
const val ROW_DELETE_ACCOUNT = "row-delete-account"
|
||||
const val DIALOG_CONFIRM_DELETE_ALL_DATA = "dialog-confirm-delete-all-data"
|
||||
}
|
||||
|
||||
@Composable
|
||||
@VisibleForTesting
|
||||
fun AccountSettingsScreen(
|
||||
state: AccountSettingsState,
|
||||
callbacks: AccountSettingsScreenCallbacks
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.AccountSettingsFragment__account),
|
||||
onNavigationClick = callbacks::onNavigationClick,
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.ic_arrow_left_24)
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(contentPadding).testTag(AccountSettingsTestTags.SCROLLER)
|
||||
) {
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = stringResource(R.string.preferences_app_protection__signal_pin)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
@StringRes val textId = if (state.hasPin || state.hasRestoredAep) {
|
||||
R.string.preferences_app_protection__change_your_pin
|
||||
} else {
|
||||
R.string.preferences_app_protection__create_a_pin
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(textId),
|
||||
enabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
if (state.hasPin) {
|
||||
callbacks.onChangePinClick()
|
||||
} else {
|
||||
callbacks.onCreatePinClick()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_MODIFY_PIN)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(R.string.preferences_app_protection__pin_reminders),
|
||||
label = stringResource(R.string.AccountSettingsFragment__youll_be_asked_less_frequently),
|
||||
checked = state.hasPin && state.pinRemindersEnabled,
|
||||
enabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
|
||||
onCheckChanged = callbacks::setPinRemindersEnabled,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_PIN_REMINDER)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(R.string.preferences_app_protection__registration_lock),
|
||||
label = stringResource(R.string.AccountSettingsFragment__require_your_signal_pin),
|
||||
checked = state.registrationLockEnabled,
|
||||
enabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
|
||||
onCheckChanged = callbacks::setRegistrationLockEnabled,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_REGISTRATION_LOCK)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences__advanced_pin_settings),
|
||||
enabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = callbacks::openAdvancedPinSettings,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_ADVANCED_PIN_SETTINGS)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = stringResource(R.string.AccountSettingsFragment__account)
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.userUnregistered) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.AccountSettingsFragment__change_phone_number),
|
||||
enabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = callbacks::openChangeNumberFlow,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_CHANGE_PHONE_NUMBER)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences_chats__transfer_account),
|
||||
label = stringResource(R.string.preferences_chats__transfer_account_to_a_new_android_device),
|
||||
enabled = state.canTransferWhileUnregistered || state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = callbacks::openDeviceTransferFlow,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_TRANSFER_ACCOUNT)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.AccountSettingsFragment__request_account_data),
|
||||
enabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = callbacks::openExportAccountDataFlow,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_REQUEST_ACCOUNT_DATA)
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.isNotDeprecatedOrUnregistered()) {
|
||||
if (state.clientDeprecated) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences_account_update_signal),
|
||||
onClick = callbacks::openUpdateAppFlow,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_UPDATE_SIGNAL)
|
||||
)
|
||||
}
|
||||
} else if (state.userUnregistered) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences_account_reregister),
|
||||
onClick = callbacks::openReRegistrationFlow,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_RE_REGISTER)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
var displayDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.preferences_account_delete_all_data),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = colorResource(R.color.signal_alert_primary)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
displayDialog = true
|
||||
},
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_DELETE_ALL_DATA)
|
||||
)
|
||||
|
||||
if (displayDialog) {
|
||||
DeleteAllDataConfirmationDialog(
|
||||
onDismissRequest = { displayDialog = false },
|
||||
onConfirm = callbacks::deleteAllData
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
@ColorRes val textColor = if (state.isNotDeprecatedOrUnregistered()) {
|
||||
R.color.signal_alert_primary
|
||||
} else {
|
||||
R.color.signal_alert_primary_50
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.preferences__delete_account),
|
||||
color = colorResource(textColor)
|
||||
)
|
||||
},
|
||||
enabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = callbacks::openDeleteAccountFlow,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.ROW_DELETE_ACCOUNT)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeleteAllDataConfirmationDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.preferences_account_delete_all_data_confirmation_title),
|
||||
body = stringResource(R.string.preferences_account_delete_all_data_confirmation_message),
|
||||
confirm = stringResource(R.string.preferences_account_delete_all_data_confirmation_proceed),
|
||||
onConfirm = onConfirm,
|
||||
dismiss = stringResource(R.string.preferences_account_delete_all_data_confirmation_cancel),
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier.testTag(AccountSettingsTestTags.DIALOG_CONFIRM_DELETE_ALL_DATA)
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun AccountSettingsScreenPreview() {
|
||||
Previews.Preview {
|
||||
AccountSettingsScreen(
|
||||
state = AccountSettingsState(
|
||||
hasPin = true,
|
||||
hasRestoredAep = true,
|
||||
pinRemindersEnabled = true,
|
||||
registrationLockEnabled = true,
|
||||
userUnregistered = false,
|
||||
clientDeprecated = false,
|
||||
canTransferWhileUnregistered = true
|
||||
),
|
||||
callbacks = AccountSettingsScreenCallbacks.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun DeleteAllDataConfirmationDialogPreview() {
|
||||
Previews.Preview {
|
||||
DeleteAllDataConfirmationDialog({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.account
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class AccountSettingsViewModel : ViewModel() {
|
||||
private val store: Store<AccountSettingsState> = Store(getCurrentState())
|
||||
private val store: MutableStateFlow<AccountSettingsState> = MutableStateFlow(getCurrentState())
|
||||
|
||||
val state: LiveData<AccountSettingsState> = store.stateLiveData
|
||||
val state: StateFlow<AccountSettingsState> = store
|
||||
|
||||
fun refreshState() {
|
||||
store.update { getCurrentState() }
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.account
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class AccountSettingsScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun givenANormalRegisteredUserWithAPin_whenIDisplayScreen_thenIExpectChangePinFlowToLaunch() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState()
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(
|
||||
state = state,
|
||||
callbacks = callback
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_MODIFY_PIN).performClick()
|
||||
verify { callback.onChangePinClick() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenANormalRegisteredUserWithoutAPin_whenIDisplayScreen_thenIExpectChangePinFlowToLaunch() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(hasPin = false)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(
|
||||
state = state,
|
||||
callbacks = callback
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_MODIFY_PIN).performClick()
|
||||
verify { callback.onCreatePinClick() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUserWithPin_whenPinReminderToggleClicked_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(hasPin = true, pinRemindersEnabled = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_PIN_REMINDER)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify { callback.setPinRemindersEnabled(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUserWithoutPin_whenPinReminderDisplayed_thenRowIsDisabled() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(hasPin = false)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_PIN_REMINDER)
|
||||
.assertIsDisplayed()
|
||||
.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenRegistrationLockEnabled_whenToggleClicked_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(hasPin = true, registrationLockEnabled = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_REGISTRATION_LOCK)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify { callback.setRegistrationLockEnabled(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUserWithoutPin_whenRegistrationLockDisplayed_thenRowIsDisabled() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(hasPin = false, registrationLockEnabled = false)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_REGISTRATION_LOCK)
|
||||
.assertIsDisplayed()
|
||||
.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNormalUser_whenAdvancedPinSettingsClicked_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState()
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_ADVANCED_PIN_SETTINGS)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify { callback.openAdvancedPinSettings() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenRegisteredUser_whenChangePhoneNumberClicked_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(userUnregistered = false)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_CHANGE_PHONE_NUMBER))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_CHANGE_PHONE_NUMBER)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify { callback.openChangeNumberFlow() }
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun whenUnregisteredUser_thenChangePhoneNumberNotDisplayed() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(userUnregistered = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_CHANGE_PHONE_NUMBER))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNormalUser_whenTransferAccountClicked_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState()
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_TRANSFER_ACCOUNT))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_TRANSFER_ACCOUNT)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify { callback.openDeviceTransferFlow() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNormalUser_whenRequestAccountDataClicked_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState()
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_REQUEST_ACCOUNT_DATA))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_REQUEST_ACCOUNT_DATA)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify { callback.openExportAccountDataFlow() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenDeprecatedClient_whenUpdateSignalDisplayed_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(clientDeprecated = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_UPDATE_SIGNAL))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_UPDATE_SIGNAL)
|
||||
.assertIsDisplayed()
|
||||
.assertHasClickAction()
|
||||
.performClick()
|
||||
|
||||
verify { callback.openUpdateAppFlow() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUnregisteredUser_whenReRegisterDisplayed_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(userUnregistered = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_RE_REGISTER))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_RE_REGISTER)
|
||||
.assertIsDisplayed()
|
||||
.assertHasClickAction()
|
||||
.performClick()
|
||||
|
||||
verify { callback.openReRegistrationFlow() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenDeprecatedClient_whenDeleteAllDataClicked_thenDialogDisplayed() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(clientDeprecated = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_DELETE_ALL_DATA))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_DELETE_ALL_DATA)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.DIALOG_CONFIRM_DELETE_ALL_DATA)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNormalUser_whenDeleteAccountClicked_thenCallbackInvoked() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState()
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_DELETE_ACCOUNT))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_DELETE_ACCOUNT)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify { callback.openDeleteAccountFlow() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenDeprecatedClient_whenDeleteAccountDisplayed_thenDisabled() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(clientDeprecated = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_DELETE_ACCOUNT))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_DELETE_ACCOUNT)
|
||||
.assertIsDisplayed()
|
||||
.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUnregisteredUser_whenDeleteAccountDisplayed_thenDisabled() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(userUnregistered = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_DELETE_ACCOUNT))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_DELETE_ACCOUNT)
|
||||
.assertIsDisplayed()
|
||||
.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenUnregisteredButCanTransfer_thenTransferAccountEnabled() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(userUnregistered = true, canTransferWhileUnregistered = true)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_TRANSFER_ACCOUNT))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_TRANSFER_ACCOUNT)
|
||||
.assertIsDisplayed()
|
||||
.assertIsEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUnregisteredAndCannotTransfer_whenTransferAccountDisabled() {
|
||||
val callback = mockk<AccountSettingsScreenCallbacks>(relaxUnitFun = true)
|
||||
val state = createState(userUnregistered = true, canTransferWhileUnregistered = false)
|
||||
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(state = state, callbacks = callback)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(AccountSettingsTestTags.ROW_TRANSFER_ACCOUNT))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_TRANSFER_ACCOUNT)
|
||||
.assertIsDisplayed()
|
||||
.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
private fun createState(
|
||||
hasPin: Boolean = true,
|
||||
hasRestoredAep: Boolean = true,
|
||||
pinRemindersEnabled: Boolean = true,
|
||||
registrationLockEnabled: Boolean = true,
|
||||
userUnregistered: Boolean = false,
|
||||
clientDeprecated: Boolean = false,
|
||||
canTransferWhileUnregistered: Boolean = true
|
||||
): AccountSettingsState {
|
||||
return AccountSettingsState(
|
||||
hasPin = hasPin,
|
||||
hasRestoredAep = hasRestoredAep,
|
||||
pinRemindersEnabled = pinRemindersEnabled,
|
||||
registrationLockEnabled = registrationLockEnabled,
|
||||
userUnregistered = userUnregistered,
|
||||
clientDeprecated = clientDeprecated,
|
||||
canTransferWhileUnregistered = canTransferWhileUnregistered
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ tasks.register("qa") {
|
||||
"clean",
|
||||
"checkStopship",
|
||||
"buildQa",
|
||||
":Signal-Android:testPlayProdReleaseUnitTest",
|
||||
":Signal-Android:testPlayProdPerfUnitTest",
|
||||
":Signal-Android:lintPlayProdRelease",
|
||||
"Signal-Android:ktlintCheck",
|
||||
":libsignal-service:test",
|
||||
|
||||
@@ -158,9 +158,10 @@ object Rows {
|
||||
label: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
enabled: Boolean = true,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
val enabled = !isLoading
|
||||
val enabled = enabled && !isLoading
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
|
||||
Reference in New Issue
Block a user