[#1159] Send.Confirmation according to new design

- Closes #1159
- Closes #1269
- Closes #1073
- Its direct follow-ups are #1294 and #1161
- Other follow up is #1260
- These changes also enable having two Primary buttons side-by-side
- This adds sorting history of transactions by a new calculated height after send done
- This also changes how we treat empty transaction dates from `-` to `` in UI
- Changelog update
This commit is contained in:
Honza Rychnovský 2024-03-21 09:57:36 +01:00 committed by GitHub
parent 24cd22186f
commit 3845772071
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1007 additions and 730 deletions

View File

@ -15,9 +15,13 @@ directly impact users rather than highlighting other key architectural updates.*
- A new Server switching screen was 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
### Changed
- The Transaction History UI has been incorporated into the Account screen
- Reworked Send screens flow and their look (e.g., Send Failure screen is now a modal dialog instead of a separate
screen)
- The sending and shielding funds logic has been connected to the new Proposal API from the Zcash SDK
### Fixed
- Button sizing has been updated to align with the design guidelines and preserve stretching if necessary

View File

@ -158,7 +158,7 @@ ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.0.1
ANDROIDX_CORE_VERSION=1.9.0
ANDROIDX_ESPRESSO_VERSION=3.5.1
ANDROIDX_LIFECYCLE_VERSION=2.6.2
ANDROIDX_NAVIGATION_COMPOSE_VERSION=2.7.5
ANDROIDX_NAVIGATION_COMPOSE_VERSION=2.7.7
ANDROIDX_PROFILE_INSTALLER_VERSION=1.3.1
ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha06
ANDROIDX_SPLASH_SCREEN_VERSION=1.0.1

View File

@ -1,20 +0,0 @@
@file:Suppress("ktlint:standard:filename")
package cash.z.ecc.sdk.extension
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend
// TODO [#1285]: Adopt proposal API
// TODO [#1285]: https://github.com/Electric-Coin-Company/zashi-android/issues/1285
@Suppress("deprecation")
suspend fun Synchronizer.send(
spendingKey: UnifiedSpendingKey,
send: ZecSend
) = sendToAddress(
spendingKey,
send.amount,
send.destination.address,
send.memo.value
)

View File

@ -50,6 +50,7 @@ dependencies {
implementation(libs.androidx.core)
implementation(libs.androidx.splash)
implementation(libs.bundles.androidx.compose.core)
implementation(libs.bundles.androidx.compose.extended)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)

View File

@ -0,0 +1,47 @@
package co.electriccoin.zcash.ui.design.animation
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.IntOffset
import androidx.navigation.NavBackStackEntry
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
object ScreenAnimation {
private const val DURATION = 400
fun AnimatedContentTransitionScope<NavBackStackEntry>.enterTransition() =
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
initialOffset = { it },
animationSpec = tween(duration = DURATION.milliseconds)
)
fun AnimatedContentTransitionScope<NavBackStackEntry>.exitTransition() =
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
targetOffset = { it },
animationSpec = tween(duration = DURATION.milliseconds)
)
fun AnimatedContentTransitionScope<NavBackStackEntry>.popEnterTransition() =
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.End,
initialOffset = { it },
animationSpec = tween(duration = DURATION.milliseconds)
)
fun AnimatedContentTransitionScope<NavBackStackEntry>.popExitTransition() =
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.End,
targetOffset = { it },
animationSpec = tween(duration = DURATION.milliseconds)
)
}
private fun tween(duration: Duration): TweenSpec<IntOffset> =
tween(
durationMillis = duration.toInt(DurationUnit.MILLISECONDS)
)

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -57,6 +58,12 @@ private fun ButtonComposablePreview() {
TertiaryButton(onClick = { }, text = "Tertiary", enabled = false)
NavigationButton(onClick = { }, text = "Navigation")
DangerousButton(onClick = { }, text = "Dangerous")
@Suppress("MagicNumber")
Row {
PrimaryButton(onClick = { }, text = "Button 1", modifier = Modifier.weight(0.5f))
Spacer(modifier = Modifier.width(24.dp))
PrimaryButton(onClick = { }, text = "Button 2", modifier = Modifier.weight(0.5f))
}
}
}
}
@ -80,7 +87,7 @@ fun PrimaryButton(
horizontal = ZcashTheme.dimens.spacingNone,
vertical = ZcashTheme.dimens.spacingSmall
),
contentPaddingValues: PaddingValues = PaddingValues(all = 16.dp)
contentPaddingValues: PaddingValues = PaddingValues(all = 14.dp)
) {
Button(
shape = RectangleShape,
@ -104,7 +111,6 @@ fun PrimaryButton(
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
)
.defaultMinSize(minWidth, minHeight)
.fillMaxWidth()
.border(1.dp, Color.Black)
),
colors =

View File

@ -222,7 +222,7 @@ fun Tiny(
overflow = overflow,
textAlign = textAlign,
modifier = modifier,
style = MaterialTheme.typography.labelSmall,
style = MaterialTheme.typography.bodySmall,
)
}

View File

@ -14,6 +14,7 @@ data class Dimens(
val spacingXtiny: Dp,
val spacingTiny: Dp,
val spacingSmall: Dp,
val spacingMid: Dp,
val spacingDefault: Dp,
val spacingLarge: Dp,
val spacingXlarge: Dp,
@ -58,6 +59,7 @@ private val defaultDimens =
spacingXtiny = 2.dp,
spacingTiny = 4.dp,
spacingSmall = 8.dp,
spacingMid = 12.dp,
spacingDefault = 16.dp,
spacingLarge = 24.dp,
spacingXlarge = 32.dp,

View File

@ -253,7 +253,7 @@ val LocalExtendedTypography =
lineHeight = 20.sp
),
buttonText =
PrimaryTypography.bodySmall.copy(
PrimaryTypography.bodyMedium.copy(
fontWeight = FontWeight.Medium
),
buttonTextSmall =

View File

@ -46,6 +46,7 @@ android {
"src/main/res/ui/scan",
"src/main/res/ui/seed_recovery",
"src/main/res/ui/send",
"src/main/res/ui/send_confirmation",
"src/main/res/ui/settings",
"src/main/res/ui/support",
"src/main/res/ui/update",

View File

@ -1,33 +1,20 @@
package co.electriccoin.zcash.ui.fixture
import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
import co.electriccoin.zcash.ui.common.model.SerializableAddress
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
internal object SendArgumentsWrapperFixture {
val RECIPIENT_ADDRESS =
ScanResult(
SerializableAddress(
address = WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified,
type = AddressType.Unified
)
val MEMO = MemoFixture.new("Thanks for lunch").value
val AMOUNT = ZatoshiFixture.new(1)
fun amountToFixtureZecString(amount: Zatoshi?) = amount?.toZecString()
fun new(
recipientAddress: ScanResult? = RECIPIENT_ADDRESS,
amount: Zatoshi? = AMOUNT,
memo: String? = MEMO
) = SendArgumentsWrapper(
recipientAddress = recipientAddress?.toRecipient(),
amount = amountToFixtureZecString(amount),
memo = memo
)
fun new(recipientAddress: SerializableAddress? = RECIPIENT_ADDRESS) =
SendArgumentsWrapper(
recipientAddress = recipientAddress?.toRecipient(),
)
}

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.test.performTextInput
import cash.z.ecc.sdk.fixture.ZecSendFixture
import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.sendconfirmation.SendConfirmationTag
import co.electriccoin.zcash.ui.test.getAppContext
import co.electriccoin.zcash.ui.test.getStringResource
import co.electriccoin.zcash.ui.test.getStringResourceWithArgs
@ -94,8 +95,8 @@ internal fun ComposeContentTestRule.clickCreateAndSend() {
}
}
internal fun ComposeContentTestRule.clickConfirmation() {
onNodeWithTag(SendTag.SEND_CONFIRMATION_BUTTON).also {
internal fun ComposeContentTestRule.dismissFailureDialog() {
onNodeWithText(getStringResource(R.string.send_dialog_error_btn)).also {
it.performClick()
}
}
@ -107,25 +108,13 @@ internal fun ComposeContentTestRule.assertOnForm() {
}
internal fun ComposeContentTestRule.assertOnConfirmation() {
onNodeWithTag(SendTag.SEND_CONFIRMATION_BUTTON).also {
it.assertExists()
}
}
internal fun ComposeContentTestRule.assertOnSending() {
onNodeWithText(getStringResource(R.string.send_in_progress_wait)).also {
it.assertExists()
}
}
internal fun ComposeContentTestRule.assertOnSendSuccessful() {
onNodeWithText(getStringResource(R.string.send_successful_title)).also {
onNodeWithTag(SendConfirmationTag.SEND_CONFIRMATION_SEND_BUTTON).also {
it.assertExists()
}
}
internal fun ComposeContentTestRule.assertOnSendFailure() {
onNodeWithText(getStringResource(R.string.send_failure_title)).also {
onNodeWithText(getStringResource(R.string.send_dialog_error_title)).also {
it.assertExists()
}
}

View File

@ -21,7 +21,6 @@ import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.util.Locale
import java.util.concurrent.atomic.AtomicInteger
@ -91,10 +90,8 @@ class SendViewTestSetup(
onBackCount.incrementAndGet()
when (sendStage) {
SendStage.Form -> {}
SendStage.Confirmation -> setSendStage(SendStage.Form)
SendStage.Sending -> {}
SendStage.Proposing -> {}
is SendStage.SendFailure -> setSendStage(SendStage.Form)
SendStage.SendSuccessful -> {}
}
}
@ -119,17 +116,11 @@ class SendViewTestSetup(
)
),
sendStage = sendStage,
onSendStageChange = setSendStage,
onCreateZecSend = setZecSend,
zecSend = zecSend,
focusManager = LocalFocusManager.current,
onBack = onBackAction,
onSettings = { onSettingsCount.incrementAndGet() },
onCreateAndSend = {
onCreateCount.incrementAndGet()
lastZecSend = it
mutableActionExecuted.update { true }
},
onQrScannerOpen = {
onScannerCount.incrementAndGet()
},

View File

@ -77,7 +77,8 @@ class SendViewIntegrationTest {
goBalances = {},
hasCameraFeature = true,
goSettings = {},
monetarySeparators = monetarySeparators
monetarySeparators = monetarySeparators,
goSendConfirmation = {}
)
}

View File

@ -7,12 +7,10 @@ import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.sdk.fixture.ZecSendFixture
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
import co.electriccoin.zcash.ui.screen.send.assertOnForm
import co.electriccoin.zcash.ui.screen.send.assertOnSendFailure
import co.electriccoin.zcash.ui.screen.send.assertOnSendSuccessful
import co.electriccoin.zcash.ui.screen.send.assertOnSending
import co.electriccoin.zcash.ui.screen.send.clickConfirmation
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
import co.electriccoin.zcash.ui.screen.send.dismissFailureDialog
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import kotlinx.coroutines.runBlocking
import org.junit.Rule
@ -42,37 +40,18 @@ class SendViewAndroidTest : UiTestPrerequisites() {
fun back_on_sending_with_system_navigation_disabled_check() {
val testSetup =
newTestSetup(
SendStage.Confirmation,
SendStage.Form,
runBlocking { ZecSendFixture.new() }
)
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.assertOnConfirmation()
composeTestRule.clickConfirmation()
composeTestRule.assertOnSending()
composeTestRule.assertOnForm()
composeTestRule.clickCreateAndSend()
Espresso.pressBack()
composeTestRule.assertOnSending()
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun back_on_send_successful_with_system_navigation() {
val testSetup =
newTestSetup(
SendStage.SendSuccessful,
runBlocking { ZecSendFixture.new() }
)
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.assertOnSendSuccessful()
Espresso.pressBack()
composeTestRule.assertOnForm()
assertEquals(1, testSetup.getOnBackCount())
}
@ -90,7 +69,7 @@ class SendViewAndroidTest : UiTestPrerequisites() {
composeTestRule.assertOnSendFailure()
Espresso.pressBack()
composeTestRule.dismissFailureDialog()
composeTestRule.assertOnForm()

View File

@ -6,7 +6,6 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
@ -20,20 +19,17 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture
import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.send.SendTag.SEND_FAILED_BUTTON
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
import co.electriccoin.zcash.ui.screen.send.assertOnForm
import co.electriccoin.zcash.ui.screen.send.assertOnSendFailure
import co.electriccoin.zcash.ui.screen.send.assertOnSendSuccessful
import co.electriccoin.zcash.ui.screen.send.assertOnSending
import co.electriccoin.zcash.ui.screen.send.assertSendDisabled
import co.electriccoin.zcash.ui.screen.send.assertSendEnabled
import co.electriccoin.zcash.ui.screen.send.clickBack
import co.electriccoin.zcash.ui.screen.send.clickConfirmation
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
import co.electriccoin.zcash.ui.screen.send.clickScanner
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
import co.electriccoin.zcash.ui.screen.send.dismissFailureDialog
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.setAmount
import co.electriccoin.zcash.ui.screen.send.setMemo
@ -95,7 +91,6 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.setValidAddress()
composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation()
composeTestRule.clickConfirmation()
launch {
testSetup.mutableActionExecuted.collectWith(this) {
@ -132,7 +127,6 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation()
composeTestRule.clickConfirmation()
launch {
testSetup.mutableActionExecuted.collectWith(this) {
@ -191,7 +185,6 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation()
composeTestRule.clickConfirmation()
launch {
testSetup.mutableActionExecuted.collectWith(this) {
@ -273,7 +266,6 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation()
composeTestRule.clickConfirmation()
launch {
testSetup.mutableActionExecuted.collectWith(this) {
@ -324,60 +316,6 @@ class SendViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun back_on_sending_disabled_check() {
newTestSetup(
SendStage.Confirmation,
runBlocking { ZecSendFixture.new() }
)
composeTestRule.assertOnConfirmation()
composeTestRule.clickConfirmation()
composeTestRule.assertOnSending()
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.send_back_content_description)).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun back_on_send_successful() {
val testSetup =
newTestSetup(
SendStage.SendSuccessful,
runBlocking { ZecSendFixture.new() }
)
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.assertOnSendSuccessful()
composeTestRule.clickBack()
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun close_on_send_successful() {
val testSetup =
newTestSetup(
SendStage.SendSuccessful,
runBlocking { ZecSendFixture.new() }
)
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.assertOnSendSuccessful()
composeTestRule.onNodeWithText(getStringResource(R.string.send_successful_button), ignoreCase = true).also {
it.assertExists()
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun back_on_send_failure() {
@ -390,7 +328,9 @@ class SendViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.assertOnSendFailure()
composeTestRule.clickBack()
composeTestRule.dismissFailureDialog()
composeTestRule.assertOnForm()
assertEquals(1, testSetup.getOnBackCount())
@ -408,10 +348,9 @@ class SendViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.assertOnSendFailure()
composeTestRule.onNodeWithTag(SEND_FAILED_BUTTON).also {
it.assertExists()
it.performClick()
}
composeTestRule.dismissFailureDialog()
composeTestRule.assertOnForm()
assertEquals(1, testSetup.getOnBackCount())
@ -451,20 +390,6 @@ class SendViewTest : UiTestPrerequisites() {
includeEditableText = true
)
}
composeTestRule.onNodeWithText(getStringResource(R.string.send_amount_hint)).also {
it.assertTextEquals(
getStringResource(R.string.send_amount_hint),
SendArgumentsWrapperFixture.amountToFixtureZecString(SendArgumentsWrapperFixture.AMOUNT)!!,
includeEditableText = true
)
}
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo_hint)).also {
it.assertTextEquals(
getStringResource(R.string.send_memo_hint),
SendArgumentsWrapperFixture.MEMO,
includeEditableText = true
)
}
}
@Test

View File

@ -6,9 +6,11 @@ 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.SEND_AMOUNT
import co.electriccoin.zcash.ui.NavigationArguments.SEND_MEMO
import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS
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
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_RECIPIENT_ADDRESS
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.CHOOSE_SERVER
@ -17,10 +19,16 @@ import co.electriccoin.zcash.ui.NavigationTargets.HOME
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
import co.electriccoin.zcash.ui.NavigationTargets.SCAN
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
import co.electriccoin.zcash.ui.NavigationTargets.SEND_CONFIRMATION
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
import co.electriccoin.zcash.ui.common.model.SerializableAddress
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popExitTransition
import co.electriccoin.zcash.ui.screen.about.WrapAbout
import co.electriccoin.zcash.ui.screen.advancedsettings.WrapAdvancedSettings
import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer
@ -28,14 +36,19 @@ import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData
import co.electriccoin.zcash.ui.screen.home.WrapHome
import co.electriccoin.zcash.ui.screen.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
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.sendconfirmation.WrapSendConfirmation
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationArgsWrapper
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
import co.electriccoin.zcash.ui.screen.support.WrapSupport
import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate
import kotlinx.serialization.json.Json
// TODO [#1297]: Consider: Navigation passing complex data arguments different way
// TODO [#1297]: https://github.com/Electric-Coin-Company/zashi-android/issues/1297
@Composable
@Suppress("LongMethod")
internal fun MainActivity.Navigation() {
@ -44,30 +57,47 @@ internal fun MainActivity.Navigation() {
navControllerForTesting = it
}
NavHost(navController = navController, startDestination = HOME) {
NavHost(
navController = navController,
startDestination = HOME,
enterTransition = { enterTransition() },
exitTransition = { exitTransition() },
popEnterTransition = { popEnterTransition() },
popExitTransition = { popExitTransition() }
) {
composable(HOME) { backStackEntry ->
WrapHome(
onPageChange = {
homeViewModel.screenIndex.value = it
},
goBack = { finish() },
goSettings = { navController.navigateJustOnce(SETTINGS) },
goScan = { navController.navigateJustOnce(SCAN) },
goSendConfirmation = { zecSend ->
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
handle[SEND_CONFIRM_RECIPIENT_ADDRESS] =
Json.encodeToString(
serializer = SerializableAddress.serializer(),
value = zecSend.destination.toSerializableAddress()
)
handle[SEND_CONFIRM_AMOUNT] = zecSend.amount.value
handle[SEND_CONFIRM_MEMO] = zecSend.memo.value
handle[SEND_CONFIRM_PROPOSAL] = zecSend.proposal?.toByteArray()
}
navController.navigateJustOnce(SEND_CONFIRMATION)
},
goSettings = { navController.navigateJustOnce(SETTINGS) },
// At this point we only read scan result data
sendArgumentsWrapper =
SendArgumentsWrapper(
recipientAddress =
backStackEntry.savedStateHandle.get<String>(SEND_RECIPIENT_ADDRESS)?.let {
Json.decodeFromString<ScanResult>(it).toRecipient()
backStackEntry.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
Json.decodeFromString<SerializableAddress>(it).toRecipient()
},
amount = backStackEntry.savedStateHandle.get<String>(SEND_AMOUNT),
memo = backStackEntry.savedStateHandle.get<String>(SEND_MEMO)
),
).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 used Send screen parameters passed from the Scan screen if some exist
backStackEntry.savedStateHandle.remove<String>(SEND_RECIPIENT_ADDRESS)
backStackEntry.savedStateHandle.remove<String>(SEND_AMOUNT)
backStackEntry.savedStateHandle.remove<String>(SEND_MEMO)
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
WrapCheckForUpdate()
@ -135,11 +165,11 @@ internal fun MainActivity.Navigation() {
composable(SCAN) {
WrapScanValidator(
onScanValid = { scanResult ->
// At this point we only pass scan result data to recipient address
navController.previousBackStackEntry?.savedStateHandle?.apply {
set(SEND_RECIPIENT_ADDRESS, Json.encodeToString(ScanResult.serializer(), scanResult))
set(SEND_AMOUNT, null)
set(SEND_MEMO, null)
set(
SEND_SCAN_RECIPIENT_ADDRESS,
Json.encodeToString(SerializableAddress.serializer(), scanResult)
)
}
navController.popBackStackJustOnce(SCAN)
},
@ -152,6 +182,23 @@ internal fun MainActivity.Navigation() {
onConfirm = { navController.popBackStackJustOnce(EXPORT_PRIVATE_DATA) }
)
}
composable(route = SEND_CONFIRMATION) {
navController.previousBackStackEntry?.let { backStackEntry ->
WrapSendConfirmation(
goBack = { navController.popBackStackJustOnce(SEND_CONFIRMATION) },
goHome = { navController.navigateJustOnce(HOME) },
arguments =
SendConfirmationArgsWrapper.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)
backStackEntry.savedStateHandle.remove<Long>(SEND_CONFIRM_AMOUNT)
backStackEntry.savedStateHandle.remove<String>(SEND_CONFIRM_MEMO)
backStackEntry.savedStateHandle.remove<ByteArray>(SEND_CONFIRM_PROPOSAL)
}
)
}
}
}
}
@ -184,23 +231,24 @@ private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: Strin
}
object NavigationArguments {
const val SEND_RECIPIENT_ADDRESS = "send_recipient_address"
const val SEND_AMOUNT = "send_amount"
const val SEND_MEMO = "send_memo"
const val SEND_SCAN_RECIPIENT_ADDRESS = "send_scan_recipient_address"
const val SEND_CONFIRM_RECIPIENT_ADDRESS = "send_confirm_recipient_address"
const val SEND_CONFIRM_AMOUNT = "send_confirm_amount"
const val SEND_CONFIRM_MEMO = "send_confirm_memo"
const val SEND_CONFIRM_PROPOSAL = "send_confirm_proposal"
}
object NavigationTargets {
const val ABOUT = "about"
const val ACCOUNT = "account"
const val ADVANCED_SETTINGS = "advanced_settings"
const val EXPORT_PRIVATE_DATA = "export_private_data"
const val HOME = "home"
const val CHOOSE_SERVER = "choose_server"
const val RECEIVE = "receive"
const val REQUEST = "request"
const val SCAN = "scan"
const val SEED_RECOVERY = "seed_recovery"
const val SEND = "send"
const val SEND_CONFIRMATION = "send_confirmation"
const val SETTINGS = "settings"
const val SUPPORT = "support"
}

View File

@ -61,7 +61,6 @@ private fun BalanceWidgetPreview() {
}
@Composable
@Suppress("LongMethod")
fun BalanceWidget(
walletSnapshot: WalletSnapshot,
isReferenceToBalances: Boolean,
@ -75,25 +74,7 @@ fun BalanceWidget(
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
StyledBalance(
balanceString = walletSnapshot.totalBalance().toZecString(),
textStyles =
Pair(
ZcashTheme.extendedTypography.balanceWidgetStyles.first,
ZcashTheme.extendedTypography.balanceWidgetStyles.second
)
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
Image(
painter = painterResource(id = R.drawable.ic_zcash_zec_icon),
contentDescription = null,
)
}
BalanceWidgetBigLineOnly(text = walletSnapshot.totalBalance().toZecString())
Row(
verticalAlignment = Alignment.CenterVertically
@ -131,3 +112,30 @@ fun BalanceWidget(
}
}
}
@Composable
fun BalanceWidgetBigLineOnly(
text: String,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
StyledBalance(
balanceString = text,
textStyles =
Pair(
ZcashTheme.extendedTypography.balanceWidgetStyles.first,
ZcashTheme.extendedTypography.balanceWidgetStyles.second
)
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
Image(
painter = painterResource(id = R.drawable.ic_zcash_zec_icon),
contentDescription = null,
)
}
}

View File

@ -0,0 +1,35 @@
package co.electriccoin.zcash.ui.common.model
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.common.extension.AddressTypeAsStringSerializer
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
@Serializable
data class SerializableAddress(
val address: String,
@Serializable(with = AddressTypeAsStringSerializer::class)
val type: AddressType
) {
init {
// Basic validation to support the class properties type-safeness
require(address.isNotEmpty()) {
"Address parameter $address can not be empty"
}
}
internal fun toRecipient() = RecipientAddressState(address, type)
// Calling the conversion inside the blocking coroutine is ok, as we do not expect it to be time-consuming
internal fun toWalletAddress() =
runBlocking {
when (type) {
AddressType.Unified -> WalletAddress.Unified.new(address)
AddressType.Shielded -> WalletAddress.Sapling.new(address)
AddressType.Transparent -> WalletAddress.Transparent.new(address)
is AddressType.Invalid -> error("Invalid address type")
}
}
}

View File

@ -36,6 +36,7 @@ import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.state.getSortHeight
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -186,23 +187,32 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
synchronizer
.filterNotNull()
.flatMapLatest { synchronizer ->
synchronizer.transactions
.combine(synchronizer.status) {
transactions: List<TransactionOverview>, status: Synchronizer.Status ->
val enhancedTransactions =
transactions.map {
combine(
synchronizer.transactions,
synchronizer.status,
synchronizer.networkHeight.filterNotNull()
) {
transactions: List<TransactionOverview>,
status: Synchronizer.Status,
networkHeight: BlockHeight ->
val enhancedTransactions =
transactions
.sortedByDescending {
it.getSortHeight(networkHeight)
}
.map {
if (it.isSentTransaction) {
TransactionOverviewExt(it, synchronizer.getRecipients(it).firstOrNull())
} else {
TransactionOverviewExt(it, null)
}
}
if (status.isSyncing()) {
TransactionHistorySyncState.Syncing(enhancedTransactions.toPersistentList())
} else {
TransactionHistorySyncState.Done(enhancedTransactions.toPersistentList())
}
if (status.isSyncing()) {
TransactionHistorySyncState.Syncing(enhancedTransactions.toPersistentList())
} else {
TransactionHistorySyncState.Done(enhancedTransactions.toPersistentList())
}
}
}
.stateIn(
scope = viewModelScope,

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.screen.account.state
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
@ -7,3 +8,13 @@ data class TransactionOverviewExt(
val overview: TransactionOverview,
val recipient: TransactionRecipient?
)
fun TransactionOverview.getSortHeight(networkHeight: BlockHeight): BlockHeight {
// Non-null assertion operator is necessary here as the smart cast to is impossible because `minedHeight` and
// `expiryHeight` are declared in a different module
return when {
minedHeight != null -> minedHeight!!
(expiryHeight?.value ?: 0) > 0 -> expiryHeight!!
else -> networkHeight
}
}

View File

@ -352,9 +352,8 @@ fun HistoryItem(
// * 1000 to covert to millis
@Suppress("MagicNumber")
dateFormat.format(blockTimeEpochSeconds.times(1000))
} ?: "-"
} ?: "-"
// For now, use the same label for the above missing transaction date
} ?: ""
} ?: ""
Text(
text = dateString,

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.advancedsettings.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@ -97,21 +98,24 @@ private fun AdvancedSettingsMainContent(
) {
PrimaryButton(
onClick = onSeedRecovery,
text = stringResource(R.string.advanced_settings_backup_wallet)
text = stringResource(R.string.advanced_settings_backup_wallet),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onExportPrivateData,
text = stringResource(R.string.advanced_settings_export_private_data)
text = stringResource(R.string.advanced_settings_export_private_data),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onChooseServer,
text = stringResource(R.string.advanced_settings_choose_server)
text = stringResource(R.string.advanced_settings_choose_server),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingHuge))

View File

@ -151,7 +151,9 @@ internal fun WrapBalances(
synchronizer.createProposedTransactions(
proposal = newProposal,
usk = spendingKey
)
).collect {
Twig.info { "Printing only for now. Will be reworked. Result: $it" }
}
}.onSuccess {
Twig.debug { "Shielding transaction event" }
setShieldState(ShieldState.None)

View File

@ -78,7 +78,7 @@ import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues
@Preview("Balances")
@Composable
private fun ComposablePreview() {
private fun ComposableBalancesPreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Balances(
@ -96,6 +96,26 @@ private fun ComposablePreview() {
}
}
@Preview("BalancesShieldFailure")
@Composable
private fun ComposableBalancesShieldFailurePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Balances(
onSettings = {},
isFiatConversionEnabled = false,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
onShielding = {},
shieldState = ShieldState.Available,
walletSnapshot = WalletSnapshotFixture.new(),
isShowingErrorDialog = true,
setShowErrorDialog = {},
)
}
}
}
@Suppress("LongParameterList")
@Composable
fun Balances(
@ -273,6 +293,7 @@ fun TransparentBalancePanel(
textStyle = ZcashTheme.extendedTypography.buttonTextSmall,
enabled = shieldState == ShieldState.Available,
minHeight = ZcashTheme.dimens.buttonHeightSmall,
modifier = Modifier.fillMaxWidth(),
outerPaddingValues =
PaddingValues(
horizontal = 54.dp,

View File

@ -338,7 +338,10 @@ fun SaveButton(
onServerChange(selectedServer)
},
modifier = modifier
modifier =
modifier.then(
Modifier.fillMaxWidth()
)
)
}

View File

@ -146,7 +146,8 @@ private fun ExportPrivateDataContent(
PrimaryButton(
onClick = onConfirm,
text = stringResource(R.string.export_data_confirm).uppercase(),
enabled = checkedState.value
enabled = checkedState.value,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingHuge))

View File

@ -8,6 +8,7 @@ import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource
import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
@ -23,11 +24,13 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@Composable
@Suppress("LongParameterList")
internal fun MainActivity.WrapHome(
onPageChange: (HomeScreenIndex) -> Unit,
goBack: () -> Unit,
goSettings: () -> Unit,
goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
sendArgumentsWrapper: SendArgumentsWrapper
) {
WrapHome(
@ -35,6 +38,7 @@ internal fun MainActivity.WrapHome(
onPageChange = onPageChange,
goBack = goBack,
goScan = goScan,
goSendConfirmation = goSendConfirmation,
goSettings = goSettings,
sendArgumentsWrapper = sendArgumentsWrapper
)
@ -47,6 +51,7 @@ internal fun WrapHome(
goBack: () -> Unit,
goSettings: () -> Unit,
goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
onPageChange: (HomeScreenIndex) -> Unit,
sendArgumentsWrapper: SendArgumentsWrapper
) {
@ -98,6 +103,7 @@ internal fun WrapHome(
goToQrScanner = goScan,
goBack = homeGoBack,
goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) },
goSendConfirmation = goSendConfirmation,
goSettings = goSettings,
sendArgumentsWrapper = sendArgumentsWrapper
)

View File

@ -210,6 +210,7 @@ private fun NewWalletRecoveryMainContent(
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
end = ZcashTheme.dimens.screenHorizontalSpacingBig
)
.fillMaxWidth()
)
}
}

View File

@ -200,7 +200,8 @@ private fun OnboardingMainContent(
PrimaryButton(
onClick = onCreateWallet,
text = stringResource(R.string.onboarding_create_new_wallet)
text = stringResource(R.string.onboarding_create_new_wallet),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.request.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -161,7 +162,8 @@ private fun RequestMainContent(
onCreateAndSend(ZecRequest(myAddress, zatoshi!!, ZecRequestMessage(message)))
},
text = stringResource(id = R.string.request_create),
enabled = null != zatoshi
enabled = null != zatoshi,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))

View File

@ -397,7 +397,8 @@ private fun RestoreSeedMainContent(
onClick = goNext,
enabled = isSeedValid,
text = stringResource(id = R.string.restore_seed_button_next),
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
@ -771,7 +772,8 @@ private fun RestoreBirthdayMainContent(
onDone()
},
text = stringResource(R.string.restore_birthday_button_restore),
enabled = isBirthdayValid
enabled = isBirthdayValid,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))

View File

@ -9,16 +9,16 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.SerializableAddress
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
import co.electriccoin.zcash.ui.screen.scan.view.Scan
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapScanValidator(
onScanValid: (address: ScanResult) -> Unit,
onScanValid: (address: SerializableAddress) -> Unit,
goBack: () -> Unit
) {
WrapScan(
@ -31,7 +31,7 @@ internal fun MainActivity.WrapScanValidator(
@Composable
fun WrapScan(
activity: ComponentActivity,
onScanValid: (address: ScanResult) -> Unit,
onScanValid: (address: SerializableAddress) -> Unit,
goBack: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
@ -55,7 +55,7 @@ fun WrapScan(
val addressType = synchronizer.validateAddress(result)
val isAddressValid = !addressType.isNotValid
if (isAddressValid) {
onScanValid(ScanResult(result, addressType))
onScanValid(SerializableAddress(result, addressType))
} else {
snackbarHostState.showSnackbar(
message = activity.getString(R.string.scan_validation_invalid_address)

View File

@ -1,22 +0,0 @@
package co.electriccoin.zcash.ui.screen.scan.model
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.common.extension.AddressTypeAsStringSerializer
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
import kotlinx.serialization.Serializable
@Serializable
data class ScanResult(
val address: String,
@Serializable(with = AddressTypeAsStringSerializer::class)
val type: AddressType
) {
init {
// Basic validation to support the class properties type-safeness
require(address.isNotEmpty()) {
"Address parameter $address can not be empty"
}
}
fun toRecipient() = RecipientAddressState(address, type)
}

View File

@ -138,7 +138,8 @@ private fun SecurityWarningContent(
PrimaryButton(
onClick = onConfirm,
text = stringResource(R.string.security_warning_confirm).uppercase(),
enabled = checkedState.value
enabled = checkedState.value,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))

View File

@ -220,6 +220,7 @@ private fun SeedRecoveryMainContent(
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
end = ZcashTheme.dimens.screenHorizontalSpacingBig
)
.fillMaxWidth()
)
}
}

View File

@ -45,6 +45,7 @@ internal fun WrapSend(
goToQrScanner: () -> Unit,
goBack: () -> Unit,
goBalances: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
goSettings: () -> Unit,
) {
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
@ -80,12 +81,13 @@ internal fun WrapSend(
goBack,
goBalances,
goSettings,
goSendConfirmation,
hasCameraFeature,
monetarySeparators
)
}
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
@Suppress("LongParameterList", "LongMethod")
@VisibleForTesting
@Composable
internal fun WrapSend(
@ -98,6 +100,7 @@ internal fun WrapSend(
goBack: () -> Unit,
goBalances: () -> Unit,
goSettings: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
hasCameraFeature: Boolean,
monetarySeparators: MonetarySeparators
) {
@ -105,11 +108,6 @@ internal fun WrapSend(
val context = LocalContext.current
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
// change once deep-linking support is added. It depends on whether deep linking should do one of:
// 1. Use a different UI flow entirely
// 2. Show a pre-filled Send form
// 3. Go directly to the Confirmation screen
val (sendStage, setSendStage) =
rememberSaveable(stateSaver = SendStage.Saver) { mutableStateOf(SendStage.Form) }
@ -140,41 +138,18 @@ internal fun WrapSend(
)
)
}
if (sendArgumentsWrapper?.amount != null) {
setAmountState(
AmountState.new(
context = context,
value = sendArgumentsWrapper.amount,
monetarySeparators = monetarySeparators
)
)
}
// Memo computation:
val (memoState, setMemoState) =
rememberSaveable(stateSaver = MemoState.Saver) {
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
}
if (sendArgumentsWrapper?.memo != null) {
setMemoState(MemoState.new(sendArgumentsWrapper.memo))
}
val onBackAction = {
when (sendStage) {
SendStage.Form -> goBack()
SendStage.Confirmation -> setSendStage(SendStage.Form)
SendStage.Sending -> { /* no action - wait until the sending is done */ }
SendStage.Proposing -> { /* no action - wait until the sending is done */ }
is SendStage.SendFailure -> setSendStage(SendStage.Form)
SendStage.SendSuccessful -> {
// Reset Send.Form values
setZecSend(null)
setRecipientAddressState(RecipientAddressState.new(""))
setAmountState(AmountState.new(context, "", monetarySeparators))
setMemoState(MemoState.new(""))
setSendStage(SendStage.Form)
goBack()
}
}
}
@ -191,25 +166,23 @@ internal fun WrapSend(
Send(
walletSnapshot = walletSnapshot,
sendStage = sendStage,
onSendStageChange = setSendStage,
zecSend = zecSend,
onCreateZecSend = { newZecSend ->
scope.launch {
Twig.debug { "Getting send transaction proposal" }
runCatching {
synchronizer.proposeSend(spendingKey.account, newZecSend)
}.onSuccess { proposal ->
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
val enrichedZecSend = newZecSend.copy(proposal = proposal)
setZecSend(enrichedZecSend)
goSendConfirmation(enrichedZecSend)
}.onFailure {
Twig.error(it) { "Transaction proposal failed" }
// TODO [#1161]: Remove Send-Success and rework Send-Failure
// TODO [#1161]: https://github.com/Electric-Coin-Company/zashi-android/issues/1161
setSendStage(SendStage.SendFailure(it.message ?: ""))
}
.onSuccess { proposal ->
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
setSendStage(SendStage.Confirmation)
setZecSend(newZecSend.copy(proposal = proposal))
}
.onFailure {
Twig.error(it) { "Transaction proposal failed" }
// TODO [#1161]: Remove Send-Success and rework Send-Failure
// TODO [#1161]: https://github.com/Electric-Coin-Company/zashi-android/issues/1161
setSendStage(SendStage.SendFailure(it.message ?: ""))
}
}
},
focusManager = focusManager,
@ -228,30 +201,6 @@ internal fun WrapSend(
)
}
},
onCreateAndSend = { newZecSend ->
scope.launch {
Twig.debug { "Sending transaction" }
// 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)
}
.onSuccess {
setSendStage(SendStage.SendSuccessful)
Twig.debug { "Transaction id:$it submitted successfully" }
}
.onFailure {
Twig.error(it) { "Transaction submission failed" }
setSendStage(SendStage.SendFailure(it.message ?: ""))
}
}
},
memoState = memoState,
setMemoState = setMemoState,
amountState = amountState,

View File

@ -5,7 +5,6 @@ package co.electriccoin.zcash.ui.screen.send
*/
object SendTag {
const val SEND_FORM_BUTTON = "send_form_button"
const val SEND_CONFIRMATION_BUTTON = "send_confirmation_button"
const val SEND_FAILED_BUTTON = "send_failed_button"
const val SEND_SUCCESS_BUTTON = "send_success_button"
}

View File

@ -6,7 +6,9 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.SerializableAddress
/**
* How far into the address will be abbreviation look forwards and backwards.
@ -26,5 +28,16 @@ internal fun WalletAddress.abbreviated(context: Context): String {
val firstFive = address.substring(0, ABBREVIATION_INDEX)
val lastFive = address.substring(address.length - ABBREVIATION_INDEX, address.length)
return context.getString(R.string.send_confirmation_abbreviated_address_format, firstFive, lastFive)
return context.getString(R.string.send_abbreviated_address_format, firstFive, lastFive)
}
internal fun WalletAddress.toSerializableAddress() =
SerializableAddress(
address = address,
type =
when (this) {
is WalletAddress.Unified -> AddressType.Unified
is WalletAddress.Sapling -> AddressType.Shielded
is WalletAddress.Transparent -> AddressType.Transparent
}
)

View File

@ -2,6 +2,4 @@ package co.electriccoin.zcash.ui.screen.send.model
data class SendArgumentsWrapper(
val recipientAddress: RecipientAddressState? = null,
val amount: String? = null,
val memo: String? = null
)

View File

@ -5,20 +5,14 @@ import androidx.compose.runtime.saveable.mapSaver
sealed class SendStage {
data object Form : SendStage()
data object Confirmation : SendStage()
data object Sending : SendStage()
data object Proposing : SendStage()
data class SendFailure(val error: String) : SendStage()
data object SendSuccessful : SendStage()
companion object {
private const val TYPE_FORM = "form" // $NON-NLS
private const val TYPE_CONFIRMATION = "confirmation" // $NON-NLS
private const val TYPE_SENDING = "sending" // $NON-NLS
private const val TYPE_PROPOSING = "proposing" // $NON-NLS
private const val TYPE_FAILURE = "failure" // $NON-NLS
private const val TYPE_SUCCESSFUL = "successful" // $NON-NLS
private const val KEY_TYPE = "type" // $NON-NLS
private const val KEY_ERROR = "error" // $NON-NLS
@ -34,10 +28,8 @@ sealed class SendStage {
val sendStageString = (it[KEY_TYPE] as String)
when (sendStageString) {
TYPE_FORM -> Form
TYPE_CONFIRMATION -> Confirmation
TYPE_SENDING -> Sending
TYPE_PROPOSING -> Proposing
TYPE_FAILURE -> SendFailure((it[KEY_ERROR] as String))
TYPE_SUCCESSFUL -> SendSuccessful
else -> null
}
}
@ -49,13 +41,11 @@ sealed class SendStage {
val saverMap = HashMap<String, String>()
when (this) {
Form -> saverMap[KEY_TYPE] = TYPE_FORM
Confirmation -> saverMap[KEY_TYPE] = TYPE_CONFIRMATION
Proposing -> saverMap[KEY_TYPE] = TYPE_PROPOSING
is SendFailure -> {
saverMap[KEY_TYPE] = TYPE_FAILURE
saverMap[KEY_ERROR] = this.error
}
SendSuccessful -> saverMap[KEY_TYPE] = TYPE_SUCCESSFUL
Sending -> saverMap[KEY_TYPE] = TYPE_SENDING
}
return saverMap

View File

@ -5,7 +5,6 @@ package co.electriccoin.zcash.ui.screen.send.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@ -40,7 +39,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi
@ -48,7 +46,6 @@ import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.ZecSendExt
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.spackle.Twig
@ -58,23 +55,21 @@ import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.model.spendableBalance
import co.electriccoin.zcash.ui.common.test.CommonTag
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.BodySmall
import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Small
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.send.ext.abbreviated
import co.electriccoin.zcash.ui.screen.send.ext.valueOrEmptyChar
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.SendStage
import kotlinx.coroutines.runBlocking
import java.util.Locale
@Composable
@ -85,13 +80,11 @@ private fun PreviewSendForm() {
Send(
walletSnapshot = WalletSnapshotFixture.new(),
sendStage = SendStage.Form,
onSendStageChange = {},
zecSend = null,
onCreateZecSend = {},
focusManager = LocalFocusManager.current,
onBack = {},
onSettings = {},
onCreateAndSend = {},
onQrScannerOpen = {},
goBalances = {},
hasCameraFeature = true,
@ -106,38 +99,12 @@ private fun PreviewSendForm() {
}
}
@Composable
@Preview("SendSuccessful")
private fun PreviewSendSuccessful() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SendSuccessful(
zecSend =
ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() },
amount = ZatoshiFixture.new(),
memo = MemoFixture.new(),
proposal = null,
),
onDone = {}
)
}
}
}
@Composable
@Preview("SendFailure")
private fun PreviewSendFailure() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SendFailure(
zecSend =
ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() },
amount = ZatoshiFixture.new(),
memo = MemoFixture.new(),
proposal = null,
),
onDone = {},
reason = "Insufficient balance"
)
@ -145,37 +112,19 @@ private fun PreviewSendFailure() {
}
}
@Composable
@Preview("SendConfirmation")
private fun PreviewSendConfirmation() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SendConfirmation(
zecSend =
ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() },
amount = ZatoshiFixture.new(),
memo = MemoFixture.new(),
proposal = null,
),
onConfirmation = {}
)
}
}
}
// TODO [#1260]: Cover Send screens UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
@Suppress("LongParameterList")
@Composable
fun Send(
walletSnapshot: WalletSnapshot,
sendStage: SendStage,
onSendStageChange: (SendStage) -> Unit,
zecSend: ZecSend?,
onCreateZecSend: (ZecSend) -> Unit,
focusManager: FocusManager,
onBack: () -> Unit,
onSettings: () -> Unit,
onCreateAndSend: (ZecSend) -> Unit,
onQrScannerOpen: () -> Unit,
goBalances: () -> Unit,
hasCameraFeature: Boolean,
@ -187,18 +136,13 @@ fun Send(
memoState: MemoState,
) {
Scaffold(topBar = {
SendTopAppBar(
onBack = onBack,
onSettings = onSettings,
showBackNavigationButton = (sendStage != SendStage.Sending && sendStage != SendStage.Form)
)
SendTopAppBar(onSettings = onSettings)
}) { paddingValues ->
SendMainContent(
walletSnapshot = walletSnapshot,
onBack = onBack,
focusManager = focusManager,
sendStage = sendStage,
onSendStageChange = onSendStageChange,
zecSend = zecSend,
onCreateZecSend = onCreateZecSend,
recipientAddressState = recipientAddressState,
@ -207,7 +151,6 @@ fun Send(
setAmountState = setAmountState,
memoState = memoState,
setMemoState = setMemoState,
onSendSubmit = onCreateAndSend,
onQrScannerOpen = onQrScannerOpen,
goBalances = goBalances,
hasCameraFeature = hasCameraFeature,
@ -224,21 +167,9 @@ fun Send(
}
@Composable
private fun SendTopAppBar(
onBack: () -> Unit,
onSettings: () -> Unit,
showBackNavigationButton: Boolean = true
) {
private fun SendTopAppBar(onSettings: () -> Unit) {
SmallTopAppBar(
titleText = stringResource(id = R.string.send_title),
onBack = onBack,
backText =
if (showBackNavigationButton) {
stringResource(id = R.string.send_back)
} else {
null
},
backContentDescriptionText = stringResource(id = R.string.send_back_content_description),
titleText = stringResource(id = R.string.send_stage_send_title),
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
@ -263,8 +194,6 @@ private fun SendMainContent(
zecSend: ZecSend?,
onCreateZecSend: (ZecSend) -> Unit,
sendStage: SendStage,
onSendStageChange: (SendStage) -> Unit,
onSendSubmit: (ZecSend) -> Unit,
onQrScannerOpen: () -> Unit,
recipientAddressState: RecipientAddressState,
onRecipientAddressChange: (String) -> Unit,
@ -276,7 +205,9 @@ private fun SendMainContent(
modifier: Modifier = Modifier,
) {
when {
(sendStage == SendStage.Form || null == zecSend) -> {
// For now, we merge [SendStage.Form] and [SendStage.Proposing] into one stage. We could eventually display a
// loader if calling the Proposal API takes longer than expected
(sendStage == SendStage.Form || sendStage == SendStage.Proposing || null == zecSend) -> {
SendForm(
walletSnapshot = walletSnapshot,
recipientAddressState = recipientAddressState,
@ -293,40 +224,17 @@ private fun SendMainContent(
modifier = modifier
)
}
(sendStage == SendStage.Confirmation) -> {
SendConfirmation(
zecSend = zecSend,
onConfirmation = {
onSendStageChange(SendStage.Sending)
onSendSubmit(zecSend)
},
modifier = modifier
)
}
(sendStage == SendStage.Sending) -> {
Sending(
zecSend = zecSend,
modifier = modifier
)
}
(sendStage == SendStage.SendSuccessful) -> {
SendSuccessful(
zecSend = zecSend,
onDone = onBack,
modifier = modifier,
)
}
(sendStage is SendStage.SendFailure) -> {
SendFailure(
zecSend = zecSend,
reason = sendStage.error,
onDone = onBack,
modifier = modifier,
)
}
}
}
const val DEFAULT_LESS_THAN_FEE = 100_000L
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
// TODO [#217]: https://github.com/Electric-Coin-Company/zashi-android/issues/217
@ -471,7 +379,10 @@ private fun SendForm(
},
text = stringResource(id = R.string.send_create),
enabled = sendButtonEnabled,
modifier = Modifier.testTag(SendTag.SEND_FORM_BUTTON)
modifier =
Modifier
.testTag(SendTag.SEND_FORM_BUTTON)
.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
@ -482,8 +393,7 @@ private fun SendForm(
id = R.string.send_fee,
// TODO [#1047]: Representing Zatoshi amount
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
@Suppress("MagicNumber")
Zatoshi(100_000L).toZecString()
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
),
textFontWeight = FontWeight.SemiBold
)
@ -511,7 +421,7 @@ fun SendFormAddressTextField(
// Scroll TextField above ime keyboard
.bringIntoViewRequester(bringIntoViewRequester)
) {
Body(text = stringResource(id = R.string.send_address_label))
Small(text = stringResource(id = R.string.send_address_label))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
@ -617,7 +527,7 @@ fun SendFormAmountTextField(
// Scroll TextField above ime keyboard
.bringIntoViewRequester(bringIntoViewRequester)
) {
Body(text = stringResource(id = R.string.send_amount_label))
Small(text = stringResource(id = R.string.send_amount_label))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
@ -682,7 +592,7 @@ fun SendFormMemoTextField(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(id = R.drawable.send_papre_plane),
painter = painterResource(id = R.drawable.send_paper_plane),
contentDescription = null,
tint =
if (isMemoFieldAvailable) {
@ -694,7 +604,7 @@ fun SendFormMemoTextField(
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
Body(
Small(
text = stringResource(id = R.string.send_memo_label),
color =
if (isMemoFieldAvailable) {
@ -709,7 +619,12 @@ fun SendFormMemoTextField(
FormTextField(
enabled = isMemoFieldAvailable,
value = memoState.text,
value =
if (isMemoFieldAvailable) {
memoState.text
} else {
""
},
onValueChange = {
setMemoState(MemoState.new(it))
},
@ -762,238 +677,17 @@ fun SendFormMemoTextField(
}
@Composable
private fun SendConfirmation(
zecSend: ZecSend,
onConfirmation: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Body(
stringResource(
R.string.send_confirmation_amount_format,
zecSend.amount.toZecString(),
)
)
Body(
stringResource(
R.string.send_confirmation_address_format,
zecSend.destination.abbreviated()
)
)
if (zecSend.memo.value.isNotEmpty()) {
Body(
stringResource(
R.string.send_confirmation_memo_format,
zecSend.memo.value
)
)
}
if (zecSend.proposal != null) {
// 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(zecSend.proposal)
Body(
stringResource(
R.string.send_confirmation_fee_format,
zecSend.proposal!!.totalFeeRequired().toZecString()
)
)
}
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
PrimaryButton(
modifier =
Modifier
.padding(top = ZcashTheme.dimens.spacingSmall)
.testTag(SendTag.SEND_CONFIRMATION_BUTTON),
onClick = onConfirmation,
text = stringResource(id = R.string.send_confirmation_button),
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
}
@Composable
private fun Sending(
zecSend: ZecSend,
modifier: Modifier = Modifier
) {
Column(modifier) {
Header(
text =
stringResource(
R.string.send_in_progress_amount_format,
zecSend.amount.toZecString()
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Body(
text = zecSend.destination.abbreviated(),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
if (zecSend.memo.value.isNotEmpty()) {
Body(
stringResource(
R.string.send_in_progress_memo_format,
zecSend.memo.value
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
Body(
modifier =
Modifier
.padding(vertical = ZcashTheme.dimens.spacingSmall)
.fillMaxWidth(),
text = stringResource(R.string.send_in_progress_wait),
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun SendSuccessful(
zecSend: ZecSend,
onDone: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Header(
text = stringResource(R.string.send_successful_title),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(
modifier =
Modifier
.fillMaxWidth()
.height(ZcashTheme.dimens.spacingDefault)
)
Body(
stringResource(
R.string.send_successful_amount_address_memo,
zecSend.amount.toZecString(),
zecSend.destination.abbreviated(),
zecSend.memo.valueOrEmptyChar()
)
)
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
PrimaryButton(
modifier =
Modifier
.padding(top = ZcashTheme.dimens.spacingSmall)
.testTag(SendTag.SEND_SUCCESS_BUTTON),
text = stringResource(R.string.send_successful_button),
onClick = onDone,
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
}
@Composable
@Suppress("UNUSED_PARAMETER")
private fun SendFailure(
zecSend: ZecSend,
onDone: () -> Unit,
reason: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Header(
text = stringResource(R.string.send_failure_title),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
// Once we ensure that the [reason] contains a localized message, we can leverage it for the UI prompt
Spacer(
modifier =
Modifier
.fillMaxWidth()
.height(ZcashTheme.dimens.spacingDefault)
)
Body(
stringResource(
R.string.send_failure_amount_address_memo,
zecSend.amount.toZecString(),
zecSend.destination.abbreviated(),
zecSend.memo.valueOrEmptyChar()
)
)
Spacer(
modifier =
Modifier
.fillMaxWidth()
.height(ZcashTheme.dimens.spacingDefault)
)
Body(
stringResource(
R.string.send_failure_reason,
reason,
)
)
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
PrimaryButton(
modifier =
Modifier
.padding(top = ZcashTheme.dimens.spacingSmall)
.testTag(SendTag.SEND_FAILED_BUTTON),
text = stringResource(R.string.send_failure_button),
onClick = onDone,
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
AppAlertDialog(
title = stringResource(id = R.string.send_dialog_error_title),
text = stringResource(id = R.string.send_dialog_error_text),
confirmButtonText = stringResource(id = R.string.send_dialog_error_btn),
onConfirmButtonClick = onDone
)
}

View File

@ -0,0 +1,126 @@
@file:Suppress("ktlint:standard:filename")
package co.electriccoin.zcash.ui.screen.sendconfirmation
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.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.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.model.SendConfirmationStage
import co.electriccoin.zcash.ui.screen.sendconfirmation.view.SendConfirmation
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapSendConfirmation(
goBack: () -> Unit,
goHome: () -> Unit,
arguments: SendConfirmationArgsWrapper
) {
val walletViewModel by this.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
WrapSendConfirmation(
arguments,
synchronizer,
spendingKey,
goBack,
goHome,
)
}
@VisibleForTesting
@Composable
internal fun WrapSendConfirmation(
arguments: SendConfirmationArgsWrapper,
synchronizer: Synchronizer?,
spendingKey: UnifiedSpendingKey?,
goBack: () -> Unit,
goHome: () -> Unit,
) {
val scope = rememberCoroutineScope()
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
checkNotNull(zecSend)
val (stage, setStage) =
rememberSaveable(stateSaver = SendConfirmationStage.Saver) {
mutableStateOf(SendConfirmationStage.Confirmation)
}
val onBackAction = {
when (stage) {
SendConfirmationStage.Confirmation -> goBack()
SendConfirmationStage.Sending -> { /* no action - wait until the sending is done */ }
is SendConfirmationStage.Failure -> setStage(SendConfirmationStage.Confirmation)
is SendConfirmationStage.MultipleTrxFailure -> {
// TODO [#1161]: Remove Send-Success and rework Send-Failure
// TODO [#1161]: https://github.com/Electric-Coin-Company/zashi-android/issues/1161
setStage(SendConfirmationStage.Confirmation)
}
}
}
BackHandler {
onBackAction()
}
if (null == synchronizer || null == spendingKey) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
CircularScreenProgressIndicator()
} else {
SendConfirmation(
stage = stage,
onStageChange = setStage,
zecSend = zecSend!!,
onBack = onBackAction,
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" }
setStage(SendConfirmationStage.Confirmation)
goHome()
}
.onFailure {
Twig.error(it) { "Transaction submission failed" }
setStage(SendConfirmationStage.Failure(it.message ?: ""))
}
}
}
)
}
}

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation
/**
* These are only used for automated testing.
*/
object SendConfirmationTag {
const val SEND_CONFIRMATION_SEND_BUTTON = "send_confirmation_send_button"
const val SEND_CONFIRMATION_BACK_BUTTON = "send_confirmation_back_button"
}

View File

@ -0,0 +1,65 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.model
import androidx.lifecycle.SavedStateHandle
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.ui.NavigationArguments
import co.electriccoin.zcash.ui.common.model.SerializableAddress
import kotlinx.serialization.json.Json
data class SendConfirmationArgsWrapper(
val address: SerializableAddress?,
val amount: Long?,
val memo: String?,
val proposal: ByteArray?,
) {
companion object {
internal fun fromSavedStateHandle(savedStateHandle: SavedStateHandle) =
SendConfirmationArgsWrapper(
address =
savedStateHandle.get<String>(NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS)?.let {
Json.decodeFromString<SerializableAddress>(it)
},
amount = savedStateHandle.get<Long>(NavigationArguments.SEND_CONFIRM_AMOUNT),
memo = savedStateHandle.get<String>(NavigationArguments.SEND_CONFIRM_MEMO),
proposal = savedStateHandle.get<ByteArray>(NavigationArguments.SEND_CONFIRM_PROPOSAL),
)
}
internal fun toZecSend() =
ZecSend(
destination = address?.toWalletAddress() ?: error("Address null"),
amount = amount?.let { Zatoshi(amount) } ?: error("Amount null"),
memo = memo?.let { Memo(memo) } ?: error("Memo null"),
proposal = proposal?.let { Proposal.fromByteArray(proposal) } ?: error("Proposal null"),
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SendConfirmationArgsWrapper
if (amount != other.amount) return false
if (memo != other.memo) return false
if (address != other.address) return false
if (proposal != null) {
if (other.proposal == null) return false
if (!proposal.contentEquals(other.proposal)) return false
} else if (other.proposal != null) {
return false
}
return true
}
override fun hashCode(): Int {
var result = amount?.hashCode() ?: 0
result = 31 * result + (memo?.hashCode() ?: 0)
result = 31 * result + (address?.hashCode() ?: 0)
result = 31 * result + (proposal?.contentHashCode() ?: 0)
return result
}
}

View File

@ -0,0 +1,62 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.model
import androidx.compose.runtime.saveable.mapSaver
sealed class SendConfirmationStage {
data object Confirmation : SendConfirmationStage()
data object Sending : SendConfirmationStage()
data class Failure(val error: String) : SendConfirmationStage()
data class MultipleTrxFailure(val error: String) : 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 KEY_TYPE = "type" // $NON-NLS
private const val KEY_ERROR = "error" // $NON-NLS
internal val Saver
get() =
run {
mapSaver<SendConfirmationStage>(
save = { it.toSaverMap() },
restore = {
if (it.isEmpty()) {
null
} else {
val sendStageString = (it[KEY_TYPE] as String)
when (sendStageString) {
TYPE_CONFIRMATION -> Confirmation
TYPE_SENDING -> Sending
TYPE_FAILURE -> Failure((it[KEY_ERROR] as String))
TYPE_MULTIPLE_TRX_FAILURE -> MultipleTrxFailure((it[KEY_ERROR] as String))
else -> null
}
}
}
)
}
private fun SendConfirmationStage.toSaverMap(): HashMap<String, String> {
val saverMap = HashMap<String, String>()
when (this) {
Confirmation -> saverMap[KEY_TYPE] = TYPE_CONFIRMATION
Sending -> saverMap[KEY_TYPE] = TYPE_SENDING
is Failure -> {
saverMap[KEY_TYPE] = TYPE_FAILURE
saverMap[KEY_ERROR] = this.error
}
is MultipleTrxFailure -> {
saverMap[KEY_TYPE] = TYPE_FAILURE
saverMap[KEY_ERROR] = this.error
}
}
return saverMap
}
}
}

View File

@ -0,0 +1,319 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.view
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
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.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
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.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Small
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.StyledBalance
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.coroutines.runBlocking
@Composable
@Preview("SendConfirmationFailure")
private fun PreviewSendConfirmationFailure() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SendFailure(
onDone = {},
reason = "Failed due to network error"
)
}
}
}
@Composable
@Preview("SendConfirmation")
private fun PreviewSendConfirmation() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SendConfirmationContent(
zecSend =
ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() },
amount = ZatoshiFixture.new(),
memo = MemoFixture.new(),
proposal = null,
),
onConfirmation = {},
onBack = {},
isSending = false
)
}
}
}
// TODO [#1260]: Cover Send screens UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
@Composable
fun SendConfirmation(
stage: SendConfirmationStage,
onStageChange: (SendConfirmationStage) -> Unit,
zecSend: ZecSend,
onBack: () -> Unit,
onCreateAndSend: (ZecSend) -> Unit,
) {
Scaffold(topBar = {
SendConfirmationTopAppBar()
}) { paddingValues ->
SendConfirmationMainContent(
onBack = onBack,
stage = stage,
onStageChange = onStageChange,
zecSend = zecSend,
onSendSubmit = onCreateAndSend,
modifier =
Modifier
.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding(),
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
)
}
}
@Composable
private fun SendConfirmationTopAppBar() {
SmallTopAppBar(
titleText = stringResource(id = R.string.send_stage_confirmation_title)
)
}
@Composable
@Suppress("LongParameterList")
private fun SendConfirmationMainContent(
onBack: () -> Unit,
zecSend: ZecSend,
stage: SendConfirmationStage,
onStageChange: (SendConfirmationStage) -> Unit,
onSendSubmit: (ZecSend) -> Unit,
modifier: Modifier = Modifier,
) {
when (stage) {
SendConfirmationStage.Confirmation -> {
SendConfirmationContent(
zecSend = zecSend,
onBack = onBack,
onConfirmation = {
onStageChange(SendConfirmationStage.Sending)
onSendSubmit(zecSend)
},
isSending = false,
modifier = modifier
)
}
SendConfirmationStage.Sending -> {
SendConfirmationContent(
zecSend = zecSend,
onBack = onBack,
onConfirmation = {},
isSending = true,
modifier = modifier
)
}
is SendConfirmationStage.Failure -> {
SendFailure(
onDone = onBack,
reason = stage.error,
)
}
is SendConfirmationStage.MultipleTrxFailure -> {
// TODO [#1161]: Remove Send-Success and rework Send-Failure
// TODO [#1161]: https://github.com/Electric-Coin-Company/zashi-android/issues/1161
SendFailure(
onDone = onBack,
reason = stage.error,
)
}
}
}
const val DEFAULT_LESS_THAN_FEE = 100_000L
@Composable
@Suppress("LongMethod")
private fun SendConfirmationContent(
zecSend: ZecSend,
onConfirmation: () -> Unit,
onBack: () -> Unit,
isSending: Boolean,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.Start,
modifier =
modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Small(stringResource(R.string.send_confirmation_amount))
BalanceWidgetBigLineOnly(text = zecSend.amount.toZecString())
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Small(stringResource(R.string.send_confirmation_address))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
Tiny(zecSend.destination.address)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
Small(stringResource(R.string.send_confirmation_fee))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
StyledBalance(
balanceString =
if (zecSend.proposal == null) {
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
} else {
// 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(zecSend.proposal)
zecSend.proposal!!.totalFeeRequired().toZecString()
},
textStyles =
Pair(
ZcashTheme.extendedTypography.balanceSingleStyles.first,
ZcashTheme.extendedTypography.balanceSingleStyles.second
)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
if (zecSend.memo.value.isNotEmpty()) {
Small(stringResource(R.string.send_confirmation_memo))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
Box(
modifier =
Modifier
.fillMaxWidth()
.border(width = 1.dp, color = ZcashTheme.colors.textFieldFrame)
) {
Tiny(
text = zecSend.memo.value,
modifier =
Modifier
.fillMaxWidth()
.padding(all = ZcashTheme.dimens.spacingMid)
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
}
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
SendConfirmationActionButtons(
isSending = isSending,
onBack = onBack,
onConfirmation = onConfirmation
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
}
}
const val BUTTON_WIDTH_RATIO = 0.5f
@Composable
fun SendConfirmationActionButtons(
onConfirmation: () -> Unit,
onBack: () -> Unit,
isSending: Boolean,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
) {
PrimaryButton(
text = stringResource(id = R.string.send_confirmation_send_button),
onClick = onConfirmation,
enabled = !isSending,
showProgressBar = isSending,
minHeight = ZcashTheme.dimens.buttonHeightSmall,
modifier =
Modifier
.testTag(SendConfirmationTag.SEND_CONFIRMATION_SEND_BUTTON)
.weight(BUTTON_WIDTH_RATIO)
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingLarge))
PrimaryButton(
text = stringResource(R.string.send_confirmation_back_button),
onClick = onBack,
enabled = !isSending,
minHeight = ZcashTheme.dimens.buttonHeightSmall,
modifier =
Modifier
.testTag(SendConfirmationTag.SEND_CONFIRMATION_BACK_BUTTON)
.weight(BUTTON_WIDTH_RATIO)
)
}
}
@Composable
@Suppress("UNUSED_PARAMETER")
private fun SendFailure(
onDone: () -> Unit,
reason: String,
) {
// Once we ensure that the [reason] contains a localized message, we can leverage it for the UI prompt
AppAlertDialog(
title = stringResource(id = R.string.send_confirmation_dialog_error_title),
text = stringResource(id = R.string.send_confirmation_dialog_error_text),
confirmButtonText = stringResource(id = R.string.send_confirmation_dialog_error_btn),
onConfirmButtonClick = onDone
)
}

View File

@ -234,14 +234,16 @@ private fun SettingsMainContent(
) {
PrimaryButton(
onClick = onFeedback,
text = stringResource(R.string.settings_send_us_feedback)
text = stringResource(R.string.settings_send_us_feedback),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onAdvancedSettings,
text = stringResource(R.string.settings_advanced_settings)
text = stringResource(R.string.settings_advanced_settings),
modifier = Modifier.fillMaxWidth()
)
Spacer(
@ -255,7 +257,8 @@ private fun SettingsMainContent(
PrimaryButton(
onClick = onAbout,
text = stringResource(R.string.settings_about)
text = stringResource(R.string.settings_about),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingHuge))

View File

@ -165,7 +165,8 @@ private fun SupportMainContent(
PrimaryButton(
onClick = { setShowDialog(true) },
text = stringResource(id = R.string.support_send)
text = stringResource(id = R.string.support_send),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))

View File

@ -149,9 +149,12 @@ private fun UpdateBottomAppBar(
PrimaryButton(
onClick = { onDownload(UpdateState.Running) },
text = stringResource(R.string.update_download_button),
modifier = Modifier.testTag(UpdateTag.BTN_DOWNLOAD),
modifier =
Modifier
.testTag(UpdateTag.BTN_DOWNLOAD)
.fillMaxWidth(),
enabled = updateInfo.state != UpdateState.Running,
outerPaddingValues = PaddingValues(all = ZcashTheme.dimens.spacingNone)
outerPaddingValues = PaddingValues(all = ZcashTheme.dimens.spacingNone),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))

View File

@ -1,5 +1,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="send_title">Send</string>
<string name="send_stage_send_title">Send</string>
<string name="send_back">Back</string>
<string name="send_back_content_description">Back</string>
<string name="send_scan_content_description">Scan</string>
@ -19,26 +19,10 @@
<string name="send_create">Review</string>
<string name="send_fee">(Typical Fee &lt; <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
<string name="send_confirmation_amount_format" formatted="true">Send: <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC</string>
<string name="send_confirmation_address_format" formatted="true">To: <xliff:g id="address" example="zs1g7cqw … mvyzgm">%1$s</xliff:g>?</string>
<string name="send_confirmation_memo_format" formatted="true">Memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>
<string name="send_confirmation_fee_format" formatted="true">Fee: <xliff:g id="fee" example="0.0001">%1$s</xliff:g></string>
<string name="send_confirmation_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g><xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
<string name="send_confirmation_button">Press to send ZEC</string>
<string name="send_dialog_error_title">Failed to send funds</string>
<string name="send_dialog_error_text">Error: The attempt to send funds failed. Try it again, please.</string>
<string name="send_dialog_error_btn">OK</string>
<string name="send_in_progress_amount_format" formatted="true">Sending <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to</string>
<string name="send_in_progress_memo_format" formatted="true">with a memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>
<string name="send_in_progress_wait">Please wait</string>
<string name="send_failure_title">Sending failure</string>
<string name="send_failure_amount_address_memo" formatted="true">Sending failed for:\n<xliff:g id="amount"
example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g> with a memo: <xliff:g id="memo" example="for Veronika">%3$s</xliff:g></string>
<string name="send_failure_reason" formatted="true">Sending failed with:\n<xliff:g
id="reason" example="Insufficient balance">%1$s</xliff:g></string>
<string name="send_failure_button">Back</string>
<string name="send_successful_title">Sending successful</string>
<string name="send_successful_amount_address_memo" formatted="true">Sending succeeded for: <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g> with a memo: <xliff:g id="memo" example="for Veronika">%3$s</xliff:g></string>
<string name="send_successful_button">Close</string>
<string name="send_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g><xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
</resources>

View File

@ -0,0 +1,15 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="send_stage_confirmation_title">Confirmation</string>
<string name="send_confirmation_amount">Amount:</string>
<string name="send_confirmation_address">To:</string>
<string name="send_confirmation_memo">Message</string>
<string name="send_confirmation_fee">Fee:</string>
<string name="send_confirmation_send_button">Send</string>
<string name="send_confirmation_back_button">Go Back</string>
<string name="send_confirmation_dialog_error_title">Failed to send funds</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>
</resources>

View File

@ -466,7 +466,7 @@ private fun sendZecScreenshots(
composeTestRule.activity.walletViewModel.walletSnapshot.value != null
}
composeTestRule.onNode(hasText(resContext.getString(R.string.send_title))).also {
composeTestRule.onNode(hasText(resContext.getString(R.string.send_stage_send_title))).also {
it.assertExists()
}