[#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:
parent
24cd22186f
commit
3845772071
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
)
|
|
@ -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 =
|
||||
|
|
|
@ -222,7 +222,7 @@ fun Tiny(
|
|||
overflow = overflow,
|
||||
textAlign = textAlign,
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -253,7 +253,7 @@ val LocalExtendedTypography =
|
|||
lineHeight = 20.sp
|
||||
),
|
||||
buttonText =
|
||||
PrimaryTypography.bodySmall.copy(
|
||||
PrimaryTypography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Medium
|
||||
),
|
||||
buttonTextSmall =
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -77,7 +77,8 @@ class SendViewIntegrationTest {
|
|||
goBalances = {},
|
||||
hasCameraFeature = true,
|
||||
goSettings = {},
|
||||
monetarySeparators = monetarySeparators
|
||||
monetarySeparators = monetarySeparators,
|
||||
goSendConfirmation = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -338,7 +338,10 @@ fun SaveButton(
|
|||
|
||||
onServerChange(selectedServer)
|
||||
},
|
||||
modifier = modifier
|
||||
modifier =
|
||||
modifier.then(
|
||||
Modifier.fillMaxWidth()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -210,6 +210,7 @@ private fun NewWalletRecoveryMainContent(
|
|||
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
|
||||
end = ZcashTheme.dimens.screenHorizontalSpacingBig
|
||||
)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -220,6 +220,7 @@ private fun SeedRecoveryMainContent(
|
|||
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
|
||||
end = ZcashTheme.dimens.screenHorizontalSpacingBig
|
||||
)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 ?: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 < <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>
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue