[#1294] Send Multi-Trx-Submission failure screen

- Closes #1294
- Changelog update
This commit is contained in:
Honza Rychnovský 2024-03-25 20:50:31 +01:00 committed by GitHub
parent 73de78caf9
commit e2eb043afb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 597 additions and 141 deletions

View File

@ -11,12 +11,14 @@ directly impact users rather than highlighting other key architectural updates.*
### Added
- Advanced Settings screen that provides more technical options like Export private data, Recovery phrase, or
Choose server
- A new Server switching screen was added. Its purpose is to enable switching between predefined and custom
Choose server has been added
- A new Server switching screen has been added. Its purpose is to enable switching between predefined and custom
lightwalletd servers in runtime.
- The About screen now contains a link to the new Zashi Privacy Policy website
- The Send Confirmation screen has been reworked according to the new design
- Transitions between screens are now animated with a simple slide animation
- Proposal API from the Zcash SDK has been integrated together with handling error states for multi-transaction
submission
### Changed
- The Transaction History UI has been incorporated into the Account screen

View File

@ -1,4 +1,5 @@
Note: Contact Support will fail on some devices without an app to handle email, such as an Android TV device. See issue #386
Note: Contact Support will fail and display an error message on some devices without an app to handle email, such as an
Android TV device.
# Check Support Email Contents
1. If using a test device or emulator, be sure to configure a default email app. For example, try opening the Gmail app and confirm that it shows your inbox.

View File

@ -17,7 +17,8 @@ data class Dimens(
val spacingMid: Dp,
val spacingDefault: Dp,
val spacingLarge: Dp,
val spacingXlarge: Dp,
val spacingUpLarge: Dp,
val spacingBig: Dp,
val spacingHuge: Dp,
// List of custom spacings:
// Button:
@ -62,7 +63,8 @@ private val defaultDimens =
spacingMid = 12.dp,
spacingDefault = 16.dp,
spacingLarge = 24.dp,
spacingXlarge = 32.dp,
spacingUpLarge = 32.dp,
spacingBig = 48.dp,
spacingHuge = 64.dp,
buttonShadowOffsetX = 20.dp,
buttonShadowOffsetY = 20.dp,

View File

@ -104,8 +104,8 @@ internal val SecondaryTypography =
headlineSmall =
TextStyle(
fontFamily = ArchivoFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
textAlign = TextAlign.Center
),
bodyLarge =

View File

@ -4,7 +4,7 @@ import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.common.model.SerializableAddress
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
internal object SendArgumentsWrapperFixture {
val RECIPIENT_ADDRESS =
@ -14,7 +14,7 @@ internal object SendArgumentsWrapperFixture {
)
fun new(recipientAddress: SerializableAddress? = RECIPIENT_ADDRESS) =
SendArgumentsWrapper(
SendArguments(
recipientAddress = recipientAddress?.toRecipient(),
)
}

View File

@ -67,7 +67,7 @@ class SendViewIntegrationTest {
restorationTester.setContent {
WrapSend(
sendArgumentsWrapper = null,
sendArguments = null,
focusManager = LocalFocusManager.current,
synchronizer = synchronizer,
walletSnapshot = walletSnapshot,

View File

@ -1,14 +1,12 @@
package co.electriccoin.zcash.ui.screen.support.model
import co.electriccoin.zcash.ui.test.getAppContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class SupportInfoTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun filter_time() =
runTest {
@ -23,7 +21,6 @@ class SupportInfoTest {
assertFalse(actualExcluded.contains(individualExpected))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun filter_app() =
runTest {
@ -38,7 +35,6 @@ class SupportInfoTest {
assertFalse(actualExcluded.contains(individualExpected))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun filter_os() =
runTest {
@ -53,7 +49,6 @@ class SupportInfoTest {
assertFalse(actualExcluded.contains(individualExpected))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun filter_device() =
runTest {
@ -68,7 +63,6 @@ class SupportInfoTest {
assertFalse(actualExcluded.contains(individualExpected))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun filter_crash() =
runTest {
@ -78,12 +72,8 @@ class SupportInfoTest {
val actualIncluded = supportInfo.toSupportString(setOf(SupportInfoType.Crash))
assertTrue(actualIncluded.contains(individualExpected))
val actualExcluded = supportInfo.toSupportString(emptySet())
assertFalse(actualExcluded.contains(individualExpected))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun filter_environment() =
runTest {
@ -98,7 +88,6 @@ class SupportInfoTest {
assertFalse(actualExcluded.contains(individualExpected))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun filter_permission() =
runTest {

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.screen.support.util
import androidx.test.filters.SmallTest
import co.electriccoin.zcash.ui.util.EmailUtil
import kotlin.test.Test
import kotlin.test.assertEquals

View File

@ -6,6 +6,7 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_MEMO
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_PROPOSAL
@ -38,9 +39,9 @@ import co.electriccoin.zcash.ui.screen.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery
import co.electriccoin.zcash.ui.screen.send.ext.toSerializableAddress
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import co.electriccoin.zcash.ui.screen.sendconfirmation.WrapSendConfirmation
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationArgsWrapper
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationArguments
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
import co.electriccoin.zcash.ui.screen.support.WrapSupport
import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate
@ -65,13 +66,13 @@ internal fun MainActivity.Navigation() {
popEnterTransition = { popEnterTransition() },
popExitTransition = { popExitTransition() }
) {
composable(HOME) { backStackEntry ->
composable(HOME) { backStack ->
WrapHome(
goBack = { finish() },
goScan = { navController.navigateJustOnce(SCAN) },
onPageChange = {
homeViewModel.screenIndex.value = it
},
goBack = { finish() },
goScan = { navController.navigateJustOnce(SCAN) },
goSendConfirmation = { zecSend ->
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
handle[SEND_CONFIRM_RECIPIENT_ADDRESS] =
@ -86,16 +87,18 @@ internal fun MainActivity.Navigation() {
navController.navigateJustOnce(SEND_CONFIRMATION)
},
goSettings = { navController.navigateJustOnce(SETTINGS) },
// At this point we only read scan result data
sendArgumentsWrapper =
SendArgumentsWrapper(
sendArguments =
SendArguments(
recipientAddress =
backStackEntry.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
backStack.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
Json.decodeFromString<SerializableAddress>(it).toRecipient()
},
clearForm = backStack.savedStateHandle.get<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM) ?: false
).also {
// Remove Send screen arguments passed from the Scan screen if some exist after we use them
backStackEntry.savedStateHandle.remove<String>(SEND_SCAN_RECIPIENT_ADDRESS)
// Remove Send screen arguments passed from the Scan or MultipleSubmissionFailure screens if
// some exist after we use them
backStack.savedStateHandle.remove<String>(SEND_SCAN_RECIPIENT_ADDRESS)
backStack.savedStateHandle.remove<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM)
},
)
@ -185,10 +188,15 @@ internal fun MainActivity.Navigation() {
composable(route = SEND_CONFIRMATION) {
navController.previousBackStackEntry?.let { backStackEntry ->
WrapSendConfirmation(
goBack = { navController.popBackStackJustOnce(SEND_CONFIRMATION) },
goBack = { clearForm ->
navController.previousBackStackEntry?.savedStateHandle?.apply {
set(MULTIPLE_SUBMISSION_CLEAR_FORM, clearForm)
}
navController.popBackStackJustOnce(SEND_CONFIRMATION)
},
goHome = { navController.navigateJustOnce(HOME) },
arguments =
SendConfirmationArgsWrapper.fromSavedStateHandle(backStackEntry.savedStateHandle).also {
SendConfirmationArguments.fromSavedStateHandle(backStackEntry.savedStateHandle).also {
// Remove SendConfirmation screen arguments passed from the Send screen if some exist
// after we use them
backStackEntry.savedStateHandle.remove<String>(SEND_CONFIRM_RECIPIENT_ADDRESS)
@ -237,6 +245,8 @@ object NavigationArguments {
const val SEND_CONFIRM_AMOUNT = "send_confirm_amount"
const val SEND_CONFIRM_MEMO = "send_confirm_memo"
const val SEND_CONFIRM_PROPOSAL = "send_confirm_proposal"
const val MULTIPLE_SUBMISSION_CLEAR_FORM = "multiple_submission_clear_form"
}
object NavigationTargets {

View File

@ -159,7 +159,7 @@ private fun AccountMainContent(
.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
HistoryContainer(
transactionState = transactionState,

View File

@ -165,7 +165,7 @@ private fun HistoryList(
) {
if (transactions.isEmpty()) {
Column {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Text(
modifier = Modifier.fillMaxWidth(),

View File

@ -315,7 +315,7 @@ fun TransparentBalancePanel(
textFontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
}
if (showHelpPanel) {
@ -611,7 +611,7 @@ fun SyncStatus(
progress = walletSnapshot.progress.decimal,
modifier =
Modifier.padding(
horizontal = ZcashTheme.dimens.spacingXlarge
horizontal = ZcashTheme.dimens.spacingUpLarge
)
)
}

View File

@ -205,7 +205,7 @@ private fun ChooseServerMainContent(
},
setShowErrorDialog = setShowErrorDialog,
selectedOption = selectedOption,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingXlarge)
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingUpLarge)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))

View File

@ -18,7 +18,7 @@ import co.electriccoin.zcash.ui.screen.home.model.TabItem
import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.receive.WrapReceive
import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -31,7 +31,7 @@ internal fun MainActivity.WrapHome(
goSettings: () -> Unit,
goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
sendArgumentsWrapper: SendArgumentsWrapper
sendArguments: SendArguments
) {
WrapHome(
this,
@ -40,7 +40,7 @@ internal fun MainActivity.WrapHome(
goScan = goScan,
goSendConfirmation = goSendConfirmation,
goSettings = goSettings,
sendArgumentsWrapper = sendArgumentsWrapper
sendArguments = sendArguments
)
}
@ -53,7 +53,7 @@ internal fun WrapHome(
goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
onPageChange: (HomeScreenIndex) -> Unit,
sendArgumentsWrapper: SendArgumentsWrapper
sendArguments: SendArguments
) {
val homeViewModel by activity.viewModels<HomeViewModel>()
@ -105,7 +105,7 @@ internal fun WrapHome(
goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) },
goSendConfirmation = goSendConfirmation,
goSettings = goSettings,
sendArgumentsWrapper = sendArgumentsWrapper
sendArguments = sendArguments
)
}
),

View File

@ -31,7 +31,7 @@ import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.send.model.AmountState
import co.electriccoin.zcash.ui.screen.send.model.MemoState
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.launch
@ -41,7 +41,7 @@ import java.util.Locale
@Suppress("LongParameterList")
internal fun WrapSend(
activity: ComponentActivity,
sendArgumentsWrapper: SendArgumentsWrapper?,
sendArguments: SendArguments?,
goToQrScanner: () -> Unit,
goBack: () -> Unit,
goBalances: () -> Unit,
@ -72,7 +72,7 @@ internal fun WrapSend(
val monetarySeparators = MonetarySeparators.current(Locale.US)
WrapSend(
sendArgumentsWrapper,
sendArguments,
synchronizer,
walletSnapshot,
spendingKey,
@ -91,7 +91,7 @@ internal fun WrapSend(
@VisibleForTesting
@Composable
internal fun WrapSend(
sendArgumentsWrapper: SendArgumentsWrapper?,
sendArguments: SendArguments?,
synchronizer: Synchronizer?,
walletSnapshot: WalletSnapshot?,
spendingKey: UnifiedSpendingKey?,
@ -116,13 +116,13 @@ internal fun WrapSend(
// Address computation:
val (recipientAddressState, setRecipientAddressState) =
rememberSaveable(stateSaver = RecipientAddressState.Saver) {
mutableStateOf(RecipientAddressState(zecSend?.destination?.address ?: "", null))
mutableStateOf(RecipientAddressState.new(zecSend?.destination?.address ?: "", null))
}
if (sendArgumentsWrapper?.recipientAddress != null) {
if (sendArguments?.recipientAddress != null) {
setRecipientAddressState(
RecipientAddressState.new(
sendArgumentsWrapper.recipientAddress.address,
sendArgumentsWrapper.recipientAddress.type
sendArguments.recipientAddress.address,
sendArguments.recipientAddress.type
)
)
}
@ -145,6 +145,15 @@ internal fun WrapSend(
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
}
// Clearing form if required form the previous navigation destination
if (sendArguments?.clearForm == true) {
setSendStage(SendStage.Form)
setZecSend(null)
setRecipientAddressState(RecipientAddressState.new("", null))
setAmountState(AmountState.new(context, "", monetarySeparators))
setMemoState(MemoState.new(""))
}
val onBackAction = {
when (sendStage) {
SendStage.Form -> goBack()

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.screen.send.model
data class SendArgumentsWrapper(
data class SendArguments(
val recipientAddress: RecipientAddressState? = null,
val clearForm: Boolean = false,
)

View File

@ -276,7 +276,7 @@ private fun SendForm(
onReferenceClick = goBalances
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
// TODO [#1256]: Consider Send.Form TextFields scrolling
// TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256

View File

@ -2,60 +2,92 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation
import android.content.Context
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationArgsWrapper
import co.electriccoin.zcash.ui.screen.sendconfirmation.ext.toSupportString
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationArguments
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import co.electriccoin.zcash.ui.screen.sendconfirmation.view.SendConfirmation
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.SendConfirmationViewModel
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
import co.electriccoin.zcash.ui.util.EmailUtil
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapSendConfirmation(
goBack: () -> Unit,
goBack: (clearForm: Boolean) -> Unit,
goHome: () -> Unit,
arguments: SendConfirmationArgsWrapper
arguments: SendConfirmationArguments
) {
val walletViewModel by this.viewModels<WalletViewModel>()
val walletViewModel by viewModels<WalletViewModel>()
val sendViewModel by viewModels<SendConfirmationViewModel>()
val viewModel by viewModels<SupportViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
val supportMessage = viewModel.supportInfo.collectAsStateWithLifecycle().value
WrapSendConfirmation(
arguments,
synchronizer,
spendingKey,
goBack,
goHome,
activity = this,
arguments = arguments,
goBack = goBack,
goHome = goHome,
sendViewModel = sendViewModel,
spendingKey = spendingKey,
supportMessage = supportMessage,
synchronizer = synchronizer,
)
}
@VisibleForTesting
@Composable
@Suppress("LongParameterList", "LongMethod")
internal fun WrapSendConfirmation(
arguments: SendConfirmationArgsWrapper,
synchronizer: Synchronizer?,
spendingKey: UnifiedSpendingKey?,
goBack: () -> Unit,
activity: ComponentActivity,
arguments: SendConfirmationArguments,
goBack: (clearForm: Boolean) -> Unit,
goHome: () -> Unit,
sendViewModel: SendConfirmationViewModel,
spendingKey: UnifiedSpendingKey?,
supportMessage: SupportInfo?,
synchronizer: Synchronizer?,
) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val zecSend by rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(arguments.toZecSend()) }
// Because of the [zecSend] has the same Saver as on the Send screen, we do not expect this to be ever null
@ -66,16 +98,15 @@ internal fun WrapSendConfirmation(
mutableStateOf(SendConfirmationStage.Confirmation)
}
val submissionResults = sendViewModel.submissions.collectAsState().value.toImmutableList()
val onBackAction = {
when (stage) {
SendConfirmationStage.Confirmation -> goBack()
SendConfirmationStage.Confirmation -> goBack(false)
SendConfirmationStage.Sending -> { /* no action - wait until the sending is done */ }
is SendConfirmationStage.Failure -> setStage(SendConfirmationStage.Confirmation)
is SendConfirmationStage.MultipleTrxFailure -> {
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294
setStage(SendConfirmationStage.Confirmation)
}
is SendConfirmationStage.MultipleTrxFailure -> { /* no action - wait until report the result */ }
is SendConfirmationStage.MultipleTrxFailureReported -> goBack(true)
}
}
@ -93,34 +124,82 @@ internal fun WrapSendConfirmation(
stage = stage,
onStageChange = setStage,
zecSend = zecSend!!,
submissionResults = submissionResults,
snackbarHostState = snackbarHostState,
onBack = onBackAction,
onContactSupport = {
val fullMessage =
formatMessage(
context = activity,
appInfo = supportMessage,
submissionResults = submissionResults
)
val mailIntent =
EmailUtil.newMailActivityIntent(
activity.getString(R.string.support_email_address),
activity.getString(R.string.app_name),
fullMessage
).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
runCatching {
activity.startActivity(mailIntent)
}.onSuccess {
setStage(SendConfirmationStage.MultipleTrxFailureReported)
}.onFailure {
setStage(SendConfirmationStage.MultipleTrxFailureReported)
scope.launch {
snackbarHostState.showSnackbar(
message = activity.getString(R.string.send_confirmation_multiple_report_unable_open_email)
)
}
}
},
onCreateAndSend = { newZecSend ->
scope.launch {
Twig.debug { "Sending transactions" }
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen
// TODO [#1294]: Note that the following processing is not entirely correct and will be reworked
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294
runCatching {
// The not-null assertion operator is necessary here even if we check its nullability before
// due to: "Smart cast to 'Proposal' is impossible, because 'zecSend.proposal' is a public API
// property declared in different module
// See more details on the Kotlin forum
checkNotNull(newZecSend.proposal)
synchronizer.createProposedTransactions(newZecSend.proposal!!, spendingKey).collect {
Twig.info { "Printing only for now. Will be reworked. Result: $it" }
}
}
.onSuccess {
Twig.debug { "Transaction submitted successfully" }
Twig.debug { "Sending transactions..." }
// The not-null assertion operator is necessary here even if we check its nullability before
// due to property is declared in different module. See more details on the Kotlin forum
checkNotNull(newZecSend.proposal)
val result =
sendViewModel.runSending(
synchronizer = synchronizer,
spendingKey = spendingKey,
proposal = newZecSend.proposal!!
)
when (result) {
SubmitResult.Success -> {
setStage(SendConfirmationStage.Confirmation)
goHome()
}
.onFailure {
Twig.error(it) { "Transaction submission failed" }
setStage(SendConfirmationStage.Failure(it.message ?: ""))
is SubmitResult.SimpleTrxFailure -> {
setStage(SendConfirmationStage.Failure(result.errorDescription))
}
is SubmitResult.MultipleTrxFailure -> {
setStage(SendConfirmationStage.MultipleTrxFailure)
}
}
}
}
)
}
}
private fun formatMessage(
context: Context,
appInfo: SupportInfo?,
supportInfoValues: Set<SupportInfoType> = SupportInfoType.entries.toSet(),
submissionResults: ImmutableList<TransactionSubmitResult>
): String =
buildString {
appendLine(context.getString(R.string.send_confirmation_multiple_report_text))
appendLine()
append(appInfo?.toSupportString(supportInfoValues) ?: "")
if (submissionResults.isNotEmpty()) {
append(submissionResults.toSupportString(context))
}
}

View File

@ -0,0 +1,45 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.ext
import android.content.Context
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import co.electriccoin.zcash.ui.R
fun List<TransactionSubmitResult>.toSupportString(context: Context): String {
return buildString {
appendLine(context.getString(R.string.send_confirmation_multiple_report_statuses))
this@toSupportString.forEachIndexed { index, result ->
when (result) {
is TransactionSubmitResult.Success -> {
appendLine(
context.getString(
R.string.send_confirmation_multiple_report_status_success,
index + 1
)
)
}
is TransactionSubmitResult.Failure -> {
appendLine(
context.getString(
R.string.send_confirmation_multiple_report_status_failure,
index + 1,
result.grpcError.toString(),
result.code,
result.description,
)
)
}
is TransactionSubmitResult.NotAttempted -> {
appendLine(
context.getString(
R.string.send_confirmation_multiple_report_status_not_attempt,
index + 1
)
)
}
}
}
}
}

View File

@ -9,7 +9,7 @@ import co.electriccoin.zcash.ui.NavigationArguments
import co.electriccoin.zcash.ui.common.model.SerializableAddress
import kotlinx.serialization.json.Json
data class SendConfirmationArgsWrapper(
data class SendConfirmationArguments(
val address: SerializableAddress?,
val amount: Long?,
val memo: String?,
@ -17,7 +17,7 @@ data class SendConfirmationArgsWrapper(
) {
companion object {
internal fun fromSavedStateHandle(savedStateHandle: SavedStateHandle) =
SendConfirmationArgsWrapper(
SendConfirmationArguments(
address =
savedStateHandle.get<String>(NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS)?.let {
Json.decodeFromString<SerializableAddress>(it)
@ -40,7 +40,7 @@ data class SendConfirmationArgsWrapper(
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SendConfirmationArgsWrapper
other as SendConfirmationArguments
if (amount != other.amount) return false
if (memo != other.memo) return false

View File

@ -9,13 +9,16 @@ sealed class SendConfirmationStage {
data class Failure(val error: String) : SendConfirmationStage()
data class MultipleTrxFailure(val error: String) : SendConfirmationStage()
data object MultipleTrxFailure : SendConfirmationStage()
data object MultipleTrxFailureReported : SendConfirmationStage()
companion object {
private const val TYPE_CONFIRMATION = "confirmation" // $NON-NLS
private const val TYPE_SENDING = "sending" // $NON-NLS
private const val TYPE_FAILURE = "failure" // $NON-NLS
private const val TYPE_MULTIPLE_TRX_FAILURE = "multiple_trx_failure" // $NON-NLS
private const val TYPE_MULTIPLE_TRX_FAILURE_REPORTED = "multiple_trx_failure_reported" // $NON-NLS
private const val KEY_TYPE = "type" // $NON-NLS
private const val KEY_ERROR = "error" // $NON-NLS
@ -33,7 +36,8 @@ sealed class SendConfirmationStage {
TYPE_CONFIRMATION -> Confirmation
TYPE_SENDING -> Sending
TYPE_FAILURE -> Failure((it[KEY_ERROR] as String))
TYPE_MULTIPLE_TRX_FAILURE -> MultipleTrxFailure((it[KEY_ERROR] as String))
TYPE_MULTIPLE_TRX_FAILURE -> MultipleTrxFailure
TYPE_MULTIPLE_TRX_FAILURE_REPORTED -> MultipleTrxFailureReported
else -> null
}
}
@ -50,10 +54,8 @@ sealed class SendConfirmationStage {
saverMap[KEY_TYPE] = TYPE_FAILURE
saverMap[KEY_ERROR] = this.error
}
is MultipleTrxFailure -> {
saverMap[KEY_TYPE] = TYPE_FAILURE
saverMap[KEY_ERROR] = this.error
}
is MultipleTrxFailure -> saverMap[KEY_TYPE] = TYPE_MULTIPLE_TRX_FAILURE
is MultipleTrxFailureReported -> saverMap[KEY_TYPE] = TYPE_MULTIPLE_TRX_FAILURE_REPORTED
}
return saverMap

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.model
sealed class SubmitResult {
data object Success : SubmitResult()
data class SimpleTrxFailure(val errorDescription: String) : SubmitResult()
data object MultipleTrxFailure : SubmitResult()
}

View File

@ -1,6 +1,10 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.sendconfirmation.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -11,17 +15,26 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.toZecString
@ -31,6 +44,7 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceWidgetBigLineOnly
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.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Small
@ -40,6 +54,8 @@ import co.electriccoin.zcash.ui.design.component.Tiny
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.sendconfirmation.SendConfirmationTag
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.runBlocking
@Composable
@ -76,26 +92,62 @@ private fun PreviewSendConfirmation() {
}
}
@Composable
@Preview("SendMultipleTransactionFailure")
private fun PreviewSendMultipleTransactionFailure() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
@Suppress("MagicNumber")
MultipleSubmissionFailure(
onContactSupport = {},
// Rework this into a test fixture
submissionResults =
persistentListOf(
TransactionSubmitResult.Failure(
FirstClassByteArray("test_transaction_id_1".toByteArray()),
true,
123,
"test transaction id failure"
),
TransactionSubmitResult.NotAttempted(
FirstClassByteArray("test_transaction_id_2".toByteArray())
),
TransactionSubmitResult.NotAttempted(
FirstClassByteArray("test_transaction_id_3".toByteArray())
)
)
)
}
}
}
// TODO [#1260]: Cover Send screens UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
@Composable
@Suppress("LongParameterList")
fun SendConfirmation(
stage: SendConfirmationStage,
onStageChange: (SendConfirmationStage) -> Unit,
zecSend: ZecSend,
onBack: () -> Unit,
onContactSupport: () -> Unit,
onCreateAndSend: (ZecSend) -> Unit,
onStageChange: (SendConfirmationStage) -> Unit,
snackbarHostState: SnackbarHostState,
stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend,
) {
Scaffold(topBar = {
SendConfirmationTopAppBar()
}) { paddingValues ->
Scaffold(
topBar = { SendConfirmationTopAppBar(onBack, stage) },
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
SendConfirmationMainContent(
onBack = onBack,
stage = stage,
onStageChange = onStageChange,
zecSend = zecSend,
onContactSupport = onContactSupport,
onSendSubmit = onCreateAndSend,
onStageChange = onStageChange,
stage = stage,
submissionResults = submissionResults,
zecSend = zecSend,
modifier =
Modifier
.padding(
@ -109,20 +161,43 @@ fun SendConfirmation(
}
@Composable
private fun SendConfirmationTopAppBar() {
SmallTopAppBar(
titleText = stringResource(id = R.string.send_stage_confirmation_title)
)
private fun SendConfirmationTopAppBar(
onBack: () -> Unit,
stage: SendConfirmationStage
) {
when (stage) {
SendConfirmationStage.Confirmation,
SendConfirmationStage.Sending,
is SendConfirmationStage.Failure -> {
SmallTopAppBar(titleText = stringResource(id = R.string.send_stage_confirmation_title))
}
SendConfirmationStage.MultipleTrxFailure -> {
SmallTopAppBar(titleText = stringResource(id = R.string.send_confirmation_multiple_error_title))
}
SendConfirmationStage.MultipleTrxFailureReported -> {
SmallTopAppBar(
titleText = stringResource(id = R.string.send_confirmation_multiple_error_title),
backText = stringResource(id = R.string.send_confirmation_multiple_error_back),
backContentDescriptionText =
stringResource(
id = R.string.send_confirmation_multiple_error_back_content_description
),
onBack = onBack
)
}
}
}
@Composable
@Suppress("LongParameterList")
private fun SendConfirmationMainContent(
onBack: () -> Unit,
zecSend: ZecSend,
stage: SendConfirmationStage,
onStageChange: (SendConfirmationStage) -> Unit,
onContactSupport: () -> Unit,
onSendSubmit: (ZecSend) -> Unit,
onStageChange: (SendConfirmationStage) -> Unit,
stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend,
modifier: Modifier = Modifier,
) {
when (stage) {
@ -144,12 +219,11 @@ private fun SendConfirmationMainContent(
)
}
}
is SendConfirmationStage.MultipleTrxFailure -> {
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294
SendFailure(
onDone = onBack,
reason = stage.error,
is SendConfirmationStage.MultipleTrxFailure, SendConfirmationStage.MultipleTrxFailureReported -> {
MultipleSubmissionFailure(
onContactSupport = onContactSupport,
submissionResults = submissionResults,
modifier = modifier
)
}
}
@ -187,7 +261,7 @@ private fun SendConfirmationContent(
Tiny(zecSend.destination.address)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Small(stringResource(R.string.send_confirmation_fee))
@ -212,7 +286,7 @@ private fun SendConfirmationContent(
)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
if (zecSend.memo.value.isNotEmpty()) {
Small(stringResource(R.string.send_confirmation_memo))
@ -234,7 +308,7 @@ private fun SendConfirmationContent(
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
}
Spacer(
@ -308,3 +382,98 @@ private fun SendFailure(
onConfirmButtonClick = onDone
)
}
@Composable
fun MultipleSubmissionFailure(
onContactSupport: () -> Unit,
submissionResults: ImmutableList<TransactionSubmitResult>,
modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Box(
contentAlignment = Alignment.BottomEnd
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.zashi_logo_sign),
contentDescription = null,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_alert_circle_fill),
contentDescription = null,
modifier = Modifier.padding(bottom = ZcashTheme.dimens.spacingMid)
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))
Body(
text = stringResource(id = R.string.send_confirmation_multiple_error_text_1),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Body(
text = stringResource(id = R.string.send_confirmation_multiple_error_text_2),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))
if (submissionResults.isNotEmpty()) {
TransactionSubmitResultWidget(submissionResults)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Spacer(modifier = Modifier.weight(1f, true))
PrimaryButton(
onClick = onContactSupport,
text = stringResource(id = R.string.send_confirmation_multiple_error_btn)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
}
}
@Composable
fun TransactionSubmitResultWidget(
submissionResults: ImmutableList<TransactionSubmitResult>,
modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Small(text = stringResource(id = R.string.send_confirmation_multiple_error_trx_title))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
submissionResults.forEachIndexed { index, item ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Small(
text =
stringResource(
id = R.string.send_confirmation_multiple_error_trx_item,
index + 1
),
modifier = Modifier.wrapContentSize()
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
Small(text = item.txIdString())
}
}
}
}

View File

@ -0,0 +1,61 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import kotlinx.coroutines.flow.MutableStateFlow
class SendConfirmationViewModel(application: Application) : AndroidViewModel(application) {
// Technically this value will not survive process dead, but will survive all possible configuration changes
// Possible solution would be storing the value within [SavedStateHandle]
val submissions: MutableStateFlow<List<TransactionSubmitResult>> = MutableStateFlow(emptyList())
suspend fun runSending(
synchronizer: Synchronizer,
spendingKey: UnifiedSpendingKey,
proposal: Proposal
): SubmitResult {
val submitResults = mutableListOf<TransactionSubmitResult>()
return runCatching {
synchronizer.createProposedTransactions(
proposal = proposal,
usk = spendingKey
).collect { submitResult ->
Twig.info { "Transaction submit result: $submitResult" }
submitResults.add(submitResult)
}
if (submitResults.find { it is TransactionSubmitResult.Failure } != null) {
if (submitResults.size == 1) {
// The first transaction submission failed - user might just be able to re-submit the transaction
// proposal. Simple error pop up is fine then
SubmitResult.SimpleTrxFailure(
(submitResults[0] as TransactionSubmitResult.Failure).description ?: ""
)
} else {
// Any subsequent transaction submission failed - user needs to resolve this manually. Multiple
// transaction failure screen presented
SubmitResult.MultipleTrxFailure
}
}
// All transaction submissions were successful
SubmitResult.Success
}.onSuccess {
Twig.debug { "Transactions submitted successfully" }
}.onFailure {
Twig.error(it) { "Transactions submission failed" }
}.getOrElse {
SubmitResult.SimpleTrxFailure(it.message ?: "")
}.also {
// Save the submission results for the later MultipleSubmissionError screen
if (it == SubmitResult.MultipleTrxFailure) {
submissions.value = submitResults
}
}
}
}

View File

@ -16,9 +16,9 @@ import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType
import co.electriccoin.zcash.ui.screen.support.util.EmailUtil
import co.electriccoin.zcash.ui.screen.support.view.Support
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
import co.electriccoin.zcash.ui.util.EmailUtil
import kotlinx.coroutines.launch
@Composable
@ -55,14 +55,12 @@ internal fun WrapSupport(
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
// TODO [#386]: This should only fail if there's no email app, e.g. on a TV device
// TODO [#386]: https://github.com/Electric-Coin-Company/zashi-android/issues/386
runCatching {
activity.startActivity(mailIntent)
}.onSuccess {
setShowDialog(false)
}.onFailure {
setShowDialog(false)
scope.launch {
snackbarHostState.showSnackbar(
message = activity.getString(R.string.support_unable_to_open_email)

View File

@ -7,7 +7,8 @@ import co.electriccoin.zcash.spackle.versionCodeCompat
data class AppInfo(val versionName: String, val versionCode: Long, val gitSha: String) {
fun toSupportString() =
buildString {
appendLine("App version: $versionName ($versionCode) $gitSha")
// [versionName] contains [versionCode]
appendLine("App version: $versionName $gitSha")
}
companion object {

View File

@ -3,6 +3,9 @@ package co.electriccoin.zcash.ui.screen.support.model
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import kotlinx.datetime.Instant
// TODO [#1301]: Localize support text content
// TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class ConfigInfo(val configurationUpdatedAt: Instant?) {
fun toSupportString() =
buildString {

View File

@ -9,6 +9,9 @@ import co.electriccoin.zcash.spackle.io.listFilesSuspend
import kotlinx.datetime.Instant
import java.io.File
// TODO [#1301]: Localize support text content
// TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, val timestamp: Instant) {
fun toSupportString() =
buildString {
@ -24,11 +27,15 @@ data class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, va
}
fun List<CrashInfo>.toCrashSupportString() =
buildString {
// Using the header "Exceptions" instead of "Crashes" to reduce risk of alarming users
appendLine("Exceptions:")
this@toCrashSupportString.forEach {
appendLine(it.toSupportString())
if (isEmpty()) {
""
} else {
buildString {
// Using the header "Exceptions" instead of "Crashes" to reduce risk of alarming users
appendLine("Exceptions:")
this@toCrashSupportString.forEach {
appendLine(it.toSupportString())
}
}
}

View File

@ -2,6 +2,9 @@ package co.electriccoin.zcash.ui.screen.support.model
import android.os.Build
// TODO [#1301]: Localize support text content
// TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class DeviceInfo(val manufacturer: String, val device: String, val model: String) {
fun toSupportString() =
buildString {

View File

@ -5,6 +5,9 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
import co.electriccoin.zcash.global.StorageChecker
import java.util.Locale
// TODO [#1301]: Localize support text content
// TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class EnvironmentInfo(
val locale: Locale,
val monetarySeparators: MonetarySeparators,

View File

@ -3,6 +3,9 @@ package co.electriccoin.zcash.ui.screen.support.model
import android.os.Build
import co.electriccoin.zcash.spackle.AndroidApiVersion
// TODO [#1301]: Localize support text content
// TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class OperatingSystemInfo(val sdkInt: Int, val isPreview: Boolean) {
fun toSupportString() =
buildString {

View File

@ -6,6 +6,9 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import co.electriccoin.zcash.spackle.getPackageInfoCompatSuspend
// TODO [#1301]: Localize support text content
// TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class PermissionInfo(val permissionName: String, val permissionStatus: PermissionStatus) {
fun toSupportString() =
buildString {

View File

@ -9,6 +9,9 @@ import java.util.Date
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds
// TODO [#1301]: Localize support text content
// TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class TimeInfo(
val currentTime: Instant,
val rebootTime: Instant,

View File

@ -58,11 +58,11 @@ fun NotEnoughSpaceView(
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Header(text = stringResource(id = R.string.not_enough_space_title))
Spacer(Modifier.height(ZcashTheme.dimens.spacingXlarge))
Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Body(
text = stringResource(id = R.string.not_enough_space_description, storageSpaceRequiredGigabytes),

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.support.util
package co.electriccoin.zcash.ui.util
import android.content.Intent
import android.net.Uri

View File

@ -5,4 +5,6 @@
<string name="not_implemented_yet">Not implemented yet.</string>
<string name="settings_menu_content_description">Open Settings</string>
<string name="balance_widget_available">Available Balance:</string>
<!-- This is replaced by a resource overlay via app/build.gradle.kts -->
<string name="support_email_address" />
</resources>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="25dp"
android:viewportWidth="25"
android:viewportHeight="25">
<path
android:pathData="M12.359,22.587C6.836,22.587 2.359,18.11 2.359,12.587C2.359,7.065 6.836,2.587 12.359,2.587C17.882,2.587 22.359,7.065 22.359,12.587C22.359,18.11 17.882,22.587 12.359,22.587ZM12.359,7.587C12.094,7.587 11.839,7.692 11.652,7.88C11.464,8.067 11.359,8.322 11.359,8.587V13.587C11.359,13.852 11.464,14.106 11.652,14.294C11.839,14.481 12.094,14.587 12.359,14.587C12.624,14.587 12.878,14.481 13.066,14.294C13.253,14.106 13.359,13.852 13.359,13.587V8.587C13.359,8.322 13.253,8.067 13.066,7.88C12.878,7.692 12.624,7.587 12.359,7.587ZM12.359,17.587C12.624,17.587 12.878,17.481 13.066,17.294C13.253,17.106 13.359,16.852 13.359,16.587C13.359,16.322 13.253,16.067 13.066,15.88C12.878,15.692 12.624,15.587 12.359,15.587C12.094,15.587 11.839,15.692 11.652,15.88C11.464,16.067 11.359,16.322 11.359,16.587C11.359,16.852 11.464,17.106 11.652,17.294C11.839,17.481 12.094,17.587 12.359,17.587Z"
android:fillColor="#21272A"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="92dp"
android:height="118dp"
android:viewportWidth="92"
android:viewportHeight="118">
<path
android:pathData="M50.11,86.3L64.11,114.47L27.61,98.12L89.79,65.69L84.77,47.89L6.21,5.29L58.15,71.12L84.77,47.89L27.61,98.12"
android:strokeWidth="3"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
</vector>

View File

@ -12,4 +12,36 @@
<string name="send_confirmation_dialog_error_text">Error: The attempt to send funds failed. Try it again, please.</string>
<string name="send_confirmation_dialog_error_btn">OK</string>
<string name="send_confirmation_multiple_error_title">Transaction error</string>
<string name="send_confirmation_multiple_error_back">Back</string>
<string name="send_confirmation_multiple_error_back_content_description">Back</string>
<string name="send_confirmation_multiple_error_text_1">Sending to this recipient required multiple transactions,
but only some of them succeeded. Your funds are safe, but they need to be recovered with the help of Zashi
team support.</string>
<string name="send_confirmation_multiple_error_text_2">Please use the button below to contact us and recover your
funds. Your message to us will be pre-populated with all the data we need to resolve this issue.</string>
<string name="send_confirmation_multiple_error_trx_title">TRANSACTION IDs:</string>
<string name="send_confirmation_multiple_error_trx_item" formatted="true">
<xliff:g id="order" example="1">%1$d. </xliff:g>ID:
</string>
<string name="send_confirmation_multiple_error_btn">Contact Support</string>
<string name="send_confirmation_multiple_report_text">Hi, Zashi Team.\n\nWhile sending a transaction to a TEX
address, I encountered an error state. I\'m reaching out to get guidance on how to recover my funds.\n\nThank
you.</string>
<string name="send_confirmation_multiple_report_statuses" formatted="true">Transaction statuses:</string>
<string name="send_confirmation_multiple_report_status_success">
<xliff:g example="1" id="index">%1$d</xliff:g>: Success
</string>
<string name="send_confirmation_multiple_report_status_not_attempt" formatted="true">
<xliff:g example="2" id="index">%1$d</xliff:g>: Not attempt
</string>
<string name="send_confirmation_multiple_report_status_failure" formatted="true">
<xliff:g example="3" id="index">%1$d</xliff:g>: Failure:
gRPC: <xliff:g example="true" id="grpc">%2$s</xliff:g>,
code: <xliff:g example="123" id="code">%3$d</xliff:g>,
description: <xliff:g example="Network unreachable" id="desc">%4$s</xliff:g>
</string>
<string name="send_confirmation_multiple_report_unable_open_email">Unable to launch email app.</string>
</resources>

View File

@ -9,8 +9,6 @@
<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>
<string name="support_disclaimer">Information provided is handled in accordance with our Privacy Policy.</string>
<string name="support_unable_to_open_email">Unable to launch email app.</string>