[#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
|
- A new Server switching screen was added. Its purpose is to enable switching between predefined and custom
|
||||||
lightwalletd servers in runtime.
|
lightwalletd servers in runtime.
|
||||||
- The About screen now contains a link to the new Zashi Privacy Policy website
|
- The About screen now contains a link to the new Zashi Privacy Policy website
|
||||||
|
- The Send Confirmation screen has been reworked according to the new design
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The Transaction History UI has been incorporated into the Account screen
|
- 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
|
### Fixed
|
||||||
- Button sizing has been updated to align with the design guidelines and preserve stretching if necessary
|
- 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_CORE_VERSION=1.9.0
|
||||||
ANDROIDX_ESPRESSO_VERSION=3.5.1
|
ANDROIDX_ESPRESSO_VERSION=3.5.1
|
||||||
ANDROIDX_LIFECYCLE_VERSION=2.6.2
|
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_PROFILE_INSTALLER_VERSION=1.3.1
|
||||||
ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha06
|
ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha06
|
||||||
ANDROIDX_SPLASH_SCREEN_VERSION=1.0.1
|
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.core)
|
||||||
implementation(libs.androidx.splash)
|
implementation(libs.androidx.splash)
|
||||||
implementation(libs.bundles.androidx.compose.core)
|
implementation(libs.bundles.androidx.compose.core)
|
||||||
|
implementation(libs.bundles.androidx.compose.extended)
|
||||||
implementation(libs.kotlin.stdlib)
|
implementation(libs.kotlin.stdlib)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
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.gestures.waitForUpOrCancellation
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
@ -57,6 +58,12 @@ private fun ButtonComposablePreview() {
|
||||||
TertiaryButton(onClick = { }, text = "Tertiary", enabled = false)
|
TertiaryButton(onClick = { }, text = "Tertiary", enabled = false)
|
||||||
NavigationButton(onClick = { }, text = "Navigation")
|
NavigationButton(onClick = { }, text = "Navigation")
|
||||||
DangerousButton(onClick = { }, text = "Dangerous")
|
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,
|
horizontal = ZcashTheme.dimens.spacingNone,
|
||||||
vertical = ZcashTheme.dimens.spacingSmall
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
),
|
),
|
||||||
contentPaddingValues: PaddingValues = PaddingValues(all = 16.dp)
|
contentPaddingValues: PaddingValues = PaddingValues(all = 14.dp)
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
|
@ -104,7 +111,6 @@ fun PrimaryButton(
|
||||||
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||||
)
|
)
|
||||||
.defaultMinSize(minWidth, minHeight)
|
.defaultMinSize(minWidth, minHeight)
|
||||||
.fillMaxWidth()
|
|
||||||
.border(1.dp, Color.Black)
|
.border(1.dp, Color.Black)
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
|
|
|
@ -222,7 +222,7 @@ fun Tiny(
|
||||||
overflow = overflow,
|
overflow = overflow,
|
||||||
textAlign = textAlign,
|
textAlign = textAlign,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ data class Dimens(
|
||||||
val spacingXtiny: Dp,
|
val spacingXtiny: Dp,
|
||||||
val spacingTiny: Dp,
|
val spacingTiny: Dp,
|
||||||
val spacingSmall: Dp,
|
val spacingSmall: Dp,
|
||||||
|
val spacingMid: Dp,
|
||||||
val spacingDefault: Dp,
|
val spacingDefault: Dp,
|
||||||
val spacingLarge: Dp,
|
val spacingLarge: Dp,
|
||||||
val spacingXlarge: Dp,
|
val spacingXlarge: Dp,
|
||||||
|
@ -58,6 +59,7 @@ private val defaultDimens =
|
||||||
spacingXtiny = 2.dp,
|
spacingXtiny = 2.dp,
|
||||||
spacingTiny = 4.dp,
|
spacingTiny = 4.dp,
|
||||||
spacingSmall = 8.dp,
|
spacingSmall = 8.dp,
|
||||||
|
spacingMid = 12.dp,
|
||||||
spacingDefault = 16.dp,
|
spacingDefault = 16.dp,
|
||||||
spacingLarge = 24.dp,
|
spacingLarge = 24.dp,
|
||||||
spacingXlarge = 32.dp,
|
spacingXlarge = 32.dp,
|
||||||
|
|
|
@ -253,7 +253,7 @@ val LocalExtendedTypography =
|
||||||
lineHeight = 20.sp
|
lineHeight = 20.sp
|
||||||
),
|
),
|
||||||
buttonText =
|
buttonText =
|
||||||
PrimaryTypography.bodySmall.copy(
|
PrimaryTypography.bodyMedium.copy(
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
),
|
),
|
||||||
buttonTextSmall =
|
buttonTextSmall =
|
||||||
|
|
|
@ -46,6 +46,7 @@ android {
|
||||||
"src/main/res/ui/scan",
|
"src/main/res/ui/scan",
|
||||||
"src/main/res/ui/seed_recovery",
|
"src/main/res/ui/seed_recovery",
|
||||||
"src/main/res/ui/send",
|
"src/main/res/ui/send",
|
||||||
|
"src/main/res/ui/send_confirmation",
|
||||||
"src/main/res/ui/settings",
|
"src/main/res/ui/settings",
|
||||||
"src/main/res/ui/support",
|
"src/main/res/ui/support",
|
||||||
"src/main/res/ui/update",
|
"src/main/res/ui/update",
|
||||||
|
|
|
@ -1,33 +1,20 @@
|
||||||
package co.electriccoin.zcash.ui.fixture
|
package co.electriccoin.zcash.ui.fixture
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
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.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.toZecString
|
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
|
|
||||||
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
||||||
|
|
||||||
internal object SendArgumentsWrapperFixture {
|
internal object SendArgumentsWrapperFixture {
|
||||||
val RECIPIENT_ADDRESS =
|
val RECIPIENT_ADDRESS =
|
||||||
ScanResult(
|
SerializableAddress(
|
||||||
address = WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified,
|
address = WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified,
|
||||||
type = AddressType.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: SerializableAddress? = RECIPIENT_ADDRESS) =
|
||||||
|
SendArgumentsWrapper(
|
||||||
fun new(
|
recipientAddress = recipientAddress?.toRecipient(),
|
||||||
recipientAddress: ScanResult? = RECIPIENT_ADDRESS,
|
)
|
||||||
amount: Zatoshi? = AMOUNT,
|
|
||||||
memo: String? = MEMO
|
|
||||||
) = SendArgumentsWrapper(
|
|
||||||
recipientAddress = recipientAddress?.toRecipient(),
|
|
||||||
amount = amountToFixtureZecString(amount),
|
|
||||||
memo = memo
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.test.performTextInput
|
||||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||||
import co.electriccoin.zcash.ui.R
|
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.getAppContext
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import co.electriccoin.zcash.ui.test.getStringResourceWithArgs
|
import co.electriccoin.zcash.ui.test.getStringResourceWithArgs
|
||||||
|
@ -94,8 +95,8 @@ internal fun ComposeContentTestRule.clickCreateAndSend() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ComposeContentTestRule.clickConfirmation() {
|
internal fun ComposeContentTestRule.dismissFailureDialog() {
|
||||||
onNodeWithTag(SendTag.SEND_CONFIRMATION_BUTTON).also {
|
onNodeWithText(getStringResource(R.string.send_dialog_error_btn)).also {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,25 +108,13 @@ internal fun ComposeContentTestRule.assertOnForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ComposeContentTestRule.assertOnConfirmation() {
|
internal fun ComposeContentTestRule.assertOnConfirmation() {
|
||||||
onNodeWithTag(SendTag.SEND_CONFIRMATION_BUTTON).also {
|
onNodeWithTag(SendConfirmationTag.SEND_CONFIRMATION_SEND_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 {
|
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ComposeContentTestRule.assertOnSendFailure() {
|
internal fun ComposeContentTestRule.assertOnSendFailure() {
|
||||||
onNodeWithText(getStringResource(R.string.send_failure_title)).also {
|
onNodeWithText(getStringResource(R.string.send_dialog_error_title)).also {
|
||||||
it.assertExists()
|
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.model.SendStage
|
||||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
@ -91,10 +90,8 @@ class SendViewTestSetup(
|
||||||
onBackCount.incrementAndGet()
|
onBackCount.incrementAndGet()
|
||||||
when (sendStage) {
|
when (sendStage) {
|
||||||
SendStage.Form -> {}
|
SendStage.Form -> {}
|
||||||
SendStage.Confirmation -> setSendStage(SendStage.Form)
|
SendStage.Proposing -> {}
|
||||||
SendStage.Sending -> {}
|
|
||||||
is SendStage.SendFailure -> setSendStage(SendStage.Form)
|
is SendStage.SendFailure -> setSendStage(SendStage.Form)
|
||||||
SendStage.SendSuccessful -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,17 +116,11 @@ class SendViewTestSetup(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
sendStage = sendStage,
|
sendStage = sendStage,
|
||||||
onSendStageChange = setSendStage,
|
|
||||||
onCreateZecSend = setZecSend,
|
onCreateZecSend = setZecSend,
|
||||||
zecSend = zecSend,
|
zecSend = zecSend,
|
||||||
focusManager = LocalFocusManager.current,
|
focusManager = LocalFocusManager.current,
|
||||||
onBack = onBackAction,
|
onBack = onBackAction,
|
||||||
onSettings = { onSettingsCount.incrementAndGet() },
|
onSettings = { onSettingsCount.incrementAndGet() },
|
||||||
onCreateAndSend = {
|
|
||||||
onCreateCount.incrementAndGet()
|
|
||||||
lastZecSend = it
|
|
||||||
mutableActionExecuted.update { true }
|
|
||||||
},
|
|
||||||
onQrScannerOpen = {
|
onQrScannerOpen = {
|
||||||
onScannerCount.incrementAndGet()
|
onScannerCount.incrementAndGet()
|
||||||
},
|
},
|
||||||
|
|
|
@ -77,7 +77,8 @@ class SendViewIntegrationTest {
|
||||||
goBalances = {},
|
goBalances = {},
|
||||||
hasCameraFeature = true,
|
hasCameraFeature = true,
|
||||||
goSettings = {},
|
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 cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
|
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.assertOnForm
|
||||||
import co.electriccoin.zcash.ui.screen.send.assertOnSendFailure
|
import co.electriccoin.zcash.ui.screen.send.assertOnSendFailure
|
||||||
import co.electriccoin.zcash.ui.screen.send.assertOnSendSuccessful
|
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
|
||||||
import co.electriccoin.zcash.ui.screen.send.assertOnSending
|
import co.electriccoin.zcash.ui.screen.send.dismissFailureDialog
|
||||||
import co.electriccoin.zcash.ui.screen.send.clickConfirmation
|
|
||||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -42,37 +40,18 @@ class SendViewAndroidTest : UiTestPrerequisites() {
|
||||||
fun back_on_sending_with_system_navigation_disabled_check() {
|
fun back_on_sending_with_system_navigation_disabled_check() {
|
||||||
val testSetup =
|
val testSetup =
|
||||||
newTestSetup(
|
newTestSetup(
|
||||||
SendStage.Confirmation,
|
SendStage.Form,
|
||||||
runBlocking { ZecSendFixture.new() }
|
runBlocking { ZecSendFixture.new() }
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
assertEquals(0, testSetup.getOnBackCount())
|
||||||
|
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnForm()
|
||||||
composeTestRule.clickConfirmation()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnSending()
|
|
||||||
|
|
||||||
Espresso.pressBack()
|
Espresso.pressBack()
|
||||||
|
|
||||||
composeTestRule.assertOnSending()
|
composeTestRule.assertOnForm()
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
assertEquals(1, testSetup.getOnBackCount())
|
assertEquals(1, testSetup.getOnBackCount())
|
||||||
}
|
}
|
||||||
|
@ -90,7 +69,7 @@ class SendViewAndroidTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.assertOnSendFailure()
|
composeTestRule.assertOnSendFailure()
|
||||||
|
|
||||||
Espresso.pressBack()
|
composeTestRule.dismissFailureDialog()
|
||||||
|
|
||||||
composeTestRule.assertOnForm()
|
composeTestRule.assertOnForm()
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import cash.z.ecc.android.sdk.ext.collectWith
|
import cash.z.ecc.android.sdk.ext.collectWith
|
||||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
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.R
|
||||||
import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture
|
import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture
|
||||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
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.SendViewTestSetup
|
||||||
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
|
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
|
||||||
import co.electriccoin.zcash.ui.screen.send.assertOnForm
|
import co.electriccoin.zcash.ui.screen.send.assertOnForm
|
||||||
import co.electriccoin.zcash.ui.screen.send.assertOnSendFailure
|
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.assertSendDisabled
|
||||||
import co.electriccoin.zcash.ui.screen.send.assertSendEnabled
|
import co.electriccoin.zcash.ui.screen.send.assertSendEnabled
|
||||||
import co.electriccoin.zcash.ui.screen.send.clickBack
|
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.clickCreateAndSend
|
||||||
import co.electriccoin.zcash.ui.screen.send.clickScanner
|
import co.electriccoin.zcash.ui.screen.send.clickScanner
|
||||||
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
|
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.model.SendStage
|
||||||
import co.electriccoin.zcash.ui.screen.send.setAmount
|
import co.electriccoin.zcash.ui.screen.send.setAmount
|
||||||
import co.electriccoin.zcash.ui.screen.send.setMemo
|
import co.electriccoin.zcash.ui.screen.send.setMemo
|
||||||
|
@ -95,7 +91,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
composeTestRule.setValidAddress()
|
composeTestRule.setValidAddress()
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
composeTestRule.clickConfirmation()
|
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -132,7 +127,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
composeTestRule.clickConfirmation()
|
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -191,7 +185,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
composeTestRule.clickConfirmation()
|
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -273,7 +266,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
composeTestRule.clickConfirmation()
|
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -324,60 +316,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
assertEquals(1, testSetup.getOnBackCount())
|
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
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun back_on_send_failure() {
|
fun back_on_send_failure() {
|
||||||
|
@ -390,7 +328,9 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
assertEquals(0, testSetup.getOnBackCount())
|
||||||
|
|
||||||
composeTestRule.assertOnSendFailure()
|
composeTestRule.assertOnSendFailure()
|
||||||
composeTestRule.clickBack()
|
|
||||||
|
composeTestRule.dismissFailureDialog()
|
||||||
|
|
||||||
composeTestRule.assertOnForm()
|
composeTestRule.assertOnForm()
|
||||||
|
|
||||||
assertEquals(1, testSetup.getOnBackCount())
|
assertEquals(1, testSetup.getOnBackCount())
|
||||||
|
@ -408,10 +348,9 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
assertEquals(0, testSetup.getOnBackCount())
|
||||||
|
|
||||||
composeTestRule.assertOnSendFailure()
|
composeTestRule.assertOnSendFailure()
|
||||||
composeTestRule.onNodeWithTag(SEND_FAILED_BUTTON).also {
|
|
||||||
it.assertExists()
|
composeTestRule.dismissFailureDialog()
|
||||||
it.performClick()
|
|
||||||
}
|
|
||||||
composeTestRule.assertOnForm()
|
composeTestRule.assertOnForm()
|
||||||
|
|
||||||
assertEquals(1, testSetup.getOnBackCount())
|
assertEquals(1, testSetup.getOnBackCount())
|
||||||
|
@ -451,20 +390,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
includeEditableText = true
|
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
|
@Test
|
||||||
|
|
|
@ -6,9 +6,11 @@ import androidx.navigation.NavOptionsBuilder
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_AMOUNT
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_MEMO
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_MEMO
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS
|
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.ABOUT
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.CHOOSE_SERVER
|
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.REQUEST
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.SCAN
|
import co.electriccoin.zcash.ui.NavigationTargets.SCAN
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
|
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.SETTINGS
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
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.ConfigurationEntries
|
||||||
import co.electriccoin.zcash.ui.configuration.RemoteConfig
|
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.about.WrapAbout
|
||||||
import co.electriccoin.zcash.ui.screen.advancedsettings.WrapAdvancedSettings
|
import co.electriccoin.zcash.ui.screen.advancedsettings.WrapAdvancedSettings
|
||||||
import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer
|
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.home.WrapHome
|
||||||
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
||||||
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
|
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
|
|
||||||
import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery
|
import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery
|
||||||
|
import co.electriccoin.zcash.ui.screen.send.ext.toSerializableAddress
|
||||||
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
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.settings.WrapSettings
|
||||||
import co.electriccoin.zcash.ui.screen.support.WrapSupport
|
import co.electriccoin.zcash.ui.screen.support.WrapSupport
|
||||||
import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate
|
import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate
|
||||||
import kotlinx.serialization.json.Json
|
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
|
@Composable
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
internal fun MainActivity.Navigation() {
|
internal fun MainActivity.Navigation() {
|
||||||
|
@ -44,30 +57,47 @@ internal fun MainActivity.Navigation() {
|
||||||
navControllerForTesting = it
|
navControllerForTesting = it
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = HOME) {
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = HOME,
|
||||||
|
enterTransition = { enterTransition() },
|
||||||
|
exitTransition = { exitTransition() },
|
||||||
|
popEnterTransition = { popEnterTransition() },
|
||||||
|
popExitTransition = { popExitTransition() }
|
||||||
|
) {
|
||||||
composable(HOME) { backStackEntry ->
|
composable(HOME) { backStackEntry ->
|
||||||
WrapHome(
|
WrapHome(
|
||||||
onPageChange = {
|
onPageChange = {
|
||||||
homeViewModel.screenIndex.value = it
|
homeViewModel.screenIndex.value = it
|
||||||
},
|
},
|
||||||
goBack = { finish() },
|
goBack = { finish() },
|
||||||
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
|
||||||
goScan = { navController.navigateJustOnce(SCAN) },
|
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
|
// At this point we only read scan result data
|
||||||
sendArgumentsWrapper =
|
sendArgumentsWrapper =
|
||||||
SendArgumentsWrapper(
|
SendArgumentsWrapper(
|
||||||
recipientAddress =
|
recipientAddress =
|
||||||
backStackEntry.savedStateHandle.get<String>(SEND_RECIPIENT_ADDRESS)?.let {
|
backStackEntry.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
|
||||||
Json.decodeFromString<ScanResult>(it).toRecipient()
|
Json.decodeFromString<SerializableAddress>(it).toRecipient()
|
||||||
},
|
},
|
||||||
amount = backStackEntry.savedStateHandle.get<String>(SEND_AMOUNT),
|
).also {
|
||||||
memo = backStackEntry.savedStateHandle.get<String>(SEND_MEMO)
|
// 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)) {
|
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
|
||||||
WrapCheckForUpdate()
|
WrapCheckForUpdate()
|
||||||
|
@ -135,11 +165,11 @@ internal fun MainActivity.Navigation() {
|
||||||
composable(SCAN) {
|
composable(SCAN) {
|
||||||
WrapScanValidator(
|
WrapScanValidator(
|
||||||
onScanValid = { scanResult ->
|
onScanValid = { scanResult ->
|
||||||
// At this point we only pass scan result data to recipient address
|
|
||||||
navController.previousBackStackEntry?.savedStateHandle?.apply {
|
navController.previousBackStackEntry?.savedStateHandle?.apply {
|
||||||
set(SEND_RECIPIENT_ADDRESS, Json.encodeToString(ScanResult.serializer(), scanResult))
|
set(
|
||||||
set(SEND_AMOUNT, null)
|
SEND_SCAN_RECIPIENT_ADDRESS,
|
||||||
set(SEND_MEMO, null)
|
Json.encodeToString(SerializableAddress.serializer(), scanResult)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
navController.popBackStackJustOnce(SCAN)
|
navController.popBackStackJustOnce(SCAN)
|
||||||
},
|
},
|
||||||
|
@ -152,6 +182,23 @@ internal fun MainActivity.Navigation() {
|
||||||
onConfirm = { navController.popBackStackJustOnce(EXPORT_PRIVATE_DATA) }
|
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 {
|
object NavigationArguments {
|
||||||
const val SEND_RECIPIENT_ADDRESS = "send_recipient_address"
|
const val SEND_SCAN_RECIPIENT_ADDRESS = "send_scan_recipient_address"
|
||||||
const val SEND_AMOUNT = "send_amount"
|
|
||||||
const val SEND_MEMO = "send_memo"
|
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 {
|
object NavigationTargets {
|
||||||
const val ABOUT = "about"
|
const val ABOUT = "about"
|
||||||
const val ACCOUNT = "account"
|
|
||||||
const val ADVANCED_SETTINGS = "advanced_settings"
|
const val ADVANCED_SETTINGS = "advanced_settings"
|
||||||
const val EXPORT_PRIVATE_DATA = "export_private_data"
|
const val EXPORT_PRIVATE_DATA = "export_private_data"
|
||||||
const val HOME = "home"
|
const val HOME = "home"
|
||||||
const val CHOOSE_SERVER = "choose_server"
|
const val CHOOSE_SERVER = "choose_server"
|
||||||
const val RECEIVE = "receive"
|
|
||||||
const val REQUEST = "request"
|
const val REQUEST = "request"
|
||||||
const val SCAN = "scan"
|
const val SCAN = "scan"
|
||||||
const val SEED_RECOVERY = "seed_recovery"
|
const val SEED_RECOVERY = "seed_recovery"
|
||||||
const val SEND = "send"
|
const val SEND_CONFIRMATION = "send_confirmation"
|
||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
const val SUPPORT = "support"
|
const val SUPPORT = "support"
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,6 @@ private fun BalanceWidgetPreview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Suppress("LongMethod")
|
|
||||||
fun BalanceWidget(
|
fun BalanceWidget(
|
||||||
walletSnapshot: WalletSnapshot,
|
walletSnapshot: WalletSnapshot,
|
||||||
isReferenceToBalances: Boolean,
|
isReferenceToBalances: Boolean,
|
||||||
|
@ -75,25 +74,7 @@ fun BalanceWidget(
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Row(
|
BalanceWidgetBigLineOnly(text = walletSnapshot.totalBalance().toZecString())
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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.preference.StandardPreferenceSingleton
|
||||||
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
|
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.TransactionOverviewExt
|
||||||
|
import co.electriccoin.zcash.ui.screen.account.state.getSortHeight
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -186,23 +187,32 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
synchronizer
|
synchronizer
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.flatMapLatest { synchronizer ->
|
.flatMapLatest { synchronizer ->
|
||||||
synchronizer.transactions
|
combine(
|
||||||
.combine(synchronizer.status) {
|
synchronizer.transactions,
|
||||||
transactions: List<TransactionOverview>, status: Synchronizer.Status ->
|
synchronizer.status,
|
||||||
val enhancedTransactions =
|
synchronizer.networkHeight.filterNotNull()
|
||||||
transactions.map {
|
) {
|
||||||
|
transactions: List<TransactionOverview>,
|
||||||
|
status: Synchronizer.Status,
|
||||||
|
networkHeight: BlockHeight ->
|
||||||
|
val enhancedTransactions =
|
||||||
|
transactions
|
||||||
|
.sortedByDescending {
|
||||||
|
it.getSortHeight(networkHeight)
|
||||||
|
}
|
||||||
|
.map {
|
||||||
if (it.isSentTransaction) {
|
if (it.isSentTransaction) {
|
||||||
TransactionOverviewExt(it, synchronizer.getRecipients(it).firstOrNull())
|
TransactionOverviewExt(it, synchronizer.getRecipients(it).firstOrNull())
|
||||||
} else {
|
} else {
|
||||||
TransactionOverviewExt(it, null)
|
TransactionOverviewExt(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status.isSyncing()) {
|
if (status.isSyncing()) {
|
||||||
TransactionHistorySyncState.Syncing(enhancedTransactions.toPersistentList())
|
TransactionHistorySyncState.Syncing(enhancedTransactions.toPersistentList())
|
||||||
} else {
|
} else {
|
||||||
TransactionHistorySyncState.Done(enhancedTransactions.toPersistentList())
|
TransactionHistorySyncState.Done(enhancedTransactions.toPersistentList())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package co.electriccoin.zcash.ui.screen.account.state
|
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.TransactionOverview
|
||||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
|
|
||||||
|
@ -7,3 +8,13 @@ data class TransactionOverviewExt(
|
||||||
val overview: TransactionOverview,
|
val overview: TransactionOverview,
|
||||||
val recipient: TransactionRecipient?
|
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
|
// * 1000 to covert to millis
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
dateFormat.format(blockTimeEpochSeconds.times(1000))
|
dateFormat.format(blockTimeEpochSeconds.times(1000))
|
||||||
} ?: "-"
|
} ?: ""
|
||||||
} ?: "-"
|
} ?: ""
|
||||||
// For now, use the same label for the above missing transaction date
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = dateString,
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
@ -97,21 +98,24 @@ private fun AdvancedSettingsMainContent(
|
||||||
) {
|
) {
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onSeedRecovery,
|
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))
|
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onExportPrivateData,
|
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))
|
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onChooseServer,
|
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))
|
Spacer(modifier = Modifier.height(dimens.spacingHuge))
|
||||||
|
|
|
@ -151,7 +151,9 @@ internal fun WrapBalances(
|
||||||
synchronizer.createProposedTransactions(
|
synchronizer.createProposedTransactions(
|
||||||
proposal = newProposal,
|
proposal = newProposal,
|
||||||
usk = spendingKey
|
usk = spendingKey
|
||||||
)
|
).collect {
|
||||||
|
Twig.info { "Printing only for now. Will be reworked. Result: $it" }
|
||||||
|
}
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Twig.debug { "Shielding transaction event" }
|
Twig.debug { "Shielding transaction event" }
|
||||||
setShieldState(ShieldState.None)
|
setShieldState(ShieldState.None)
|
||||||
|
|
|
@ -78,7 +78,7 @@ import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues
|
||||||
|
|
||||||
@Preview("Balances")
|
@Preview("Balances")
|
||||||
@Composable
|
@Composable
|
||||||
private fun ComposablePreview() {
|
private fun ComposableBalancesPreview() {
|
||||||
ZcashTheme(forceDarkMode = false) {
|
ZcashTheme(forceDarkMode = false) {
|
||||||
GradientSurface {
|
GradientSurface {
|
||||||
Balances(
|
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")
|
@Suppress("LongParameterList")
|
||||||
@Composable
|
@Composable
|
||||||
fun Balances(
|
fun Balances(
|
||||||
|
@ -273,6 +293,7 @@ fun TransparentBalancePanel(
|
||||||
textStyle = ZcashTheme.extendedTypography.buttonTextSmall,
|
textStyle = ZcashTheme.extendedTypography.buttonTextSmall,
|
||||||
enabled = shieldState == ShieldState.Available,
|
enabled = shieldState == ShieldState.Available,
|
||||||
minHeight = ZcashTheme.dimens.buttonHeightSmall,
|
minHeight = ZcashTheme.dimens.buttonHeightSmall,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
outerPaddingValues =
|
outerPaddingValues =
|
||||||
PaddingValues(
|
PaddingValues(
|
||||||
horizontal = 54.dp,
|
horizontal = 54.dp,
|
||||||
|
|
|
@ -338,7 +338,10 @@ fun SaveButton(
|
||||||
|
|
||||||
onServerChange(selectedServer)
|
onServerChange(selectedServer)
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier =
|
||||||
|
modifier.then(
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,8 @@ private fun ExportPrivateDataContent(
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onConfirm,
|
onClick = onConfirm,
|
||||||
text = stringResource(R.string.export_data_confirm).uppercase(),
|
text = stringResource(R.string.export_data_confirm).uppercase(),
|
||||||
enabled = checkedState.value
|
enabled = checkedState.value,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(ZcashTheme.dimens.spacingHuge))
|
Spacer(Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.activity.viewModels
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.res.stringResource
|
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.MainActivity
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||||
|
@ -23,11 +24,13 @@ import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal fun MainActivity.WrapHome(
|
internal fun MainActivity.WrapHome(
|
||||||
onPageChange: (HomeScreenIndex) -> Unit,
|
onPageChange: (HomeScreenIndex) -> Unit,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
goSettings: () -> Unit,
|
goSettings: () -> Unit,
|
||||||
goScan: () -> Unit,
|
goScan: () -> Unit,
|
||||||
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
sendArgumentsWrapper: SendArgumentsWrapper
|
sendArgumentsWrapper: SendArgumentsWrapper
|
||||||
) {
|
) {
|
||||||
WrapHome(
|
WrapHome(
|
||||||
|
@ -35,6 +38,7 @@ internal fun MainActivity.WrapHome(
|
||||||
onPageChange = onPageChange,
|
onPageChange = onPageChange,
|
||||||
goBack = goBack,
|
goBack = goBack,
|
||||||
goScan = goScan,
|
goScan = goScan,
|
||||||
|
goSendConfirmation = goSendConfirmation,
|
||||||
goSettings = goSettings,
|
goSettings = goSettings,
|
||||||
sendArgumentsWrapper = sendArgumentsWrapper
|
sendArgumentsWrapper = sendArgumentsWrapper
|
||||||
)
|
)
|
||||||
|
@ -47,6 +51,7 @@ internal fun WrapHome(
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
goSettings: () -> Unit,
|
goSettings: () -> Unit,
|
||||||
goScan: () -> Unit,
|
goScan: () -> Unit,
|
||||||
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
onPageChange: (HomeScreenIndex) -> Unit,
|
onPageChange: (HomeScreenIndex) -> Unit,
|
||||||
sendArgumentsWrapper: SendArgumentsWrapper
|
sendArgumentsWrapper: SendArgumentsWrapper
|
||||||
) {
|
) {
|
||||||
|
@ -98,6 +103,7 @@ internal fun WrapHome(
|
||||||
goToQrScanner = goScan,
|
goToQrScanner = goScan,
|
||||||
goBack = homeGoBack,
|
goBack = homeGoBack,
|
||||||
goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) },
|
goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) },
|
||||||
|
goSendConfirmation = goSendConfirmation,
|
||||||
goSettings = goSettings,
|
goSettings = goSettings,
|
||||||
sendArgumentsWrapper = sendArgumentsWrapper
|
sendArgumentsWrapper = sendArgumentsWrapper
|
||||||
)
|
)
|
||||||
|
|
|
@ -210,6 +210,7 @@ private fun NewWalletRecoveryMainContent(
|
||||||
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
|
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
|
||||||
end = ZcashTheme.dimens.screenHorizontalSpacingBig
|
end = ZcashTheme.dimens.screenHorizontalSpacingBig
|
||||||
)
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,7 +200,8 @@ private fun OnboardingMainContent(
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onCreateWallet,
|
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))
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
@ -161,7 +162,8 @@ private fun RequestMainContent(
|
||||||
onCreateAndSend(ZecRequest(myAddress, zatoshi!!, ZecRequestMessage(message)))
|
onCreateAndSend(ZecRequest(myAddress, zatoshi!!, ZecRequestMessage(message)))
|
||||||
},
|
},
|
||||||
text = stringResource(id = R.string.request_create),
|
text = stringResource(id = R.string.request_create),
|
||||||
enabled = null != zatoshi
|
enabled = null != zatoshi,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
|
|
|
@ -397,7 +397,8 @@ private fun RestoreSeedMainContent(
|
||||||
onClick = goNext,
|
onClick = goNext,
|
||||||
enabled = isSeedValid,
|
enabled = isSeedValid,
|
||||||
text = stringResource(id = R.string.restore_seed_button_next),
|
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))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
|
@ -771,7 +772,8 @@ private fun RestoreBirthdayMainContent(
|
||||||
onDone()
|
onDone()
|
||||||
},
|
},
|
||||||
text = stringResource(R.string.restore_birthday_button_restore),
|
text = stringResource(R.string.restore_birthday_button_restore),
|
||||||
enabled = isBirthdayValid
|
enabled = isBirthdayValid,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
|
|
|
@ -9,16 +9,16 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import co.electriccoin.zcash.ui.MainActivity
|
import co.electriccoin.zcash.ui.MainActivity
|
||||||
import co.electriccoin.zcash.ui.R
|
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.common.viewmodel.WalletViewModel
|
||||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
|
|
||||||
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
|
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
|
||||||
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MainActivity.WrapScanValidator(
|
internal fun MainActivity.WrapScanValidator(
|
||||||
onScanValid: (address: ScanResult) -> Unit,
|
onScanValid: (address: SerializableAddress) -> Unit,
|
||||||
goBack: () -> Unit
|
goBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
WrapScan(
|
WrapScan(
|
||||||
|
@ -31,7 +31,7 @@ internal fun MainActivity.WrapScanValidator(
|
||||||
@Composable
|
@Composable
|
||||||
fun WrapScan(
|
fun WrapScan(
|
||||||
activity: ComponentActivity,
|
activity: ComponentActivity,
|
||||||
onScanValid: (address: ScanResult) -> Unit,
|
onScanValid: (address: SerializableAddress) -> Unit,
|
||||||
goBack: () -> Unit
|
goBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
val walletViewModel by activity.viewModels<WalletViewModel>()
|
||||||
|
@ -55,7 +55,7 @@ fun WrapScan(
|
||||||
val addressType = synchronizer.validateAddress(result)
|
val addressType = synchronizer.validateAddress(result)
|
||||||
val isAddressValid = !addressType.isNotValid
|
val isAddressValid = !addressType.isNotValid
|
||||||
if (isAddressValid) {
|
if (isAddressValid) {
|
||||||
onScanValid(ScanResult(result, addressType))
|
onScanValid(SerializableAddress(result, addressType))
|
||||||
} else {
|
} else {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = activity.getString(R.string.scan_validation_invalid_address)
|
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(
|
PrimaryButton(
|
||||||
onClick = onConfirm,
|
onClick = onConfirm,
|
||||||
text = stringResource(R.string.security_warning_confirm).uppercase(),
|
text = stringResource(R.string.security_warning_confirm).uppercase(),
|
||||||
enabled = checkedState.value
|
enabled = checkedState.value,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
|
|
|
@ -220,6 +220,7 @@ private fun SeedRecoveryMainContent(
|
||||||
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
|
start = ZcashTheme.dimens.screenHorizontalSpacingBig,
|
||||||
end = ZcashTheme.dimens.screenHorizontalSpacingBig
|
end = ZcashTheme.dimens.screenHorizontalSpacingBig
|
||||||
)
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ internal fun WrapSend(
|
||||||
goToQrScanner: () -> Unit,
|
goToQrScanner: () -> Unit,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
goBalances: () -> Unit,
|
goBalances: () -> Unit,
|
||||||
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
goSettings: () -> Unit,
|
goSettings: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
|
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
|
||||||
|
@ -80,12 +81,13 @@ internal fun WrapSend(
|
||||||
goBack,
|
goBack,
|
||||||
goBalances,
|
goBalances,
|
||||||
goSettings,
|
goSettings,
|
||||||
|
goSendConfirmation,
|
||||||
hasCameraFeature,
|
hasCameraFeature,
|
||||||
monetarySeparators
|
monetarySeparators
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongParameterList", "LongMethod")
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@Composable
|
@Composable
|
||||||
internal fun WrapSend(
|
internal fun WrapSend(
|
||||||
|
@ -98,6 +100,7 @@ internal fun WrapSend(
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
goBalances: () -> Unit,
|
goBalances: () -> Unit,
|
||||||
goSettings: () -> Unit,
|
goSettings: () -> Unit,
|
||||||
|
goSendConfirmation: (ZecSend) -> Unit,
|
||||||
hasCameraFeature: Boolean,
|
hasCameraFeature: Boolean,
|
||||||
monetarySeparators: MonetarySeparators
|
monetarySeparators: MonetarySeparators
|
||||||
) {
|
) {
|
||||||
|
@ -105,11 +108,6 @@ internal fun WrapSend(
|
||||||
|
|
||||||
val context = LocalContext.current
|
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) =
|
val (sendStage, setSendStage) =
|
||||||
rememberSaveable(stateSaver = SendStage.Saver) { mutableStateOf(SendStage.Form) }
|
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:
|
// Memo computation:
|
||||||
val (memoState, setMemoState) =
|
val (memoState, setMemoState) =
|
||||||
rememberSaveable(stateSaver = MemoState.Saver) {
|
rememberSaveable(stateSaver = MemoState.Saver) {
|
||||||
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
|
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
|
||||||
}
|
}
|
||||||
if (sendArgumentsWrapper?.memo != null) {
|
|
||||||
setMemoState(MemoState.new(sendArgumentsWrapper.memo))
|
|
||||||
}
|
|
||||||
|
|
||||||
val onBackAction = {
|
val onBackAction = {
|
||||||
when (sendStage) {
|
when (sendStage) {
|
||||||
SendStage.Form -> goBack()
|
SendStage.Form -> goBack()
|
||||||
SendStage.Confirmation -> setSendStage(SendStage.Form)
|
SendStage.Proposing -> { /* no action - wait until the sending is done */ }
|
||||||
SendStage.Sending -> { /* no action - wait until the sending is done */ }
|
|
||||||
is SendStage.SendFailure -> setSendStage(SendStage.Form)
|
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(
|
Send(
|
||||||
walletSnapshot = walletSnapshot,
|
walletSnapshot = walletSnapshot,
|
||||||
sendStage = sendStage,
|
sendStage = sendStage,
|
||||||
onSendStageChange = setSendStage,
|
|
||||||
zecSend = zecSend,
|
zecSend = zecSend,
|
||||||
onCreateZecSend = { newZecSend ->
|
onCreateZecSend = { newZecSend ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Twig.debug { "Getting send transaction proposal" }
|
Twig.debug { "Getting send transaction proposal" }
|
||||||
runCatching {
|
runCatching {
|
||||||
synchronizer.proposeSend(spendingKey.account, newZecSend)
|
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,
|
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,
|
memoState = memoState,
|
||||||
setMemoState = setMemoState,
|
setMemoState = setMemoState,
|
||||||
amountState = amountState,
|
amountState = amountState,
|
||||||
|
|
|
@ -5,7 +5,6 @@ package co.electriccoin.zcash.ui.screen.send
|
||||||
*/
|
*/
|
||||||
object SendTag {
|
object SendTag {
|
||||||
const val SEND_FORM_BUTTON = "send_form_button"
|
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_FAILED_BUTTON = "send_failed_button"
|
||||||
const val SEND_SUCCESS_BUTTON = "send_success_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.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
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.R
|
||||||
|
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How far into the address will be abbreviation look forwards and backwards.
|
* 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 firstFive = address.substring(0, ABBREVIATION_INDEX)
|
||||||
val lastFive = address.substring(address.length - ABBREVIATION_INDEX, address.length)
|
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(
|
data class SendArgumentsWrapper(
|
||||||
val recipientAddress: RecipientAddressState? = null,
|
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 {
|
sealed class SendStage {
|
||||||
data object Form : SendStage()
|
data object Form : SendStage()
|
||||||
|
|
||||||
data object Confirmation : SendStage()
|
data object Proposing : SendStage()
|
||||||
|
|
||||||
data object Sending : SendStage()
|
|
||||||
|
|
||||||
data class SendFailure(val error: String) : SendStage()
|
data class SendFailure(val error: String) : SendStage()
|
||||||
|
|
||||||
data object SendSuccessful : SendStage()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TYPE_FORM = "form" // $NON-NLS
|
private const val TYPE_FORM = "form" // $NON-NLS
|
||||||
private const val TYPE_CONFIRMATION = "confirmation" // $NON-NLS
|
private const val TYPE_PROPOSING = "proposing" // $NON-NLS
|
||||||
private const val TYPE_SENDING = "sending" // $NON-NLS
|
|
||||||
private const val TYPE_FAILURE = "failure" // $NON-NLS
|
private const val TYPE_FAILURE = "failure" // $NON-NLS
|
||||||
private const val TYPE_SUCCESSFUL = "successful" // $NON-NLS
|
|
||||||
private const val KEY_TYPE = "type" // $NON-NLS
|
private const val KEY_TYPE = "type" // $NON-NLS
|
||||||
private const val KEY_ERROR = "error" // $NON-NLS
|
private const val KEY_ERROR = "error" // $NON-NLS
|
||||||
|
|
||||||
|
@ -34,10 +28,8 @@ sealed class SendStage {
|
||||||
val sendStageString = (it[KEY_TYPE] as String)
|
val sendStageString = (it[KEY_TYPE] as String)
|
||||||
when (sendStageString) {
|
when (sendStageString) {
|
||||||
TYPE_FORM -> Form
|
TYPE_FORM -> Form
|
||||||
TYPE_CONFIRMATION -> Confirmation
|
TYPE_PROPOSING -> Proposing
|
||||||
TYPE_SENDING -> Sending
|
|
||||||
TYPE_FAILURE -> SendFailure((it[KEY_ERROR] as String))
|
TYPE_FAILURE -> SendFailure((it[KEY_ERROR] as String))
|
||||||
TYPE_SUCCESSFUL -> SendSuccessful
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,13 +41,11 @@ sealed class SendStage {
|
||||||
val saverMap = HashMap<String, String>()
|
val saverMap = HashMap<String, String>()
|
||||||
when (this) {
|
when (this) {
|
||||||
Form -> saverMap[KEY_TYPE] = TYPE_FORM
|
Form -> saverMap[KEY_TYPE] = TYPE_FORM
|
||||||
Confirmation -> saverMap[KEY_TYPE] = TYPE_CONFIRMATION
|
Proposing -> saverMap[KEY_TYPE] = TYPE_PROPOSING
|
||||||
is SendFailure -> {
|
is SendFailure -> {
|
||||||
saverMap[KEY_TYPE] = TYPE_FAILURE
|
saverMap[KEY_TYPE] = TYPE_FAILURE
|
||||||
saverMap[KEY_ERROR] = this.error
|
saverMap[KEY_ERROR] = this.error
|
||||||
}
|
}
|
||||||
SendSuccessful -> saverMap[KEY_TYPE] = TYPE_SUCCESSFUL
|
|
||||||
Sending -> saverMap[KEY_TYPE] = TYPE_SENDING
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return saverMap
|
return saverMap
|
||||||
|
|
|
@ -5,7 +5,6 @@ package co.electriccoin.zcash.ui.screen.send.view
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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.Memo
|
||||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
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.ZecSendExt
|
||||||
import cash.z.ecc.android.sdk.model.toZecString
|
import cash.z.ecc.android.sdk.model.toZecString
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
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.fixture.ZatoshiFixture
|
||||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||||
import co.electriccoin.zcash.spackle.Twig
|
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.model.spendableBalance
|
||||||
import co.electriccoin.zcash.ui.common.test.CommonTag
|
import co.electriccoin.zcash.ui.common.test.CommonTag
|
||||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||||
|
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||||
import co.electriccoin.zcash.ui.design.component.Body
|
import co.electriccoin.zcash.ui.design.component.Body
|
||||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||||
import co.electriccoin.zcash.ui.design.component.FormTextField
|
import co.electriccoin.zcash.ui.design.component.FormTextField
|
||||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
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.PrimaryButton
|
||||||
|
import co.electriccoin.zcash.ui.design.component.Small
|
||||||
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
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.AmountState
|
||||||
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
||||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -85,13 +80,11 @@ private fun PreviewSendForm() {
|
||||||
Send(
|
Send(
|
||||||
walletSnapshot = WalletSnapshotFixture.new(),
|
walletSnapshot = WalletSnapshotFixture.new(),
|
||||||
sendStage = SendStage.Form,
|
sendStage = SendStage.Form,
|
||||||
onSendStageChange = {},
|
|
||||||
zecSend = null,
|
zecSend = null,
|
||||||
onCreateZecSend = {},
|
onCreateZecSend = {},
|
||||||
focusManager = LocalFocusManager.current,
|
focusManager = LocalFocusManager.current,
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onSettings = {},
|
onSettings = {},
|
||||||
onCreateAndSend = {},
|
|
||||||
onQrScannerOpen = {},
|
onQrScannerOpen = {},
|
||||||
goBalances = {},
|
goBalances = {},
|
||||||
hasCameraFeature = true,
|
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
|
@Composable
|
||||||
@Preview("SendFailure")
|
@Preview("SendFailure")
|
||||||
private fun PreviewSendFailure() {
|
private fun PreviewSendFailure() {
|
||||||
ZcashTheme(forceDarkMode = false) {
|
ZcashTheme(forceDarkMode = false) {
|
||||||
GradientSurface {
|
GradientSurface {
|
||||||
SendFailure(
|
SendFailure(
|
||||||
zecSend =
|
|
||||||
ZecSend(
|
|
||||||
destination = runBlocking { WalletAddressFixture.sapling() },
|
|
||||||
amount = ZatoshiFixture.new(),
|
|
||||||
memo = MemoFixture.new(),
|
|
||||||
proposal = null,
|
|
||||||
),
|
|
||||||
onDone = {},
|
onDone = {},
|
||||||
reason = "Insufficient balance"
|
reason = "Insufficient balance"
|
||||||
)
|
)
|
||||||
|
@ -145,37 +112,19 @@ private fun PreviewSendFailure() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
// TODO [#1260]: Cover Send screens UI with tests
|
||||||
@Preview("SendConfirmation")
|
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||||
private fun PreviewSendConfirmation() {
|
|
||||||
ZcashTheme(forceDarkMode = false) {
|
|
||||||
GradientSurface {
|
|
||||||
SendConfirmation(
|
|
||||||
zecSend =
|
|
||||||
ZecSend(
|
|
||||||
destination = runBlocking { WalletAddressFixture.sapling() },
|
|
||||||
amount = ZatoshiFixture.new(),
|
|
||||||
memo = MemoFixture.new(),
|
|
||||||
proposal = null,
|
|
||||||
),
|
|
||||||
onConfirmation = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
@Composable
|
@Composable
|
||||||
fun Send(
|
fun Send(
|
||||||
walletSnapshot: WalletSnapshot,
|
walletSnapshot: WalletSnapshot,
|
||||||
sendStage: SendStage,
|
sendStage: SendStage,
|
||||||
onSendStageChange: (SendStage) -> Unit,
|
|
||||||
zecSend: ZecSend?,
|
zecSend: ZecSend?,
|
||||||
onCreateZecSend: (ZecSend) -> Unit,
|
onCreateZecSend: (ZecSend) -> Unit,
|
||||||
focusManager: FocusManager,
|
focusManager: FocusManager,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSettings: () -> Unit,
|
onSettings: () -> Unit,
|
||||||
onCreateAndSend: (ZecSend) -> Unit,
|
|
||||||
onQrScannerOpen: () -> Unit,
|
onQrScannerOpen: () -> Unit,
|
||||||
goBalances: () -> Unit,
|
goBalances: () -> Unit,
|
||||||
hasCameraFeature: Boolean,
|
hasCameraFeature: Boolean,
|
||||||
|
@ -187,18 +136,13 @@ fun Send(
|
||||||
memoState: MemoState,
|
memoState: MemoState,
|
||||||
) {
|
) {
|
||||||
Scaffold(topBar = {
|
Scaffold(topBar = {
|
||||||
SendTopAppBar(
|
SendTopAppBar(onSettings = onSettings)
|
||||||
onBack = onBack,
|
|
||||||
onSettings = onSettings,
|
|
||||||
showBackNavigationButton = (sendStage != SendStage.Sending && sendStage != SendStage.Form)
|
|
||||||
)
|
|
||||||
}) { paddingValues ->
|
}) { paddingValues ->
|
||||||
SendMainContent(
|
SendMainContent(
|
||||||
walletSnapshot = walletSnapshot,
|
walletSnapshot = walletSnapshot,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
focusManager = focusManager,
|
focusManager = focusManager,
|
||||||
sendStage = sendStage,
|
sendStage = sendStage,
|
||||||
onSendStageChange = onSendStageChange,
|
|
||||||
zecSend = zecSend,
|
zecSend = zecSend,
|
||||||
onCreateZecSend = onCreateZecSend,
|
onCreateZecSend = onCreateZecSend,
|
||||||
recipientAddressState = recipientAddressState,
|
recipientAddressState = recipientAddressState,
|
||||||
|
@ -207,7 +151,6 @@ fun Send(
|
||||||
setAmountState = setAmountState,
|
setAmountState = setAmountState,
|
||||||
memoState = memoState,
|
memoState = memoState,
|
||||||
setMemoState = setMemoState,
|
setMemoState = setMemoState,
|
||||||
onSendSubmit = onCreateAndSend,
|
|
||||||
onQrScannerOpen = onQrScannerOpen,
|
onQrScannerOpen = onQrScannerOpen,
|
||||||
goBalances = goBalances,
|
goBalances = goBalances,
|
||||||
hasCameraFeature = hasCameraFeature,
|
hasCameraFeature = hasCameraFeature,
|
||||||
|
@ -224,21 +167,9 @@ fun Send(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SendTopAppBar(
|
private fun SendTopAppBar(onSettings: () -> Unit) {
|
||||||
onBack: () -> Unit,
|
|
||||||
onSettings: () -> Unit,
|
|
||||||
showBackNavigationButton: Boolean = true
|
|
||||||
) {
|
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
titleText = stringResource(id = R.string.send_title),
|
titleText = stringResource(id = R.string.send_stage_send_title),
|
||||||
onBack = onBack,
|
|
||||||
backText =
|
|
||||||
if (showBackNavigationButton) {
|
|
||||||
stringResource(id = R.string.send_back)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
backContentDescriptionText = stringResource(id = R.string.send_back_content_description),
|
|
||||||
hamburgerMenuActions = {
|
hamburgerMenuActions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onSettings,
|
onClick = onSettings,
|
||||||
|
@ -263,8 +194,6 @@ private fun SendMainContent(
|
||||||
zecSend: ZecSend?,
|
zecSend: ZecSend?,
|
||||||
onCreateZecSend: (ZecSend) -> Unit,
|
onCreateZecSend: (ZecSend) -> Unit,
|
||||||
sendStage: SendStage,
|
sendStage: SendStage,
|
||||||
onSendStageChange: (SendStage) -> Unit,
|
|
||||||
onSendSubmit: (ZecSend) -> Unit,
|
|
||||||
onQrScannerOpen: () -> Unit,
|
onQrScannerOpen: () -> Unit,
|
||||||
recipientAddressState: RecipientAddressState,
|
recipientAddressState: RecipientAddressState,
|
||||||
onRecipientAddressChange: (String) -> Unit,
|
onRecipientAddressChange: (String) -> Unit,
|
||||||
|
@ -276,7 +205,9 @@ private fun SendMainContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when {
|
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(
|
SendForm(
|
||||||
walletSnapshot = walletSnapshot,
|
walletSnapshot = walletSnapshot,
|
||||||
recipientAddressState = recipientAddressState,
|
recipientAddressState = recipientAddressState,
|
||||||
|
@ -293,40 +224,17 @@ private fun SendMainContent(
|
||||||
modifier = modifier
|
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) -> {
|
(sendStage is SendStage.SendFailure) -> {
|
||||||
SendFailure(
|
SendFailure(
|
||||||
zecSend = zecSend,
|
|
||||||
reason = sendStage.error,
|
reason = sendStage.error,
|
||||||
onDone = onBack,
|
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]: 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
|
// 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),
|
text = stringResource(id = R.string.send_create),
|
||||||
enabled = sendButtonEnabled,
|
enabled = sendButtonEnabled,
|
||||||
modifier = Modifier.testTag(SendTag.SEND_FORM_BUTTON)
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.testTag(SendTag.SEND_FORM_BUTTON)
|
||||||
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
@ -482,8 +393,7 @@ private fun SendForm(
|
||||||
id = R.string.send_fee,
|
id = R.string.send_fee,
|
||||||
// TODO [#1047]: Representing Zatoshi amount
|
// TODO [#1047]: Representing Zatoshi amount
|
||||||
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
|
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
|
||||||
@Suppress("MagicNumber")
|
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
|
||||||
Zatoshi(100_000L).toZecString()
|
|
||||||
),
|
),
|
||||||
textFontWeight = FontWeight.SemiBold
|
textFontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
|
@ -511,7 +421,7 @@ fun SendFormAddressTextField(
|
||||||
// Scroll TextField above ime keyboard
|
// Scroll TextField above ime keyboard
|
||||||
.bringIntoViewRequester(bringIntoViewRequester)
|
.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))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||||
|
|
||||||
|
@ -617,7 +527,7 @@ fun SendFormAmountTextField(
|
||||||
// Scroll TextField above ime keyboard
|
// Scroll TextField above ime keyboard
|
||||||
.bringIntoViewRequester(bringIntoViewRequester)
|
.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))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||||
|
|
||||||
|
@ -682,7 +592,7 @@ fun SendFormMemoTextField(
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.send_papre_plane),
|
painter = painterResource(id = R.drawable.send_paper_plane),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (isMemoFieldAvailable) {
|
if (isMemoFieldAvailable) {
|
||||||
|
@ -694,7 +604,7 @@ fun SendFormMemoTextField(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
|
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
|
||||||
|
|
||||||
Body(
|
Small(
|
||||||
text = stringResource(id = R.string.send_memo_label),
|
text = stringResource(id = R.string.send_memo_label),
|
||||||
color =
|
color =
|
||||||
if (isMemoFieldAvailable) {
|
if (isMemoFieldAvailable) {
|
||||||
|
@ -709,7 +619,12 @@ fun SendFormMemoTextField(
|
||||||
|
|
||||||
FormTextField(
|
FormTextField(
|
||||||
enabled = isMemoFieldAvailable,
|
enabled = isMemoFieldAvailable,
|
||||||
value = memoState.text,
|
value =
|
||||||
|
if (isMemoFieldAvailable) {
|
||||||
|
memoState.text
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
setMemoState(MemoState.new(it))
|
setMemoState(MemoState.new(it))
|
||||||
},
|
},
|
||||||
|
@ -762,238 +677,17 @@ fun SendFormMemoTextField(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SendConfirmation(
|
@Suppress("UNUSED_PARAMETER")
|
||||||
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
|
|
||||||
private fun SendFailure(
|
private fun SendFailure(
|
||||||
zecSend: ZecSend,
|
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
reason: String,
|
reason: String,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
// Once we ensure that the [reason] contains a localized message, we can leverage it for the UI prompt
|
||||||
modifier = modifier,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Header(
|
|
||||||
text = stringResource(R.string.send_failure_title),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(
|
AppAlertDialog(
|
||||||
modifier =
|
title = stringResource(id = R.string.send_dialog_error_title),
|
||||||
Modifier
|
text = stringResource(id = R.string.send_dialog_error_text),
|
||||||
.fillMaxWidth()
|
confirmButtonText = stringResource(id = R.string.send_dialog_error_btn),
|
||||||
.height(ZcashTheme.dimens.spacingDefault)
|
onConfirmButtonClick = onDone
|
||||||
)
|
)
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
PrimaryButton(
|
||||||
onClick = onFeedback,
|
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))
|
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onAdvancedSettings,
|
onClick = onAdvancedSettings,
|
||||||
text = stringResource(R.string.settings_advanced_settings)
|
text = stringResource(R.string.settings_advanced_settings),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
|
@ -255,7 +257,8 @@ private fun SettingsMainContent(
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onAbout,
|
onClick = onAbout,
|
||||||
text = stringResource(R.string.settings_about)
|
text = stringResource(R.string.settings_about),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(dimens.spacingHuge))
|
Spacer(modifier = Modifier.height(dimens.spacingHuge))
|
||||||
|
|
|
@ -165,7 +165,8 @@ private fun SupportMainContent(
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = { setShowDialog(true) },
|
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))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
|
|
|
@ -149,9 +149,12 @@ private fun UpdateBottomAppBar(
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = { onDownload(UpdateState.Running) },
|
onClick = { onDownload(UpdateState.Running) },
|
||||||
text = stringResource(R.string.update_download_button),
|
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,
|
enabled = updateInfo.state != UpdateState.Running,
|
||||||
outerPaddingValues = PaddingValues(all = ZcashTheme.dimens.spacingNone)
|
outerPaddingValues = PaddingValues(all = ZcashTheme.dimens.spacingNone),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">Back</string>
|
||||||
<string name="send_back_content_description">Back</string>
|
<string name="send_back_content_description">Back</string>
|
||||||
<string name="send_scan_content_description">Scan</string>
|
<string name="send_scan_content_description">Scan</string>
|
||||||
|
@ -19,26 +19,10 @@
|
||||||
<string name="send_create">Review</string>
|
<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_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_dialog_error_title">Failed to send funds</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_dialog_error_text">Error: The attempt to send funds failed. Try it again, please.</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_dialog_error_btn">OK</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_in_progress_amount_format" formatted="true">Sending <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to</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>
|
||||||
<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>
|
|
||||||
|
|
||||||
</resources>
|
</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.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()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue