[#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 ### Added
- Advanced Settings screen that provides more technical options like Export private data, Recovery phrase, or - Advanced Settings screen that provides more technical options like Export private data, Recovery phrase, or
Choose server Choose server has been added
- A new Server switching screen was added. Its purpose is to enable switching between predefined and custom - A new Server switching screen has been added. Its purpose is to enable switching between predefined and custom
lightwalletd servers in runtime. lightwalletd servers in runtime.
- The About screen now contains a link to the new Zashi Privacy Policy website - 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 - The Send Confirmation screen has been reworked according to the new design
- Transitions between screens are now animated with a simple slide animation - 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 ### Changed
- The Transaction History UI has been incorporated into the Account screen - 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 # 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. 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 spacingMid: Dp,
val spacingDefault: Dp, val spacingDefault: Dp,
val spacingLarge: Dp, val spacingLarge: Dp,
val spacingXlarge: Dp, val spacingUpLarge: Dp,
val spacingBig: Dp,
val spacingHuge: Dp, val spacingHuge: Dp,
// List of custom spacings: // List of custom spacings:
// Button: // Button:
@ -62,7 +63,8 @@ private val defaultDimens =
spacingMid = 12.dp, spacingMid = 12.dp,
spacingDefault = 16.dp, spacingDefault = 16.dp,
spacingLarge = 24.dp, spacingLarge = 24.dp,
spacingXlarge = 32.dp, spacingUpLarge = 32.dp,
spacingBig = 48.dp,
spacingHuge = 64.dp, spacingHuge = 64.dp,
buttonShadowOffsetX = 20.dp, buttonShadowOffsetX = 20.dp,
buttonShadowOffsetY = 20.dp, buttonShadowOffsetY = 20.dp,

View File

@ -104,8 +104,8 @@ internal val SecondaryTypography =
headlineSmall = headlineSmall =
TextStyle( TextStyle(
fontFamily = ArchivoFontFamily, fontFamily = ArchivoFontFamily,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Bold,
fontSize = 18.sp, fontSize = 16.sp,
textAlign = TextAlign.Center textAlign = TextAlign.Center
), ),
bodyLarge = 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.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.common.model.SerializableAddress 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 { internal object SendArgumentsWrapperFixture {
val RECIPIENT_ADDRESS = val RECIPIENT_ADDRESS =
@ -14,7 +14,7 @@ internal object SendArgumentsWrapperFixture {
) )
fun new(recipientAddress: SerializableAddress? = RECIPIENT_ADDRESS) = fun new(recipientAddress: SerializableAddress? = RECIPIENT_ADDRESS) =
SendArgumentsWrapper( SendArguments(
recipientAddress = recipientAddress?.toRecipient(), recipientAddress = recipientAddress?.toRecipient(),
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -205,7 +205,7 @@ private fun ChooseServerMainContent(
}, },
setShowErrorDialog = setShowErrorDialog, setShowErrorDialog = setShowErrorDialog,
selectedOption = selectedOption, selectedOption = selectedOption,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingXlarge) modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingUpLarge)
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge)) 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.home.view.Home
import co.electriccoin.zcash.ui.screen.receive.WrapReceive import co.electriccoin.zcash.ui.screen.receive.WrapReceive
import co.electriccoin.zcash.ui.screen.send.WrapSend 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.collections.immutable.persistentListOf
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -31,7 +31,7 @@ internal fun MainActivity.WrapHome(
goSettings: () -> Unit, goSettings: () -> Unit,
goScan: () -> Unit, goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
sendArgumentsWrapper: SendArgumentsWrapper sendArguments: SendArguments
) { ) {
WrapHome( WrapHome(
this, this,
@ -40,7 +40,7 @@ internal fun MainActivity.WrapHome(
goScan = goScan, goScan = goScan,
goSendConfirmation = goSendConfirmation, goSendConfirmation = goSendConfirmation,
goSettings = goSettings, goSettings = goSettings,
sendArgumentsWrapper = sendArgumentsWrapper sendArguments = sendArguments
) )
} }
@ -53,7 +53,7 @@ internal fun WrapHome(
goScan: () -> Unit, goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
onPageChange: (HomeScreenIndex) -> Unit, onPageChange: (HomeScreenIndex) -> Unit,
sendArgumentsWrapper: SendArgumentsWrapper sendArguments: SendArguments
) { ) {
val homeViewModel by activity.viewModels<HomeViewModel>() val homeViewModel by activity.viewModels<HomeViewModel>()
@ -105,7 +105,7 @@ internal fun WrapHome(
goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) }, goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) },
goSendConfirmation = goSendConfirmation, goSendConfirmation = goSendConfirmation,
goSettings = goSettings, 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.AmountState
import co.electriccoin.zcash.ui.screen.send.model.MemoState 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.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.model.SendStage
import co.electriccoin.zcash.ui.screen.send.view.Send import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -41,7 +41,7 @@ import java.util.Locale
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal fun WrapSend( internal fun WrapSend(
activity: ComponentActivity, activity: ComponentActivity,
sendArgumentsWrapper: SendArgumentsWrapper?, sendArguments: SendArguments?,
goToQrScanner: () -> Unit, goToQrScanner: () -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
goBalances: () -> Unit, goBalances: () -> Unit,
@ -72,7 +72,7 @@ internal fun WrapSend(
val monetarySeparators = MonetarySeparators.current(Locale.US) val monetarySeparators = MonetarySeparators.current(Locale.US)
WrapSend( WrapSend(
sendArgumentsWrapper, sendArguments,
synchronizer, synchronizer,
walletSnapshot, walletSnapshot,
spendingKey, spendingKey,
@ -91,7 +91,7 @@ internal fun WrapSend(
@VisibleForTesting @VisibleForTesting
@Composable @Composable
internal fun WrapSend( internal fun WrapSend(
sendArgumentsWrapper: SendArgumentsWrapper?, sendArguments: SendArguments?,
synchronizer: Synchronizer?, synchronizer: Synchronizer?,
walletSnapshot: WalletSnapshot?, walletSnapshot: WalletSnapshot?,
spendingKey: UnifiedSpendingKey?, spendingKey: UnifiedSpendingKey?,
@ -116,13 +116,13 @@ internal fun WrapSend(
// Address computation: // Address computation:
val (recipientAddressState, setRecipientAddressState) = val (recipientAddressState, setRecipientAddressState) =
rememberSaveable(stateSaver = RecipientAddressState.Saver) { 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( setRecipientAddressState(
RecipientAddressState.new( RecipientAddressState.new(
sendArgumentsWrapper.recipientAddress.address, sendArguments.recipientAddress.address,
sendArgumentsWrapper.recipientAddress.type sendArguments.recipientAddress.type
) )
) )
} }
@ -145,6 +145,15 @@ internal fun WrapSend(
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: "")) 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 = { val onBackAction = {
when (sendStage) { when (sendStage) {
SendStage.Form -> goBack() SendStage.Form -> goBack()

View File

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

View File

@ -276,7 +276,7 @@ private fun SendForm(
onReferenceClick = goBalances 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]: Consider Send.Form TextFields scrolling
// TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256 // 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 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.compose.BackHandler
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer 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.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity 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.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.send.ext.Saver 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.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.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 import kotlinx.coroutines.launch
@Composable @Composable
internal fun MainActivity.WrapSendConfirmation( internal fun MainActivity.WrapSendConfirmation(
goBack: () -> Unit, goBack: (clearForm: Boolean) -> Unit,
goHome: () -> 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 synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
val supportMessage = viewModel.supportInfo.collectAsStateWithLifecycle().value
WrapSendConfirmation( WrapSendConfirmation(
arguments, activity = this,
synchronizer, arguments = arguments,
spendingKey, goBack = goBack,
goBack, goHome = goHome,
goHome, sendViewModel = sendViewModel,
spendingKey = spendingKey,
supportMessage = supportMessage,
synchronizer = synchronizer,
) )
} }
@VisibleForTesting @VisibleForTesting
@Composable @Composable
@Suppress("LongParameterList", "LongMethod")
internal fun WrapSendConfirmation( internal fun WrapSendConfirmation(
arguments: SendConfirmationArgsWrapper, activity: ComponentActivity,
synchronizer: Synchronizer?, arguments: SendConfirmationArguments,
spendingKey: UnifiedSpendingKey?, goBack: (clearForm: Boolean) -> Unit,
goBack: () -> Unit,
goHome: () -> Unit, goHome: () -> Unit,
sendViewModel: SendConfirmationViewModel,
spendingKey: UnifiedSpendingKey?,
supportMessage: SupportInfo?,
synchronizer: Synchronizer?,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val zecSend by rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(arguments.toZecSend()) } 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 // 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) mutableStateOf(SendConfirmationStage.Confirmation)
} }
val submissionResults = sendViewModel.submissions.collectAsState().value.toImmutableList()
val onBackAction = { val onBackAction = {
when (stage) { when (stage) {
SendConfirmationStage.Confirmation -> goBack() SendConfirmationStage.Confirmation -> goBack(false)
SendConfirmationStage.Sending -> { /* no action - wait until the sending is done */ } SendConfirmationStage.Sending -> { /* no action - wait until the sending is done */ }
is SendConfirmationStage.Failure -> setStage(SendConfirmationStage.Confirmation) is SendConfirmationStage.Failure -> setStage(SendConfirmationStage.Confirmation)
is SendConfirmationStage.MultipleTrxFailure -> { is SendConfirmationStage.MultipleTrxFailure -> { /* no action - wait until report the result */ }
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen is SendConfirmationStage.MultipleTrxFailureReported -> goBack(true)
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294
setStage(SendConfirmationStage.Confirmation)
}
} }
} }
@ -93,34 +124,82 @@ internal fun WrapSendConfirmation(
stage = stage, stage = stage,
onStageChange = setStage, onStageChange = setStage,
zecSend = zecSend!!, zecSend = zecSend!!,
submissionResults = submissionResults,
snackbarHostState = snackbarHostState,
onBack = onBackAction, 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 -> onCreateAndSend = { newZecSend ->
scope.launch { scope.launch {
Twig.debug { "Sending transactions" } 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 // The not-null assertion operator is necessary here even if we check its nullability before
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294 // due to property is declared in different module. See more details on the Kotlin forum
runCatching { checkNotNull(newZecSend.proposal)
// 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 val result =
// property declared in different module sendViewModel.runSending(
// See more details on the Kotlin forum synchronizer = synchronizer,
checkNotNull(newZecSend.proposal) spendingKey = spendingKey,
synchronizer.createProposedTransactions(newZecSend.proposal!!, spendingKey).collect { proposal = newZecSend.proposal!!
Twig.info { "Printing only for now. Will be reworked. Result: $it" } )
} when (result) {
} SubmitResult.Success -> {
.onSuccess {
Twig.debug { "Transaction submitted successfully" }
setStage(SendConfirmationStage.Confirmation) setStage(SendConfirmationStage.Confirmation)
goHome() goHome()
} }
.onFailure { is SubmitResult.SimpleTrxFailure -> {
Twig.error(it) { "Transaction submission failed" } setStage(SendConfirmationStage.Failure(result.errorDescription))
setStage(SendConfirmationStage.Failure(it.message ?: ""))
} }
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 co.electriccoin.zcash.ui.common.model.SerializableAddress
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
data class SendConfirmationArgsWrapper( data class SendConfirmationArguments(
val address: SerializableAddress?, val address: SerializableAddress?,
val amount: Long?, val amount: Long?,
val memo: String?, val memo: String?,
@ -17,7 +17,7 @@ data class SendConfirmationArgsWrapper(
) { ) {
companion object { companion object {
internal fun fromSavedStateHandle(savedStateHandle: SavedStateHandle) = internal fun fromSavedStateHandle(savedStateHandle: SavedStateHandle) =
SendConfirmationArgsWrapper( SendConfirmationArguments(
address = address =
savedStateHandle.get<String>(NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS)?.let { savedStateHandle.get<String>(NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS)?.let {
Json.decodeFromString<SerializableAddress>(it) Json.decodeFromString<SerializableAddress>(it)
@ -40,7 +40,7 @@ data class SendConfirmationArgsWrapper(
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as SendConfirmationArgsWrapper other as SendConfirmationArguments
if (amount != other.amount) return false if (amount != other.amount) return false
if (memo != other.memo) 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 Failure(val error: String) : SendConfirmationStage()
data class MultipleTrxFailure(val error: String) : SendConfirmationStage() data object MultipleTrxFailure : SendConfirmationStage()
data object MultipleTrxFailureReported : SendConfirmationStage()
companion object { companion object {
private const val TYPE_CONFIRMATION = "confirmation" // $NON-NLS private const val TYPE_CONFIRMATION = "confirmation" // $NON-NLS
private const val TYPE_SENDING = "sending" // $NON-NLS private const val TYPE_SENDING = "sending" // $NON-NLS
private const val TYPE_FAILURE = "failure" // $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 = "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_TYPE = "type" // $NON-NLS
private const val KEY_ERROR = "error" // $NON-NLS private const val KEY_ERROR = "error" // $NON-NLS
@ -33,7 +36,8 @@ sealed class SendConfirmationStage {
TYPE_CONFIRMATION -> Confirmation TYPE_CONFIRMATION -> Confirmation
TYPE_SENDING -> Sending TYPE_SENDING -> Sending
TYPE_FAILURE -> Failure((it[KEY_ERROR] as String)) 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 else -> null
} }
} }
@ -50,10 +54,8 @@ sealed class SendConfirmationStage {
saverMap[KEY_TYPE] = TYPE_FAILURE saverMap[KEY_TYPE] = TYPE_FAILURE
saverMap[KEY_ERROR] = this.error saverMap[KEY_ERROR] = this.error
} }
is MultipleTrxFailure -> { is MultipleTrxFailure -> saverMap[KEY_TYPE] = TYPE_MULTIPLE_TRX_FAILURE
saverMap[KEY_TYPE] = TYPE_FAILURE is MultipleTrxFailureReported -> saverMap[KEY_TYPE] = TYPE_MULTIPLE_TRX_FAILURE_REPORTED
saverMap[KEY_ERROR] = this.error
}
} }
return saverMap 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 package co.electriccoin.zcash.ui.screen.sendconfirmation.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture 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.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.toZecString 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.common.compose.BalanceWidgetBigLineOnly
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.AppAlertDialog 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.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Small 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.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.sendconfirmation.SendConfirmationTag import co.electriccoin.zcash.ui.screen.sendconfirmation.SendConfirmationTag
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@Composable @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]: Cover Send screens UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260 // TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
@Composable @Composable
@Suppress("LongParameterList")
fun SendConfirmation( fun SendConfirmation(
stage: SendConfirmationStage,
onStageChange: (SendConfirmationStage) -> Unit,
zecSend: ZecSend,
onBack: () -> Unit, onBack: () -> Unit,
onContactSupport: () -> Unit,
onCreateAndSend: (ZecSend) -> Unit, onCreateAndSend: (ZecSend) -> Unit,
onStageChange: (SendConfirmationStage) -> Unit,
snackbarHostState: SnackbarHostState,
stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend,
) { ) {
Scaffold(topBar = { Scaffold(
SendConfirmationTopAppBar() topBar = { SendConfirmationTopAppBar(onBack, stage) },
}) { paddingValues -> snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
SendConfirmationMainContent( SendConfirmationMainContent(
onBack = onBack, onBack = onBack,
stage = stage, onContactSupport = onContactSupport,
onStageChange = onStageChange,
zecSend = zecSend,
onSendSubmit = onCreateAndSend, onSendSubmit = onCreateAndSend,
onStageChange = onStageChange,
stage = stage,
submissionResults = submissionResults,
zecSend = zecSend,
modifier = modifier =
Modifier Modifier
.padding( .padding(
@ -109,20 +161,43 @@ fun SendConfirmation(
} }
@Composable @Composable
private fun SendConfirmationTopAppBar() { private fun SendConfirmationTopAppBar(
SmallTopAppBar( onBack: () -> Unit,
titleText = stringResource(id = R.string.send_stage_confirmation_title) 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 @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun SendConfirmationMainContent( private fun SendConfirmationMainContent(
onBack: () -> Unit, onBack: () -> Unit,
zecSend: ZecSend, onContactSupport: () -> Unit,
stage: SendConfirmationStage,
onStageChange: (SendConfirmationStage) -> Unit,
onSendSubmit: (ZecSend) -> Unit, onSendSubmit: (ZecSend) -> Unit,
onStageChange: (SendConfirmationStage) -> Unit,
stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (stage) { when (stage) {
@ -144,12 +219,11 @@ private fun SendConfirmationMainContent(
) )
} }
} }
is SendConfirmationStage.MultipleTrxFailure -> { is SendConfirmationStage.MultipleTrxFailure, SendConfirmationStage.MultipleTrxFailureReported -> {
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen MultipleSubmissionFailure(
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294 onContactSupport = onContactSupport,
SendFailure( submissionResults = submissionResults,
onDone = onBack, modifier = modifier
reason = stage.error,
) )
} }
} }
@ -187,7 +261,7 @@ private fun SendConfirmationContent(
Tiny(zecSend.destination.address) 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)) 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()) { if (zecSend.memo.value.isNotEmpty()) {
Small(stringResource(R.string.send_confirmation_memo)) 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( Spacer(
@ -308,3 +382,98 @@ private fun SendFailure(
onConfirmButtonClick = onDone 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.R
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo 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.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.view.Support
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
import co.electriccoin.zcash.ui.util.EmailUtil
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -55,14 +55,12 @@ internal fun WrapSupport(
flags = Intent.FLAG_ACTIVITY_NEW_TASK 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 { runCatching {
activity.startActivity(mailIntent) activity.startActivity(mailIntent)
}.onSuccess { }.onSuccess {
setShowDialog(false) setShowDialog(false)
}.onFailure { }.onFailure {
setShowDialog(false)
scope.launch { scope.launch {
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
message = activity.getString(R.string.support_unable_to_open_email) 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) { data class AppInfo(val versionName: String, val versionCode: Long, val gitSha: String) {
fun toSupportString() = fun toSupportString() =
buildString { buildString {
appendLine("App version: $versionName ($versionCode) $gitSha") // [versionName] contains [versionCode]
appendLine("App version: $versionName $gitSha")
} }
companion object { companion object {

View File

@ -3,6 +3,9 @@ package co.electriccoin.zcash.ui.screen.support.model
import co.electriccoin.zcash.configuration.api.ConfigurationProvider import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import kotlinx.datetime.Instant 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?) { data class ConfigInfo(val configurationUpdatedAt: Instant?) {
fun toSupportString() = fun toSupportString() =
buildString { buildString {

View File

@ -9,6 +9,9 @@ import co.electriccoin.zcash.spackle.io.listFilesSuspend
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import java.io.File 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) { data class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, val timestamp: Instant) {
fun toSupportString() = fun toSupportString() =
buildString { buildString {
@ -24,11 +27,15 @@ data class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, va
} }
fun List<CrashInfo>.toCrashSupportString() = fun List<CrashInfo>.toCrashSupportString() =
buildString { if (isEmpty()) {
// Using the header "Exceptions" instead of "Crashes" to reduce risk of alarming users ""
appendLine("Exceptions:") } else {
this@toCrashSupportString.forEach { buildString {
appendLine(it.toSupportString()) // 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 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) { data class DeviceInfo(val manufacturer: String, val device: String, val model: String) {
fun toSupportString() = fun toSupportString() =
buildString { buildString {

View File

@ -5,6 +5,9 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
import co.electriccoin.zcash.global.StorageChecker import co.electriccoin.zcash.global.StorageChecker
import java.util.Locale 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( data class EnvironmentInfo(
val locale: Locale, val locale: Locale,
val monetarySeparators: MonetarySeparators, val monetarySeparators: MonetarySeparators,

View File

@ -3,6 +3,9 @@ package co.electriccoin.zcash.ui.screen.support.model
import android.os.Build import android.os.Build
import co.electriccoin.zcash.spackle.AndroidApiVersion 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) { data class OperatingSystemInfo(val sdkInt: Int, val isPreview: Boolean) {
fun toSupportString() = fun toSupportString() =
buildString { buildString {

View File

@ -6,6 +6,9 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import co.electriccoin.zcash.spackle.getPackageInfoCompatSuspend 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) { data class PermissionInfo(val permissionName: String, val permissionStatus: PermissionStatus) {
fun toSupportString() = fun toSupportString() =
buildString { buildString {

View File

@ -9,6 +9,9 @@ import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds 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( data class TimeInfo(
val currentTime: Instant, val currentTime: Instant,
val rebootTime: Instant, val rebootTime: Instant,

View File

@ -58,11 +58,11 @@ fun NotEnoughSpaceView(
modifier = Modifier.fillMaxWidth() 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)) Header(text = stringResource(id = R.string.not_enough_space_title))
Spacer(Modifier.height(ZcashTheme.dimens.spacingXlarge)) Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Body( Body(
text = stringResource(id = R.string.not_enough_space_description, storageSpaceRequiredGigabytes), 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.content.Intent
import android.net.Uri import android.net.Uri

View File

@ -5,4 +5,6 @@
<string name="not_implemented_yet">Not implemented yet.</string> <string name="not_implemented_yet">Not implemented yet.</string>
<string name="settings_menu_content_description">Open Settings</string> <string name="settings_menu_content_description">Open Settings</string>
<string name="balance_widget_available">Available Balance:</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> </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_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_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> </resources>

View File

@ -9,8 +9,6 @@
<string name="support_confirmation_dialog_title">Open e-mail 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 <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> 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_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_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> <string name="support_unable_to_open_email">Unable to launch email app.</string>