[#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:
Honza Rychnovský 2024-03-04 17:45:51 +01:00 committed by GitHub
parent cc5f3504fe
commit 45ab8ce8c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 276 additions and 104 deletions

View File

@ -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,
)
}

View File

@ -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()

View File

@ -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 = {}
)
}

View File

@ -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()

View File

@ -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()

View File

@ -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 = {}
)
}

View File

@ -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)
}
}
},

View File

@ -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

View File

@ -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
)
}
}

View File

@ -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)

View File

@ -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
)
}

View File

@ -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 ?: ""))
}
}
},

View File

@ -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) ?: ""}"

View File

@ -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)))
}
)
}

View File

@ -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>

View File

@ -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">&lt;host&gt;:&lt;port&gt;</string>
<string name="choose_server_textfield_error">Invalid server</string>
<string name="choose_server_textfield_hint">&lt;hostname&gt;:&lt;port&gt;</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 &lt;hostname&gt;:&lt;port&gt;.</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>

View File

@ -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 &lt; <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>

View File

@ -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>