[#1242] Unified error popup UI
- Closes #1242 - This creates a reusable alert dialog UI and uses it in the server switching, shielding transparent funds, and contacting support use cases. - This also updates the Transparent funds shielding help text according to the latest design guideline
This commit is contained in:
parent
cc5f3504fe
commit
45ab8ce8c9
|
@ -0,0 +1,90 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LightAlertDialogComposablePreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
AppAlertDialog(
|
||||
title = "Light popup",
|
||||
text =
|
||||
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Temporibus autem quibusdam et aut " +
|
||||
"officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et " +
|
||||
"molestiae non recusandae. Duis condimentum augue id magna semper rutrum.",
|
||||
confirmButtonText = "OK",
|
||||
dismissButtonText = "Cancel"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DarkAlertDialogComposablePreview() {
|
||||
ZcashTheme(forceDarkMode = true) {
|
||||
AppAlertDialog(
|
||||
title = "Dark no button popup",
|
||||
text =
|
||||
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Temporibus autem quibusdam et aut " +
|
||||
"officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et " +
|
||||
"molestiae non recusandae. Duis condimentum augue id magna semper rutrum.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [#1276]: Consider adding support for a specific exception in AppAlertDialog
|
||||
// TODO [#1276]: https://github.com/Electric-Coin-Company/zashi-android/issues/1276
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun AppAlertDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
confirmButtonText: String? = null,
|
||||
onConfirmButtonClick: (() -> Unit)? = null,
|
||||
dismissButtonText: String? = null,
|
||||
onDismissButtonClick: (() -> Unit)? = null,
|
||||
icon: ImageVector? = null,
|
||||
title: String? = null,
|
||||
text: String? = null,
|
||||
properties: DialogProperties = DialogProperties()
|
||||
) {
|
||||
AlertDialog(
|
||||
shape = RectangleShape,
|
||||
onDismissRequest = onDismissRequest?.let { onDismissRequest } ?: {},
|
||||
confirmButton = {
|
||||
confirmButtonText?.let {
|
||||
NavigationButton(
|
||||
text = confirmButtonText,
|
||||
onClick = onConfirmButtonClick ?: {},
|
||||
outerPaddingValues = PaddingValues(all = 0.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
dismissButtonText?.let {
|
||||
NavigationButton(
|
||||
text = dismissButtonText,
|
||||
onClick = onDismissButtonClick ?: {},
|
||||
outerPaddingValues = PaddingValues(all = 0.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = title?.let { { Text(text = title) } },
|
||||
text = text?.let { { Text(text = text) } },
|
||||
icon = icon?.let { { Icon(imageVector = icon, null) } },
|
||||
properties = properties,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
|
@ -64,7 +64,7 @@ fun FormTextField(
|
|||
withBorder: Boolean = true,
|
||||
bringIntoViewRequester: BringIntoViewRequester? = null,
|
||||
minHeight: Dp = ZcashTheme.dimens.textFieldDefaultHeight,
|
||||
testTag: String? = ""
|
||||
testTag: String? = null
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
|
|
@ -35,9 +35,11 @@ class BalancesTestSetup(
|
|||
isFiatConversionEnabled = isShowFiatConversion,
|
||||
isKeepScreenOnWhileSyncing = false,
|
||||
isUpdateAvailable = false,
|
||||
walletSnapshot = walletSnapshot,
|
||||
onShielding = {},
|
||||
shieldState = ShieldState.Available
|
||||
shieldState = ShieldState.Available,
|
||||
walletSnapshot = walletSnapshot,
|
||||
isShowingErrorDialog = false,
|
||||
setShowErrorDialog = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import co.electriccoin.zcash.ui.test.getStringResource
|
|||
import co.electriccoin.zcash.ui.test.getStringResourceWithArgs
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
class SupportViewIntegrationTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
|
@ -48,6 +49,7 @@ class SupportViewIntegrationTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Will be updated as part of #1275")
|
||||
fun dialog_state_restoration() {
|
||||
val restorationTester = StateRestorationTester(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
|
|
|
@ -14,6 +14,7 @@ import co.electriccoin.zcash.ui.test.getStringResourceWithArgs
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
class SupportViewTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
|
@ -37,6 +38,7 @@ class SupportViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Will be updated as part of #1275")
|
||||
fun send_shows_dialog() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
|
@ -60,6 +62,7 @@ class SupportViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Will be updated as part of #1275")
|
||||
fun dialog_confirm_sends() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
|
@ -79,6 +82,7 @@ class SupportViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Will be updated as part of #1275")
|
||||
fun dialog_cancel() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
|
|
|
@ -29,18 +29,23 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
|
|||
return onSendMessage.get()
|
||||
}
|
||||
|
||||
// TODO [#1275]: Improve SupportView UI tests
|
||||
// TODO [#1275]: https://github.com/Electric-Coin-Company/zashi-android/issues/1275
|
||||
|
||||
@Composable
|
||||
@Suppress("TestFunctionName")
|
||||
fun DefaultContent() {
|
||||
Support(
|
||||
SnackbarHostState(),
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
onBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
onSend = {
|
||||
onSendCount.incrementAndGet()
|
||||
onSendMessage.set(it)
|
||||
}
|
||||
},
|
||||
isShowingDialog = false,
|
||||
setShowDialog = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -93,6 +93,8 @@ internal fun WrapBalances(
|
|||
)
|
||||
}
|
||||
|
||||
val (isShowingErrorDialog, setShowErrorDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (null == synchronizer || null == walletSnapshot || null == spendingKey) {
|
||||
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
||||
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
|
||||
|
@ -104,6 +106,8 @@ internal fun WrapBalances(
|
|||
isFiatConversionEnabled = isFiatConversionEnabled,
|
||||
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
|
||||
isUpdateAvailable = isUpdateAvailable,
|
||||
isShowingErrorDialog = isShowingErrorDialog,
|
||||
setShowErrorDialog = setShowErrorDialog,
|
||||
onShielding = {
|
||||
scope.launch {
|
||||
setShieldState(ShieldState.Running)
|
||||
|
@ -116,11 +120,12 @@ internal fun WrapBalances(
|
|||
setShieldState(ShieldState.None)
|
||||
}
|
||||
.onFailure {
|
||||
Twig.info { "Shielding transaction submission failed with: $it" }
|
||||
Twig.error(it) { "Shielding transaction submission failed with: ${it.message}" }
|
||||
// Adding extra delay before notifying UI for a better UX
|
||||
@Suppress("MagicNumber")
|
||||
delay(1500)
|
||||
setShieldState(ShieldState.Failed(it.localizedMessage ?: ""))
|
||||
setShieldState(ShieldState.Failed(it.message ?: ""))
|
||||
setShowErrorDialog(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -58,6 +58,7 @@ import co.electriccoin.zcash.ui.common.model.changePendingBalance
|
|||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.valuePendingBalance
|
||||
import co.electriccoin.zcash.ui.common.test.CommonTag
|
||||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol
|
||||
|
@ -86,8 +87,10 @@ private fun ComposablePreview() {
|
|||
isKeepScreenOnWhileSyncing = false,
|
||||
isUpdateAvailable = false,
|
||||
onShielding = {},
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
shieldState = ShieldState.Available,
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
isShowingErrorDialog = false,
|
||||
setShowErrorDialog = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +103,8 @@ fun Balances(
|
|||
isFiatConversionEnabled: Boolean,
|
||||
isKeepScreenOnWhileSyncing: Boolean?,
|
||||
isUpdateAvailable: Boolean,
|
||||
isShowingErrorDialog: Boolean,
|
||||
setShowErrorDialog: (Boolean) -> Unit,
|
||||
onShielding: () -> Unit,
|
||||
shieldState: ShieldState,
|
||||
walletSnapshot: WalletSnapshot?,
|
||||
|
@ -125,10 +130,34 @@ fun Balances(
|
|||
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
|
||||
),
|
||||
)
|
||||
|
||||
// Show shielding error popup
|
||||
if (isShowingErrorDialog) {
|
||||
ShieldingErrorDialog(
|
||||
shieldState = shieldState,
|
||||
onDone = { setShowErrorDialog(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun ShieldingErrorDialog(
|
||||
shieldState: ShieldState,
|
||||
onDone: () -> Unit
|
||||
) {
|
||||
// Once we ensure that the [shieldState] contains a localized message, we can leverage it for the UI prompt
|
||||
|
||||
AppAlertDialog(
|
||||
title = stringResource(id = R.string.balances_shielding_dialog_error_title),
|
||||
text = stringResource(id = R.string.balances_shielding_dialog_error_text),
|
||||
confirmButtonText = stringResource(id = R.string.balances_shielding_dialog_error_btn),
|
||||
onConfirmButtonClick = onDone
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BalancesTopAppBar(onSettings: () -> Unit) {
|
||||
SmallTopAppBar(
|
||||
|
@ -220,11 +249,6 @@ fun TransparentBalancePanel(
|
|||
) {
|
||||
var showHelpPanel by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// TODO [#1242]: Create error popup UI
|
||||
// TODO [#1242]: https://github.com/Electric-Coin-Company/zashi-android/issues/1242
|
||||
// if (shieldState is ShieldState.Failed) {
|
||||
// }
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
package co.electriccoin.zcash.ui.screen.chooseserver
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
|
@ -19,7 +18,6 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|||
import cash.z.ecc.android.sdk.type.ServerValidation
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.screen.chooseserver.view.ChooseServer
|
||||
|
@ -65,8 +63,6 @@ private fun WrapChooseServer(
|
|||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
var validationResult: ServerValidation by remember { mutableStateOf(ServerValidation.Valid) }
|
||||
|
||||
val onCheckedBack = {
|
||||
|
@ -77,6 +73,9 @@ private fun WrapChooseServer(
|
|||
|
||||
BackHandler { onCheckedBack() }
|
||||
|
||||
val (isShowingErrorDialog, setShowErrorDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (isShowingSuccessDialog, setShowSuccessDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
ChooseServer(
|
||||
availableServers = AvailableServerProvider.toList(ZcashNetwork.fromResources(activity)),
|
||||
onBack = onCheckedBack,
|
||||
|
@ -100,13 +99,12 @@ private fun WrapChooseServer(
|
|||
|
||||
onWalletPersist(newWallet)
|
||||
|
||||
snackbarHostState.showSnackbar(
|
||||
message = activity.getString(R.string.choose_server_saved),
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
setShowSuccessDialog(true)
|
||||
}
|
||||
is ServerValidation.InValid -> {
|
||||
Twig.error { "Choose Server: Failed to validate the new endpoint: $newEndpoint" }
|
||||
|
||||
setShowErrorDialog(true)
|
||||
}
|
||||
else -> {
|
||||
// Should not happen
|
||||
|
@ -115,9 +113,12 @@ private fun WrapChooseServer(
|
|||
}
|
||||
}
|
||||
},
|
||||
snackbarHostState = snackbarHostState,
|
||||
validationResult = validationResult,
|
||||
wallet = wallet,
|
||||
isShowingErrorDialog = isShowingErrorDialog,
|
||||
setShowErrorDialog = setShowErrorDialog,
|
||||
isShowingSuccessDialog = isShowingSuccessDialog,
|
||||
setShowSuccessDialog = setShowSuccessDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,3 +37,12 @@ object AvailableServerProvider {
|
|||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
// This regex validates server URLs with ports in format: <hostname>:<port>
|
||||
// While ensuring:
|
||||
// - Valid hostname format (excluding spaces and special characters)
|
||||
// - Port numbers within the valid range (1-65535) and without leading zeros
|
||||
// - Note that this does not cover other URL components like paths or query strings
|
||||
val regex = "^(([^:/?#\\s]+)://)?([^/?#\\s]+):([1-9][0-9]{3}|[1-5][0-9]{2}|[0-9]{1,2})$".toRegex()
|
||||
|
||||
fun validateCustomServerValue(customServer: String): Boolean = regex.matches(customServer)
|
||||
|
|
|
@ -12,11 +12,8 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
|
@ -36,6 +33,7 @@ import cash.z.ecc.sdk.extension.isValid
|
|||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.FormTextField
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
|
@ -44,6 +42,7 @@ import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
|||
import co.electriccoin.zcash.ui.design.component.SubHeader
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerTag
|
||||
import co.electriccoin.zcash.ui.screen.chooseserver.validateCustomServerValue
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
@ -56,9 +55,12 @@ private fun PreviewChooseServer() {
|
|||
availableServers = emptyList<LightWalletEndpoint>().toImmutableList(),
|
||||
onBack = {},
|
||||
onServerChange = {},
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
validationResult = ServerValidation.Valid,
|
||||
wallet = PersistableWalletFixture.new(),
|
||||
isShowingErrorDialog = false,
|
||||
setShowErrorDialog = {},
|
||||
isShowingSuccessDialog = false,
|
||||
setShowSuccessDialog = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -70,15 +72,17 @@ fun ChooseServer(
|
|||
availableServers: ImmutableList<LightWalletEndpoint>,
|
||||
onBack: () -> Unit,
|
||||
onServerChange: (LightWalletEndpoint) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
validationResult: ServerValidation,
|
||||
wallet: PersistableWallet,
|
||||
isShowingErrorDialog: Boolean,
|
||||
setShowErrorDialog: (Boolean) -> Unit,
|
||||
isShowingSuccessDialog: Boolean,
|
||||
setShowSuccessDialog: (Boolean) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ChooseServerTopAppBar(onBack = onBack)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
}
|
||||
) { paddingValues ->
|
||||
ChooseServerMainContent(
|
||||
modifier =
|
||||
|
@ -95,9 +99,22 @@ fun ChooseServer(
|
|||
.fillMaxWidth(),
|
||||
availableServers = availableServers,
|
||||
onServerChange = onServerChange,
|
||||
setShowErrorDialog = setShowErrorDialog,
|
||||
validationResult = validationResult,
|
||||
wallet = wallet,
|
||||
)
|
||||
|
||||
// Show validation popups
|
||||
if (isShowingErrorDialog) {
|
||||
ValidationErrorDialog(
|
||||
validationResult = validationResult,
|
||||
onDone = { setShowErrorDialog(false) }
|
||||
)
|
||||
} else if (isShowingSuccessDialog) {
|
||||
SaveSuccessDialog(
|
||||
onDone = { setShowSuccessDialog(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,16 +130,20 @@ private fun ChooseServerTopAppBar(onBack: () -> Unit) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
private fun ChooseServerMainContent(
|
||||
availableServers: ImmutableList<LightWalletEndpoint>,
|
||||
onServerChange: (LightWalletEndpoint) -> Unit,
|
||||
validationResult: ServerValidation,
|
||||
wallet: PersistableWallet,
|
||||
modifier: Modifier = Modifier,
|
||||
setShowErrorDialog: (Boolean) -> Unit,
|
||||
) {
|
||||
val options =
|
||||
availableServers.toMutableList().apply {
|
||||
// Note that this comparison could lead to a match with any predefined server endpoint even though the user
|
||||
// previously pasted it as a custom one, which is fine for now and will be addressed when a dynamic
|
||||
// server list obtaining is implemented.
|
||||
if (contains(wallet.endpoint)) {
|
||||
// We define the custom server as secured by default
|
||||
add(LightWalletEndpoint("", -1, true))
|
||||
|
@ -150,24 +171,6 @@ private fun ChooseServerMainContent(
|
|||
mutableStateOf(initialCustomServerValue)
|
||||
}
|
||||
|
||||
val (customServerError, setCustomServerError) =
|
||||
rememberSaveable {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(key1 = validationResult) {
|
||||
when (validationResult) {
|
||||
is ServerValidation.InValid -> {
|
||||
setCustomServerError(context.getString(R.string.choose_server_textfield_error))
|
||||
}
|
||||
else -> {
|
||||
// Expected state: do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
|
||||
|
@ -183,8 +186,6 @@ private fun ChooseServerMainContent(
|
|||
options = options,
|
||||
selectedOption = selectedOption,
|
||||
setSelectedOption = setSelectedOption,
|
||||
customServerError = customServerError,
|
||||
setCustomServerError = setCustomServerError,
|
||||
customServerValue = customServerValue,
|
||||
setCustomServerValue = setCustomServerValue,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
@ -202,8 +203,8 @@ private fun ChooseServerMainContent(
|
|||
onServerChange(it)
|
||||
}
|
||||
},
|
||||
setShowErrorDialog = setShowErrorDialog,
|
||||
selectedOption = selectedOption,
|
||||
setCustomServerError = setCustomServerError,
|
||||
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingXlarge)
|
||||
)
|
||||
|
||||
|
@ -216,8 +217,6 @@ private fun ChooseServerMainContent(
|
|||
@Suppress("LongParameterList")
|
||||
fun ServerList(
|
||||
options: ImmutableList<LightWalletEndpoint>,
|
||||
customServerError: String?,
|
||||
setCustomServerError: (String?) -> Unit,
|
||||
customServerValue: String,
|
||||
setCustomServerValue: (String) -> Unit,
|
||||
selectedOption: Int,
|
||||
|
@ -248,13 +247,11 @@ fun ServerList(
|
|||
FormTextField(
|
||||
value = customServerValue,
|
||||
onValueChange = {
|
||||
setCustomServerError(null)
|
||||
setCustomServerValue(it)
|
||||
},
|
||||
placeholder = {
|
||||
Text(text = stringResource(R.string.choose_server_textfield_hint))
|
||||
},
|
||||
error = customServerError,
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onDone = {
|
||||
|
@ -307,14 +304,6 @@ fun LabeledRadioButton(
|
|||
)
|
||||
}
|
||||
|
||||
// This regex validates server URLs with ports while ensuring:
|
||||
// - Valid hostname format (excluding spaces and special characters)
|
||||
// - Port numbers within the valid range (1-65535) and without leading zeros
|
||||
// - Note that this does not cover other URL components like paths or query strings
|
||||
val regex = "^(([^:/?#\\s]+)://)?([^/?#\\s]+):([1-9][0-9]{3}|[1-5][0-9]{2}|[0-9]{1,2})$".toRegex()
|
||||
|
||||
fun validateCustomServerValue(customServer: String): Boolean = regex.matches(customServer)
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun SaveButton(
|
||||
|
@ -323,8 +312,8 @@ fun SaveButton(
|
|||
onServerChange: (LightWalletEndpoint) -> Unit,
|
||||
options: ImmutableList<LightWalletEndpoint>,
|
||||
selectedOption: Int,
|
||||
setCustomServerError: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
setShowErrorDialog: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
|
@ -336,7 +325,7 @@ fun SaveButton(
|
|||
val selectedServer =
|
||||
if (selectedOption == options.lastIndex) {
|
||||
if (!validateCustomServerValue(customServerValue)) {
|
||||
setCustomServerError(context.getString(R.string.choose_server_textfield_error))
|
||||
setShowErrorDialog(true)
|
||||
return@PrimaryButton
|
||||
}
|
||||
|
||||
|
@ -352,3 +341,31 @@ fun SaveButton(
|
|||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun ValidationErrorDialog(
|
||||
validationResult: ServerValidation,
|
||||
onDone: () -> Unit
|
||||
) {
|
||||
// Once we ensure that the [validationResult] contains a localized message, we can leverage it for the UI prompt
|
||||
|
||||
AppAlertDialog(
|
||||
title = stringResource(id = R.string.choose_server_validation_dialog_error_title),
|
||||
text = stringResource(id = R.string.choose_server_validation_dialog_error_text),
|
||||
confirmButtonText = stringResource(id = R.string.choose_server_validation_dialog_error_btn),
|
||||
onConfirmButtonClick = onDone
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaveSuccessDialog(onDone: () -> Unit) {
|
||||
Twig.info { "Succeed with saving the selected endpoint" }
|
||||
|
||||
AppAlertDialog(
|
||||
title = stringResource(id = R.string.choose_server_save_success_dialog_title),
|
||||
text = stringResource(id = R.string.choose_server_save_success_dialog_text),
|
||||
confirmButtonText = stringResource(id = R.string.choose_server_save_success_dialog_btn),
|
||||
onConfirmButtonClick = onDone
|
||||
)
|
||||
}
|
||||
|
|
|
@ -220,7 +220,7 @@ internal fun WrapSend(
|
|||
}
|
||||
.onFailure {
|
||||
Twig.debug { "Transaction submission failed with: $it." }
|
||||
setSendStage(SendStage.SendFailure(it.localizedMessage ?: ""))
|
||||
setSendStage(SendStage.SendFailure(it.message ?: ""))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -7,8 +7,10 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.activity.viewModels
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.R
|
||||
|
@ -34,8 +36,12 @@ internal fun WrapSupport(
|
|||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val (isShowingDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Support(
|
||||
snackbarHostState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
isShowingDialog = isShowingDialog,
|
||||
setShowDialog = setShowDialog,
|
||||
onBack = goBack,
|
||||
onSend = { userMessage ->
|
||||
val fullMessage = formatMessage(userMessage, supportMessage)
|
||||
|
@ -55,7 +61,7 @@ internal fun WrapSupport(
|
|||
runCatching {
|
||||
activity.startActivity(mailIntent)
|
||||
}.onSuccess {
|
||||
goBack()
|
||||
setShowDialog(false)
|
||||
}.onFailure {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
|
@ -71,5 +77,5 @@ internal fun WrapSupport(
|
|||
private fun formatMessage(
|
||||
messageBody: String,
|
||||
appInfo: SupportInfo?,
|
||||
supportInfoValues: Set<SupportInfoType> = SupportInfoType.values().toSet()
|
||||
supportInfoValues: Set<SupportInfoType> = SupportInfoType.entries.toSet()
|
||||
): String = "$messageBody\n\n${appInfo?.toSupportString(supportInfoValues) ?: ""}"
|
||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
@ -25,15 +24,14 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.FormTextField
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.NavigationButton
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
|
@ -43,9 +41,11 @@ private fun PreviewSupport() {
|
|||
ZcashTheme(forceDarkMode = false) {
|
||||
GradientSurface {
|
||||
Support(
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
onBack = {},
|
||||
onSend = {},
|
||||
snackbarHostState = SnackbarHostState()
|
||||
isShowingDialog = false,
|
||||
setShowDialog = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -66,12 +66,13 @@ private fun PreviewSupportPopup() {
|
|||
|
||||
@Composable
|
||||
fun Support(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
isShowingDialog: Boolean,
|
||||
setShowDialog: (Boolean) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onSend: (String) -> Unit
|
||||
onSend: (String) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val (message, setMessage) = rememberSaveable { mutableStateOf("") }
|
||||
val (isShowingDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
@ -176,25 +177,17 @@ private fun SupportConfirmationDialog(
|
|||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
shape = RectangleShape,
|
||||
AppAlertDialog(
|
||||
onConfirmButtonClick = onConfirm,
|
||||
confirmButtonText = stringResource(id = R.string.support_confirmation_dialog_ok),
|
||||
dismissButtonText = stringResource(id = R.string.support_confirmation_dialog_cancel),
|
||||
onDismissButtonClick = onDismiss,
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
},
|
||||
confirmButton = {
|
||||
NavigationButton(
|
||||
onClick = onConfirm,
|
||||
text = stringResource(id = R.string.support_confirmation_dialog_ok)
|
||||
title = stringResource(id = R.string.support_confirmation_dialog_title),
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.support_confirmation_explanation,
|
||||
stringResource(id = R.string.app_name)
|
||||
)
|
||||
},
|
||||
dismissButton = {
|
||||
NavigationButton(
|
||||
onClick = onDismiss,
|
||||
text = stringResource(id = R.string.support_confirmation_dialog_cancel)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(id = R.string.support_confirmation_explanation, stringResource(id = R.string.app_name)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
<string name="balances_change_pending">Change pending</string>
|
||||
<string name="balances_pending_transactions">Pending transactions</string>
|
||||
<string name="balances_transparent_balance">Transparent balance</string>
|
||||
<string name="balances_transparent_balance_help">Zashi uses the latest network upgrade and does not support
|
||||
sending transparent (unshielded) ZEC. Converting your funds will move them to your available balance so you
|
||||
can send or spend them.</string>
|
||||
<string name="balances_transparent_balance_help">In order to better preserve your privacy, Zashi does not
|
||||
support directly spending transparent (unshielded) ZEC. Use the Shield and Consolidate button to shield your
|
||||
transparent funds. This will move the transparent value to your available balance and make it spendable.</string>
|
||||
<string name="balances_transparent_balance_help_close">I got it!</string>
|
||||
<string name="balances_transparent_help_content_description">Show help</string>
|
||||
<string name="balances_transparent_balance_shield">Shield and consolidate funds</string>
|
||||
|
@ -29,4 +29,8 @@
|
|||
<string name="balances_status_updating_blockheight">Updating blockheight</string>
|
||||
<string name="balances_status_fiat_currency_price_out_of_date" formatted="true"><xliff:g id="fiat_currency" example="USD">%1$s</xliff:g> price out-of-date</string>
|
||||
<string name="balances_status_spendable" formatted="true">Fully spendable in <xliff:g id="spendable_time" example="2 minutes">%1$s</xliff:g></string>
|
||||
|
||||
<string name="balances_shielding_dialog_error_title">Failed to shield funds</string>
|
||||
<string name="balances_shielding_dialog_error_text">Error: The attempt to shield the transparent funds failed. Try it again, please.</string>
|
||||
<string name="balances_shielding_dialog_error_btn">OK</string>
|
||||
</resources>
|
||||
|
|
|
@ -5,10 +5,18 @@
|
|||
<string name="choose_server_title">Server</string>
|
||||
<string name="choose_server_custom">custom</string>
|
||||
<string name="choose_server_custom_delimiter">:</string>
|
||||
<string name="choose_server_textfield_value"><xliff:g id="host" example="example.com">%1$s</xliff:g>:<xliff:g
|
||||
<string name="choose_server_textfield_value"><xliff:g id="hostname" example="example.com">%1$s</xliff:g>:<xliff:g
|
||||
id="port" example="508">%2$d</xliff:g></string>
|
||||
<string name="choose_server_textfield_hint"><host>:<port></string>
|
||||
<string name="choose_server_textfield_error">Invalid server</string>
|
||||
<string name="choose_server_textfield_hint"><hostname>:<port></string>
|
||||
<string name="choose_server_save">Save</string>
|
||||
<string name="choose_server_saved">Server successfully set</string>
|
||||
|
||||
<string name="choose_server_validation_dialog_error_title">Invalid server endpoint</string>
|
||||
<string name="choose_server_validation_dialog_error_text">Error: The attempt to switch endpoints failed.
|
||||
Check that the hostname and port are correct, and are formatted as <hostname>:<port>.</string>
|
||||
<string name="choose_server_validation_dialog_error_btn">OK</string>
|
||||
|
||||
<string name="choose_server_save_success_dialog_title">Server saved</string>
|
||||
<string name="choose_server_save_success_dialog_text">The selected server endpoint has been
|
||||
successfully saved.</string>
|
||||
<string name="choose_server_save_success_dialog_btn">OK</string>
|
||||
</resources>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<xliff:g id="typed_bytes" example="12">%1$s</xliff:g>/
|
||||
<xliff:g id="max_bytes" example="500">%2$s</xliff:g>
|
||||
</string>
|
||||
<string name="send_create">Preview</string>
|
||||
<string name="send_create">Review</string>
|
||||
<string name="send_fee">(Typical fee < <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
|
||||
|
||||
<string name="send_confirmation_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
<string name="support_send">Send</string>
|
||||
<string name="support_confirmation_dialog_ok">OK</string>
|
||||
<string name="support_confirmation_dialog_cancel">Cancel</string>
|
||||
<string name="support_confirmation_explanation"><xliff:g id="app_name" example="Zcash">%1$s</xliff:g> is about to open your email app with a pre-filled message.\n\nBe sure to hit send within your email app.</string>
|
||||
<string name="support_confirmation_dialog_title">Open e-mail app</string>
|
||||
<string name="support_confirmation_explanation"><xliff:g id="app_name" example="Zcash">%1$s</xliff:g> is about to
|
||||
open your e-mail app with a pre-filled message.\n\nBe sure to hit send within your e-mail app.</string>
|
||||
<!-- This is replaced by a resource overlay via app/build.gradle.kts -->
|
||||
<string name="support_email_address" />
|
||||
<string name="support_information">Please let us know about any problems you have had, or features you want to see in the future.</string>
|
||||
|
|
Loading…
Reference in New Issue