[#1294] Send Multi-Trx-Submission failure screen
- Closes #1294 - Changelog update
This commit is contained in:
parent
73de78caf9
commit
e2eb043afb
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ class SendViewIntegrationTest {
|
|||
|
||||
restorationTester.setContent {
|
||||
WrapSend(
|
||||
sendArgumentsWrapper = null,
|
||||
sendArguments = null,
|
||||
focusManager = LocalFocusManager.current,
|
||||
synchronizer = synchronizer,
|
||||
walletSnapshot = walletSnapshot,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue