[#1144] Send.Form screen rework
* [#1144] Send.Form screen rework - This follows the new Figma design. It also adds the TextFields values validation and proper UI reactions. - Closes #340 - Closes #810 - Closes #1157 - Closes #1158 - Closes #1253 - Closes #1254 - Closes #826 - Follow-ups: #1047, #1257 * Changelog update
This commit is contained in:
parent
4bba077fbd
commit
7285137f2e
|
@ -14,6 +14,10 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
unmetered connection and is plugged into the power, the background task will start to synchronize blocks randomly
|
||||
between 3 and 4 a.m.
|
||||
|
||||
### Changed
|
||||
- The Send screen form has changed its UI to align with the Figma design. All the form fields provide validations
|
||||
and proper UI response.
|
||||
|
||||
## [0.2.0 (554)] - 2024-02-13
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -179,6 +179,7 @@ KOTLIN_VERSION=1.9.21
|
|||
KOTLINX_COROUTINES_VERSION=1.7.3
|
||||
KOTLINX_DATETIME_VERSION=0.5.0
|
||||
KOTLINX_IMMUTABLE_COLLECTIONS_VERSION=0.3.6
|
||||
KOTLINX_SERIALIZABLE_JSON_VERSION=1.6.3
|
||||
KOVER_VERSION=0.7.3
|
||||
PLAY_APP_UPDATE_VERSION=2.1.0
|
||||
PLAY_APP_UPDATE_KTX_VERSION=2.1.0
|
||||
|
|
|
@ -65,6 +65,7 @@ pluginManagement {
|
|||
kotlin("android") version (kotlinVersion) apply (false)
|
||||
kotlin("jvm") version (kotlinVersion)
|
||||
kotlin("multiplatform") version (kotlinVersion)
|
||||
kotlin("plugin.serialization") version (kotlinVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,6 +176,7 @@ dependencyResolutionManagement {
|
|||
val kotlinxDateTimeVersion = extra["KOTLINX_DATETIME_VERSION"].toString()
|
||||
val kotlinxCoroutinesVersion = extra["KOTLINX_COROUTINES_VERSION"].toString()
|
||||
val kotlinxImmutableCollectionsVersion = extra["KOTLINX_IMMUTABLE_COLLECTIONS_VERSION"].toString()
|
||||
val kotlinxSerializableJsonVersion = extra["KOTLINX_SERIALIZABLE_JSON_VERSION"].toString()
|
||||
val playAppUpdateVersion = extra["PLAY_APP_UPDATE_VERSION"].toString()
|
||||
val playAppUpdateKtxVersion = extra["PLAY_APP_UPDATE_KTX_VERSION"].toString()
|
||||
val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
|
||||
|
@ -227,6 +229,7 @@ dependencyResolutionManagement {
|
|||
library("kotlinx-coroutines-guava", "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$kotlinxCoroutinesVersion")
|
||||
library("kotlinx-datetime", "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
||||
library("kotlinx-immutable", "org.jetbrains.kotlinx:kotlinx-collections-immutable:$kotlinxImmutableCollectionsVersion")
|
||||
library("kotlinx-serializable-json", "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializableJsonVersion")
|
||||
library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion")
|
||||
library("play-update-ktx", "com.google.android.play:app-update-ktx:$playAppUpdateKtxVersion")
|
||||
library("zcash-sdk", "cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
||||
|
|
|
@ -2,17 +2,16 @@ package co.electriccoin.zcash.ui.design.component
|
|||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
|
@ -20,6 +19,8 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -31,40 +32,53 @@ fun FormTextField(
|
|||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
error: String? = null,
|
||||
enabled: Boolean = true,
|
||||
textStyle: TextStyle = ZcashTheme.extendedTypography.textFieldValue,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
placeholder:
|
||||
@Composable()
|
||||
(() -> Unit)? = null,
|
||||
leadingIcon:
|
||||
@Composable()
|
||||
(() -> Unit)? = null,
|
||||
trailingIcon:
|
||||
@Composable()
|
||||
(() -> Unit)? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
colors: TextFieldColors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContainerColor = ZcashTheme.colors.textDisabled,
|
||||
errorContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent
|
||||
),
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
// To enable border around the TextField
|
||||
withBorder: Boolean = true,
|
||||
bringIntoViewRequester: BringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
bringIntoViewRequester: BringIntoViewRequester? = null,
|
||||
minHeight: Dp = ZcashTheme.dimens.textFieldDefaultHeight,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val composedModifier =
|
||||
val composedTextFieldModifier =
|
||||
modifier
|
||||
.defaultMinSize(minHeight = ZcashTheme.dimens.textFieldDefaultHeight)
|
||||
.defaultMinSize(minHeight = minHeight)
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
coroutineScope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
bringIntoViewRequester?.run {
|
||||
if (focusState.isFocused) {
|
||||
coroutineScope.launch {
|
||||
bringIntoView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
.then(
|
||||
if (withBorder) {
|
||||
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
|
||||
modifier.border(width = 1.dp, color = ZcashTheme.colors.textFieldFrame)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
@ -73,14 +87,31 @@ fun FormTextField(
|
|||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = label,
|
||||
placeholder =
|
||||
if (enabled) {
|
||||
placeholder
|
||||
} else {
|
||||
null
|
||||
},
|
||||
textStyle = textStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
colors = colors,
|
||||
modifier = composedModifier,
|
||||
modifier = composedTextFieldModifier,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
keyboardActions = keyboardActions,
|
||||
shape = shape
|
||||
shape = shape,
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
if (!error.isNullOrEmpty()) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
BodySmall(
|
||||
text = error,
|
||||
color = ZcashTheme.colors.textFieldError,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,8 @@ data class Dimens(
|
|||
val topAppBarZcashLogoHeight: Dp,
|
||||
// TextField:
|
||||
val textFieldDefaultHeight: Dp,
|
||||
val textFieldPanelDefaultHeight: Dp,
|
||||
val textFieldSeedPanelDefaultHeight: Dp,
|
||||
val textFieldMemoPanelDefaultHeight: Dp,
|
||||
// Any Layout:
|
||||
val divider: Dp,
|
||||
val layoutStroke: Dp,
|
||||
|
@ -63,16 +64,17 @@ private val defaultDimens =
|
|||
buttonShadowOffsetX = 20.dp,
|
||||
buttonShadowOffsetY = 20.dp,
|
||||
buttonShadowSpread = 10.dp,
|
||||
buttonWidth = 230.dp,
|
||||
buttonHeight = 50.dp,
|
||||
buttonWidth = 244.dp,
|
||||
buttonHeight = 70.dp,
|
||||
chipShadowElevation = 4.dp,
|
||||
chipStroke = 0.5.dp,
|
||||
circularScreenProgressWidth = 48.dp,
|
||||
circularSmallProgressWidth = 14.dp,
|
||||
linearProgressHeight = 14.dp,
|
||||
topAppBarZcashLogoHeight = 24.dp,
|
||||
textFieldDefaultHeight = 64.dp,
|
||||
textFieldPanelDefaultHeight = 215.dp,
|
||||
textFieldDefaultHeight = 40.dp,
|
||||
textFieldSeedPanelDefaultHeight = 215.dp,
|
||||
textFieldMemoPanelDefaultHeight = 140.dp,
|
||||
layoutStroke = 1.dp,
|
||||
divider = 1.dp,
|
||||
regularRippleEffectCorner = 28.dp,
|
||||
|
|
|
@ -20,7 +20,10 @@ data class ExtendedColors(
|
|||
val linearProgressBarBackground: Color,
|
||||
val chipIndex: Color,
|
||||
val textCommon: Color,
|
||||
val textDisabled: Color,
|
||||
val textFieldHint: Color,
|
||||
val textFieldError: Color,
|
||||
val textFieldFrame: Color,
|
||||
val textDescription: Color,
|
||||
val textPending: Color,
|
||||
val layoutStroke: Color,
|
||||
|
|
|
@ -23,9 +23,11 @@ internal object Dark {
|
|||
val textPrimaryButton = Color(0xFF000000)
|
||||
val textSecondaryButton = Color(0xFF000000)
|
||||
val textTertiaryButton = Color.White
|
||||
val textNavigationButton = Color.Black
|
||||
val textCommon = Color(0xFFFFFFFF)
|
||||
val textDisabled = Color(0xFFB7B7B7)
|
||||
val textChipIndex = Color(0xFFFFB900)
|
||||
val textFieldFrame = Color(0xFF231F20)
|
||||
val textFieldError = Color(0xFFFF0000)
|
||||
val textFieldHint = Color(0xFFB7B7B7)
|
||||
val textDescription = Color(0xFF777777)
|
||||
val textProgress = Color(0xFF8B8A8A)
|
||||
|
@ -82,13 +84,15 @@ internal object Light {
|
|||
|
||||
val textHeaderOnBackground = Color(0xFF000000)
|
||||
val textBodyOnBackground = Color(0xFF000000)
|
||||
val textNavigationButton = Color(0xFFFFFFFF)
|
||||
val textPrimaryButton = Color(0xFFFFFFFF)
|
||||
val textSecondaryButton = Color(0xFF000000)
|
||||
val textTertiaryButton = Color(0xFF000000)
|
||||
val textCommon = Color(0xFF000000)
|
||||
val textChipIndex = Color(0xFFEE8592)
|
||||
val textDisabled = Color(0xFFB7B7B7)
|
||||
val textFieldFrame = Color(0xFF231F20)
|
||||
val textFieldError = Color(0xFFCD0002)
|
||||
val textFieldHint = Color(0xFFB7B7B7)
|
||||
val textChipIndex = Color(0xFFEE8592)
|
||||
val textDescription = Color(0xFF777777)
|
||||
val textProgress = Color(0xFF8B8A8A)
|
||||
|
||||
|
@ -175,6 +179,9 @@ internal val DarkExtendedColorPalette =
|
|||
linearProgressBarBackground = Dark.linearProgressBarBackground,
|
||||
chipIndex = Dark.textChipIndex,
|
||||
textCommon = Dark.textCommon,
|
||||
textDisabled = Dark.textDisabled,
|
||||
textFieldFrame = Dark.textFieldFrame,
|
||||
textFieldError = Dark.textFieldError,
|
||||
textFieldHint = Dark.textFieldHint,
|
||||
textDescription = Dark.textDescription,
|
||||
textPending = Dark.textProgress,
|
||||
|
@ -211,6 +218,9 @@ internal val LightExtendedColorPalette =
|
|||
linearProgressBarBackground = Light.linearProgressBarBackground,
|
||||
chipIndex = Light.textChipIndex,
|
||||
textCommon = Light.textCommon,
|
||||
textDisabled = Light.textDisabled,
|
||||
textFieldFrame = Light.textFieldFrame,
|
||||
textFieldError = Light.textFieldError,
|
||||
textFieldHint = Light.textFieldHint,
|
||||
textDescription = Light.textDescription,
|
||||
textPending = Light.textProgress,
|
||||
|
@ -249,7 +259,10 @@ internal val LocalExtendedColors =
|
|||
linearProgressBarBackground = Color.Unspecified,
|
||||
chipIndex = Color.Unspecified,
|
||||
textCommon = Color.Unspecified,
|
||||
textDisabled = Color.Unspecified,
|
||||
textFieldHint = Color.Unspecified,
|
||||
textFieldError = Color.Unspecified,
|
||||
textFieldFrame = Color.Unspecified,
|
||||
textDescription = Color.Unspecified,
|
||||
textPending = Color.Unspecified,
|
||||
layoutStroke = Color.Unspecified,
|
||||
|
|
|
@ -3,6 +3,7 @@ import com.android.build.api.variant.BuildConfigField
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization")
|
||||
id("secant.android-build-conventions")
|
||||
id("wtf.emulator.gradle")
|
||||
id("secant.emulator-wtf-conventions")
|
||||
|
@ -99,6 +100,7 @@ dependencies {
|
|||
implementation(libs.kotlinx.coroutines.guava)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.immutable)
|
||||
implementation(libs.kotlinx.serializable.json)
|
||||
implementation(libs.zcash.sdk)
|
||||
implementation(libs.zcash.sdk.incubator)
|
||||
implementation(libs.zcash.bip39)
|
||||
|
|
|
@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.common
|
|||
|
||||
import androidx.test.filters.FlakyTest
|
||||
import androidx.test.filters.SmallTest
|
||||
import co.electriccoin.zcash.ui.common.extension.throttle
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import co.electriccoin.zcash.ui.common.extension.first
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.contains
|
||||
|
|
|
@ -8,6 +8,9 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
|||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.compose.BrightenScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenBrightness
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenBrightness
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
|
@ -9,6 +9,11 @@ import androidx.compose.ui.test.junit4.createComposeRule
|
|||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.filters.SmallTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.SecureScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.isRunningTest
|
||||
import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
|
@ -8,6 +8,9 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
|||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenTimeout
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
|
@ -149,9 +149,7 @@ internal class MockSynchronizer : CloseableSynchronizer {
|
|||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun validateAddress(address: String): AddressType {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
override suspend fun validateAddress(address: String): AddressType = AddressType.Unified
|
||||
|
||||
override suspend fun validateConsensusBranch(): ConsensusMatchType {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
|
|
@ -4,23 +4,29 @@ import cash.z.ecc.android.sdk.fixture.WalletFixture
|
|||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
||||
|
||||
internal object SendArgumentsWrapperFixture {
|
||||
val RECIPIENT_ADDRESS = WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified
|
||||
val RECIPIENT_ADDRESS =
|
||||
ScanResult(
|
||||
address = WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified,
|
||||
type = AddressType.Unified
|
||||
)
|
||||
val MEMO = MemoFixture.new("Thanks for lunch").value
|
||||
val AMOUNT = ZatoshiFixture.new(1)
|
||||
|
||||
fun amountToFixtureZecString(amount: Zatoshi?) = amount?.toZecString()
|
||||
|
||||
fun new(
|
||||
recipientAddress: String? = RECIPIENT_ADDRESS,
|
||||
recipientAddress: ScanResult? = RECIPIENT_ADDRESS,
|
||||
amount: Zatoshi? = AMOUNT,
|
||||
memo: String? = MEMO
|
||||
) = SendArgumentsWrapper(
|
||||
recipientAddress = recipientAddress,
|
||||
recipientAddress = recipientAddress?.toRecipient(),
|
||||
amount = amountToFixtureZecString(amount),
|
||||
memo = memo
|
||||
)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances.integration
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertWidthIsAtLeast
|
||||
import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
|
@ -61,7 +60,7 @@ class BalancesViewIntegrationTest : UiTestPrerequisites() {
|
|||
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
|
||||
|
||||
composeTestRule.onNodeWithTag(BalancesTag.STATUS).also {
|
||||
it.assertIsDisplayed()
|
||||
it.assertExists()
|
||||
it.assertWidthIsAtLeast(1.dp)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import androidx.compose.ui.test.junit4.createComposeRule
|
|||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.LocalScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.ScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
|
@ -7,10 +7,10 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
|||
import androidx.compose.ui.test.performClick
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.LocalScreenBrightness
|
||||
import co.electriccoin.zcash.ui.common.LocalScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.ScreenBrightness
|
||||
import co.electriccoin.zcash.ui.common.ScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenBrightness
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenBrightness
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.model.VersionInfo
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
|
||||
|
|
|
@ -7,8 +7,8 @@ import androidx.test.filters.MediumTest
|
|||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.LocalScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.ScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.restore.state.RestoreState
|
||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||
|
|
|
@ -6,8 +6,8 @@ import androidx.compose.ui.test.junit4.createComposeRule
|
|||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.LocalScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.ScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity
|
||||
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
|
@ -10,11 +10,12 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import co.electriccoin.zcash.ui.test.getStringResourceWithArgs
|
||||
|
||||
internal fun ComposeContentTestRule.clickBack() {
|
||||
onNodeWithContentDescription(getStringResource(R.string.send_back_content_description)).also {
|
||||
|
@ -35,44 +36,52 @@ internal fun ComposeContentTestRule.clickScanner() {
|
|||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setValidAmount() {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
onNodeWithText(
|
||||
getStringResourceWithArgs(
|
||||
R.string.send_amount_hint,
|
||||
ZcashCurrency.fromResources(getAppContext()).name
|
||||
)
|
||||
).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput("123${separators.decimal}456")
|
||||
it.performTextInput(ZecSendFixture.AMOUNT.value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setAmount(amount: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
onNodeWithText(
|
||||
getStringResourceWithArgs(
|
||||
R.string.send_amount_hint,
|
||||
ZcashCurrency.fromResources(getAppContext()).name
|
||||
)
|
||||
).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(amount)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setValidAddress() {
|
||||
onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
onNodeWithText(getStringResource(R.string.send_address_hint)).also {
|
||||
it.performTextClearance()
|
||||
// Using sapling address here, as the unified is not available in the fixture. This will change.
|
||||
it.performTextInput(WalletAddressFixture.SAPLING_ADDRESS_STRING)
|
||||
it.performTextInput(ZecSendFixture.ADDRESS)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setAddress(address: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
onNodeWithText(getStringResource(R.string.send_address_hint)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(address)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setValidMemo() {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
onNodeWithText(getStringResource(R.string.send_memo_hint)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(MemoFixture.MEMO_STRING)
|
||||
it.performTextInput(ZecSendFixture.MEMO.value)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setMemo(memo: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
onNodeWithText(getStringResource(R.string.send_memo_hint)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(memo)
|
||||
}
|
||||
|
|
|
@ -4,26 +4,31 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
||||
import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class SendViewTestSetup(
|
||||
private val composeTestRule: ComposeContentTestRule,
|
||||
private val initialState: SendStage,
|
||||
private val initialZecSend: ZecSend?,
|
||||
private val initialSendArgumentWrapper: SendArgumentsWrapper?,
|
||||
private val hasCameraFeature: Boolean
|
||||
) {
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
|
@ -71,6 +76,12 @@ class SendViewTestSetup(
|
|||
@Composable
|
||||
@Suppress("TestFunctionName")
|
||||
fun DefaultContent() {
|
||||
val context = LocalContext.current
|
||||
|
||||
// TODO [#1171]: Remove default MonetarySeparators locale
|
||||
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
||||
val monetarySeparators = MonetarySeparators.current(Locale.US)
|
||||
|
||||
val (sendStage, setSendStage) =
|
||||
rememberSaveable(stateSaver = SendStage.Saver) { mutableStateOf(initialState) }
|
||||
|
||||
|
@ -97,6 +108,8 @@ class SendViewTestSetup(
|
|||
lastZecSend = zecSend
|
||||
|
||||
ZcashTheme {
|
||||
// TODO [#1260]: Cover Send.Form screen UI with tests
|
||||
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||
Send(
|
||||
walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
|
@ -105,17 +118,12 @@ class SendViewTestSetup(
|
|||
available = Zatoshi(Zatoshi.MAX_INCLUSIVE.div(100))
|
||||
)
|
||||
),
|
||||
focusManager = LocalFocusManager.current,
|
||||
sendStage = sendStage,
|
||||
sendArgumentsWrapper = initialSendArgumentWrapper,
|
||||
onSendStageChange = setSendStage,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = setZecSend,
|
||||
focusManager = LocalFocusManager.current,
|
||||
onBack = onBackAction,
|
||||
goBalances = {
|
||||
// TODO [#1194]: Cover Current balances UI widget with tests
|
||||
// TODO [#1194]: https://github.com/Electric-Coin-Company/zashi-android/issues/1194
|
||||
},
|
||||
onSettings = { onSettingsCount.incrementAndGet() },
|
||||
onCreateAndSend = {
|
||||
onCreateCount.incrementAndGet()
|
||||
|
@ -125,7 +133,20 @@ class SendViewTestSetup(
|
|||
onQrScannerOpen = {
|
||||
onScannerCount.incrementAndGet()
|
||||
},
|
||||
hasCameraFeature = hasCameraFeature
|
||||
goBalances = {
|
||||
// TODO [#1194]: Cover Current balances UI widget with tests
|
||||
// TODO [#1194]: https://github.com/Electric-Coin-Company/zashi-android/issues/1194
|
||||
},
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
recipientAddressState = RecipientAddressState("", AddressType.Invalid()),
|
||||
onRecipientAddressChange = {
|
||||
// TODO [#1260]: Cover Send.Form screen UI with tests
|
||||
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||
},
|
||||
amountState = AmountState.new(context, "", monetarySeparators),
|
||||
setAmountState = {},
|
||||
memoState = MemoState.new(""),
|
||||
setMemoState = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.integration
|
||||
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.MockSynchronizer
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.send.WrapSend
|
||||
|
@ -22,10 +21,10 @@ import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
|
|||
import co.electriccoin.zcash.ui.screen.send.setAddress
|
||||
import co.electriccoin.zcash.ui.screen.send.setAmount
|
||||
import co.electriccoin.zcash.ui.screen.send.setMemo
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
class SendViewIntegrationTest {
|
||||
@get:Rule
|
||||
|
@ -49,6 +48,10 @@ class SendViewIntegrationTest {
|
|||
)
|
||||
)
|
||||
|
||||
// TODO [#1171]: Remove default MonetarySeparators locale
|
||||
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
||||
private val monetarySeparators = MonetarySeparators.current(Locale.US)
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun send_screens_values_state_restoration() {
|
||||
|
@ -69,7 +72,8 @@ class SendViewIntegrationTest {
|
|||
goBack = {},
|
||||
goBalances = {},
|
||||
hasCameraFeature = true,
|
||||
goSettings = {}
|
||||
goSettings = {},
|
||||
monetarySeparators = monetarySeparators
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -92,24 +96,14 @@ class SendViewIntegrationTest {
|
|||
composeTestRule.clickBack()
|
||||
composeTestRule.assertOnForm()
|
||||
|
||||
// And check recreated form values too. Note also that we don't check the amount field value, as it's changed
|
||||
// by validation mechanisms
|
||||
|
||||
// We use that the assertTextEquals searches in SemanticsProperties.EditableText too, although to be able to
|
||||
// compare its editable value to an exact match we need to pass all its texts
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
it.assertTextEquals(
|
||||
getStringResource(R.string.send_to),
|
||||
ZecSendFixture.ADDRESS,
|
||||
includeEditableText = true
|
||||
)
|
||||
composeTestRule.onNodeWithText(ZecSendFixture.ADDRESS).also {
|
||||
it.assertExists()
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.assertTextEquals(
|
||||
getStringResource(R.string.send_memo),
|
||||
ZecSendFixture.MEMO.value,
|
||||
includeEditableText = true
|
||||
)
|
||||
composeTestRule.onNodeWithText(ZecSendFixture.AMOUNT.value.toString()).also {
|
||||
it.assertExists()
|
||||
}
|
||||
composeTestRule.onNodeWithText(ZecSendFixture.MEMO.value).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,7 @@ class SendViewAndroidTest : UiTestPrerequisites() {
|
|||
composeTestRule,
|
||||
sendStage,
|
||||
zecSend,
|
||||
null,
|
||||
true
|
||||
true,
|
||||
).apply {
|
||||
setDefaultContent()
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ import co.electriccoin.zcash.ui.screen.send.clickConfirmation
|
|||
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
|
||||
import co.electriccoin.zcash.ui.screen.send.clickScanner
|
||||
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import co.electriccoin.zcash.ui.screen.send.setAmount
|
||||
import co.electriccoin.zcash.ui.screen.send.setMemo
|
||||
|
@ -47,7 +46,8 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
@ -59,13 +59,11 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
private fun newTestSetup(
|
||||
sendStage: SendStage = SendStage.Form,
|
||||
zecSend: ZecSend? = null,
|
||||
sendArgumentsWrapper: SendArgumentsWrapper? = null,
|
||||
hasCameraFeature: Boolean = true
|
||||
) = SendViewTestSetup(
|
||||
composeTestRule,
|
||||
sendStage,
|
||||
zecSend,
|
||||
sendArgumentsWrapper,
|
||||
hasCameraFeature
|
||||
).apply {
|
||||
setDefaultContent()
|
||||
|
@ -85,6 +83,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Currently disabled. Will be implemented as part of #1260")
|
||||
fun create_request_no_memo() =
|
||||
runTest {
|
||||
val testSetup = newTestSetup()
|
||||
|
@ -119,6 +118,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Currently disabled. Will be implemented as part of #1260")
|
||||
fun create_request_with_memo() =
|
||||
runTest {
|
||||
val testSetup = newTestSetup()
|
||||
|
@ -155,6 +155,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Currently disabled. Will be implemented as part of #1260")
|
||||
fun check_regex_functionality_valid_inputs() =
|
||||
runTest {
|
||||
val testSetup = newTestSetup()
|
||||
|
@ -251,6 +252,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Currently disabled. Will be implemented as part of #1260")
|
||||
fun max_memo_length() =
|
||||
runTest {
|
||||
val testSetup = newTestSetup()
|
||||
|
@ -263,6 +265,8 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
while (Memo.isWithinMaxLength(toString())) {
|
||||
append("a")
|
||||
}
|
||||
// To align with the length limit restriction
|
||||
deleteCharAt(length - 1)
|
||||
}
|
||||
|
||||
composeTestRule.setMemo(input)
|
||||
|
@ -304,6 +308,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Currently disabled. Will be implemented as part of #1260")
|
||||
fun back_on_confirmation() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
|
@ -424,17 +429,14 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
assertEquals(1, testSetup.getOnScannerCount())
|
||||
}
|
||||
|
||||
// TODO [#1260]: Cover Send.Form screen UI with tests
|
||||
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Currently disabled. Will be implemented as part of #1260")
|
||||
fun input_arguments_to_form() {
|
||||
newTestSetup(
|
||||
sendStage = SendStage.Form,
|
||||
sendArgumentsWrapper =
|
||||
SendArgumentsWrapperFixture.new(
|
||||
recipientAddress = SendArgumentsWrapperFixture.RECIPIENT_ADDRESS,
|
||||
amount = SendArgumentsWrapperFixture.AMOUNT,
|
||||
memo = SendArgumentsWrapperFixture.MEMO
|
||||
),
|
||||
zecSend = null
|
||||
)
|
||||
|
||||
|
@ -442,23 +444,23 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
// We use that the assertTextEquals searches in SemanticsProperties.EditableText too, although to be able to
|
||||
// compare its editable value to an exact match we need to pass all its texts
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_address_hint)).also {
|
||||
it.assertTextEquals(
|
||||
getStringResource(R.string.send_to),
|
||||
SendArgumentsWrapperFixture.RECIPIENT_ADDRESS,
|
||||
getStringResource(R.string.send_address_hint),
|
||||
SendArgumentsWrapperFixture.RECIPIENT_ADDRESS.address,
|
||||
includeEditableText = true
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_amount_hint)).also {
|
||||
it.assertTextEquals(
|
||||
getStringResource(R.string.send_amount),
|
||||
getStringResource(R.string.send_amount_hint),
|
||||
SendArgumentsWrapperFixture.amountToFixtureZecString(SendArgumentsWrapperFixture.AMOUNT)!!,
|
||||
includeEditableText = true
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo_hint)).also {
|
||||
it.assertTextEquals(
|
||||
getStringResource(R.string.send_memo),
|
||||
getStringResource(R.string.send_memo_hint),
|
||||
SendArgumentsWrapperFixture.MEMO,
|
||||
includeEditableText = true
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
|||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.filters.SmallTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
|
@ -134,6 +135,7 @@ class SettingsViewTest : UiTestPrerequisites() {
|
|||
text = getStringResource(R.string.settings_about),
|
||||
ignoreCase = true
|
||||
).also {
|
||||
it.performScrollTo()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
android:name=".MainActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.App.Starting"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
android:theme="@style/Theme.App.Starting" />
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import cash.z.ecc.android.sdk.model.SeedPhrase
|
|||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.ui.common.BindCompLocalProvider
|
||||
import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider
|
||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
||||
|
|
|
@ -28,11 +28,13 @@ import co.electriccoin.zcash.ui.screen.history.WrapHistory
|
|||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
||||
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
|
||||
import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
||||
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
|
||||
import co.electriccoin.zcash.ui.screen.support.WrapSupport
|
||||
import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
|
@ -53,11 +55,15 @@ internal fun MainActivity.Navigation() {
|
|||
goHistory = { navController.navigateJustOnce(HISTORY) },
|
||||
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
||||
goScan = { navController.navigateJustOnce(SCAN) },
|
||||
// At this point we only read scan result data
|
||||
sendArgumentsWrapper =
|
||||
SendArgumentsWrapper(
|
||||
recipientAddress = backStackEntry.savedStateHandle[SEND_RECIPIENT_ADDRESS],
|
||||
amount = backStackEntry.savedStateHandle[SEND_AMOUNT],
|
||||
memo = backStackEntry.savedStateHandle[SEND_MEMO]
|
||||
recipientAddress =
|
||||
backStackEntry.savedStateHandle.get<String>(SEND_RECIPIENT_ADDRESS)?.let {
|
||||
Json.decodeFromString<ScanResult>(it).toRecipient()
|
||||
},
|
||||
amount = backStackEntry.savedStateHandle.get<String>(SEND_AMOUNT),
|
||||
memo = backStackEntry.savedStateHandle.get<String>(SEND_MEMO)
|
||||
),
|
||||
)
|
||||
// Remove used Send screen parameters passed from the Scan screen if some exist
|
||||
|
@ -120,10 +126,10 @@ internal fun MainActivity.Navigation() {
|
|||
}
|
||||
composable(SCAN) {
|
||||
WrapScanValidator(
|
||||
onScanValid = { result ->
|
||||
// At this point we only pass recipient address
|
||||
onScanValid = { scanResult ->
|
||||
// At this point we only pass scan result data to recipient address
|
||||
navController.previousBackStackEntry?.savedStateHandle?.apply {
|
||||
set(SEND_RECIPIENT_ADDRESS, result)
|
||||
set(SEND_RECIPIENT_ADDRESS, Json.encodeToString(ScanResult.serializer(), scanResult))
|
||||
set(SEND_AMOUNT, null)
|
||||
set(SEND_MEMO, null)
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
|
||||
import androidx.compose.material3.DrawerState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal fun DrawerState.openDrawerMenu(scope: CoroutineScope) {
|
||||
if (isOpen) {
|
||||
return
|
||||
}
|
||||
scope.launch { open() }
|
||||
}
|
||||
|
||||
internal fun DrawerState.closeDrawerMenu(scope: CoroutineScope) {
|
||||
if (isClosed) {
|
||||
return
|
||||
}
|
||||
scope.launch { close() }
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -40,19 +40,21 @@ private fun BalanceWidgetPreview() {
|
|||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
@Suppress("MagicNumber")
|
||||
BalanceWidget(
|
||||
walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
saplingBalance =
|
||||
WalletBalance(
|
||||
Zatoshi(1234567891234567),
|
||||
Zatoshi(123456789),
|
||||
Zatoshi(123)
|
||||
)
|
||||
),
|
||||
isReferenceToBalances = true,
|
||||
onReferenceClick = {},
|
||||
modifier = Modifier
|
||||
(
|
||||
BalanceWidget(
|
||||
walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
saplingBalance =
|
||||
WalletBalance(
|
||||
Zatoshi(1234567891234567),
|
||||
Zatoshi(123456789),
|
||||
Zatoshi(123)
|
||||
)
|
||||
),
|
||||
isReferenceToBalances = true,
|
||||
onReferenceClick = {},
|
||||
modifier = Modifier
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.compose
|
||||
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
|
@ -0,0 +1,45 @@
|
|||
package co.electriccoin.zcash.ui.common.extension
|
||||
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
fun AddressType.toSerializableName(): String =
|
||||
when (this) {
|
||||
AddressType.Transparent -> "transparent"
|
||||
AddressType.Shielded -> "shielded"
|
||||
AddressType.Unified -> "unified"
|
||||
// Improve this with serializing reason
|
||||
is AddressType.Invalid -> "invalid"
|
||||
}
|
||||
|
||||
fun fromSerializableName(typeName: String): AddressType =
|
||||
when (typeName) {
|
||||
"transparent" -> AddressType.Transparent
|
||||
"shielded" -> AddressType.Shielded
|
||||
"unified" -> AddressType.Unified
|
||||
// Improve this with deserializing reason
|
||||
"invalid" -> AddressType.Invalid()
|
||||
else -> error("Unsupported AddressType: $typeName")
|
||||
}
|
||||
|
||||
object AddressTypeAsStringSerializer : KSerializer<AddressType> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("AddressType", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: AddressType
|
||||
) {
|
||||
val string = value.toSerializableName()
|
||||
encoder.encodeString(string)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): AddressType {
|
||||
val string = decoder.decodeString()
|
||||
return fromSerializableName(string)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.extension
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
|
@ -1,5 +1,5 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.extension
|
||||
|
||||
fun <T> List<T>.first(count: Int) = subList(0, minOf(size, count))
|
|
@ -1,6 +1,6 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.common
|
||||
package co.electriccoin.zcash.ui.common.extension
|
||||
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
|
|
@ -26,9 +26,9 @@ import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
|||
import co.electriccoin.zcash.global.getInstance
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.common.extension.throttle
|
||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.throttle
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
|
|
|
@ -20,8 +20,8 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.test.CommonTag
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
|
|
|
@ -9,10 +9,10 @@ import cash.z.ecc.android.sdk.model.PercentDecimal
|
|||
import cash.z.ecc.android.sdk.model.toFiatCurrencyState
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.extension.toKotlinLocale
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.totalBalance
|
||||
import co.electriccoin.zcash.ui.common.toKotlinLocale
|
||||
|
||||
data class WalletDisplayValues(
|
||||
val progress: PercentDecimal,
|
||||
|
|
|
@ -51,8 +51,8 @@ import cash.z.ecc.android.sdk.model.Zatoshi
|
|||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.sdk.extension.toPercentageWithDecimal
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.changePendingBalance
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
|
|
|
@ -28,8 +28,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.SecureScreen
|
||||
import co.electriccoin.zcash.ui.common.shouldSecureScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.SecureScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen
|
||||
import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
|
|
|
@ -42,8 +42,8 @@ import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
|
|||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.BrightenScreen
|
||||
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.compose.BrightenScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.model.VersionInfo
|
||||
import co.electriccoin.zcash.ui.common.test.CommonTag
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
|
|
|
@ -105,7 +105,6 @@ private fun RequestTopAppBar(onBack: () -> Unit) {
|
|||
|
||||
// TODO [#215]: Need to add some UI to explain to the user if a request is invalid
|
||||
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
|
||||
// TODO [#288]: TextField component can't do long-press backspace.
|
||||
@Composable
|
||||
private fun RequestMainContent(
|
||||
myAddress: WalletAddress.Unified,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package co.electriccoin.zcash.ui.screen.restore.model
|
||||
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import co.electriccoin.zcash.ui.common.first
|
||||
import co.electriccoin.zcash.ui.common.extension.first
|
||||
import java.util.Locale
|
||||
|
||||
internal sealed class ParseResult {
|
||||
|
|
|
@ -2,7 +2,7 @@ package co.electriccoin.zcash.ui.screen.restore.state
|
|||
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.sdk.model.SeedPhraseValidation
|
||||
import co.electriccoin.zcash.ui.common.first
|
||||
import co.electriccoin.zcash.ui.common.extension.first
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
|
@ -66,8 +66,8 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|||
import cash.z.ecc.sdk.model.SeedPhraseValidation
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.SecureScreen
|
||||
import co.electriccoin.zcash.ui.common.shouldSecureScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.SecureScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.ChipOnSurface
|
||||
|
@ -488,7 +488,7 @@ private fun SeedGridWithText(
|
|||
)
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = ZcashTheme.dimens.textFieldPanelDefaultHeight)
|
||||
.defaultMinSize(minHeight = ZcashTheme.dimens.textFieldSeedPanelDefaultHeight)
|
||||
.then(modifier)
|
||||
.testTag(RestoreTag.CHIP_LAYOUT)
|
||||
) {
|
||||
|
|
|
@ -11,13 +11,14 @@ import co.electriccoin.zcash.ui.MainActivity
|
|||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResult
|
||||
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
|
||||
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun MainActivity.WrapScanValidator(
|
||||
onScanValid: (address: String) -> Unit,
|
||||
onScanValid: (address: ScanResult) -> Unit,
|
||||
goBack: () -> Unit
|
||||
) {
|
||||
WrapScan(
|
||||
|
@ -30,7 +31,7 @@ internal fun MainActivity.WrapScanValidator(
|
|||
@Composable
|
||||
fun WrapScan(
|
||||
activity: ComponentActivity,
|
||||
onScanValid: (address: String) -> Unit,
|
||||
onScanValid: (address: ScanResult) -> Unit,
|
||||
goBack: () -> Unit
|
||||
) {
|
||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
||||
|
@ -51,9 +52,10 @@ fun WrapScan(
|
|||
onBack = goBack,
|
||||
onScanned = { result ->
|
||||
scope.launch {
|
||||
val isAddressValid = !synchronizer.validateAddress(result).isNotValid
|
||||
val addressType = synchronizer.validateAddress(result)
|
||||
val isAddressValid = !addressType.isNotValid
|
||||
if (isAddressValid) {
|
||||
onScanValid(result)
|
||||
onScanValid(ScanResult(result, addressType))
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = activity.getString(R.string.scan_validation_invalid_address)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
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)
|
||||
}
|
|
@ -94,7 +94,6 @@ private fun PreviewScan() {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Scan(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
|
@ -182,7 +181,7 @@ private fun ScanTopAppBar(onBack: () -> Unit) {
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Suppress("MagicNumber", "LongMethod", "LongParameterList")
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
@Composable
|
||||
private fun ScanMainContent(
|
||||
onScanned: (String) -> Unit,
|
||||
|
@ -232,6 +231,8 @@ private fun ScanMainContent(
|
|||
val framePossibleSize = remember { mutableStateOf(IntSize.Zero) }
|
||||
|
||||
val configuration = LocalConfiguration.current
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val frameActualSize =
|
||||
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
(framePossibleSize.value.height * 0.85).roundToInt()
|
||||
|
|
|
@ -28,8 +28,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.SecureScreen
|
||||
import co.electriccoin.zcash.ui.common.shouldSecureScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.SecureScreen
|
||||
import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen
|
||||
import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
|
|
|
@ -12,11 +12,14 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.sdk.extension.send
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
|
@ -25,10 +28,14 @@ import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
|||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
|
@ -59,6 +66,10 @@ internal fun WrapSend(
|
|||
focusManager.clearFocus(true)
|
||||
}
|
||||
|
||||
// TODO [#1171]: Remove default MonetarySeparators locale
|
||||
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
||||
val monetarySeparators = MonetarySeparators.current(Locale.US)
|
||||
|
||||
WrapSend(
|
||||
sendArgumentsWrapper,
|
||||
synchronizer,
|
||||
|
@ -69,11 +80,12 @@ internal fun WrapSend(
|
|||
goBack,
|
||||
goBalances,
|
||||
goSettings,
|
||||
hasCameraFeature
|
||||
hasCameraFeature,
|
||||
monetarySeparators
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
|
||||
@VisibleForTesting
|
||||
@Composable
|
||||
internal fun WrapSend(
|
||||
|
@ -86,10 +98,13 @@ internal fun WrapSend(
|
|||
goBack: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
goSettings: () -> Unit,
|
||||
hasCameraFeature: Boolean
|
||||
hasCameraFeature: Boolean,
|
||||
monetarySeparators: MonetarySeparators
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
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
|
||||
|
@ -100,6 +115,50 @@ internal fun WrapSend(
|
|||
|
||||
val (zecSend, setZecSend) = rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(null) }
|
||||
|
||||
// Address computation:
|
||||
val (recipientAddressState, setRecipientAddressState) =
|
||||
rememberSaveable(stateSaver = RecipientAddressState.Saver) {
|
||||
mutableStateOf(RecipientAddressState(zecSend?.destination?.address ?: "", null))
|
||||
}
|
||||
if (sendArgumentsWrapper?.recipientAddress != null) {
|
||||
setRecipientAddressState(
|
||||
RecipientAddressState.new(
|
||||
sendArgumentsWrapper.recipientAddress.address,
|
||||
sendArgumentsWrapper.recipientAddress.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Amount computation:
|
||||
val (amountState, setAmountState) =
|
||||
rememberSaveable(stateSaver = AmountState.Saver) {
|
||||
mutableStateOf(
|
||||
AmountState.new(
|
||||
context = context,
|
||||
value = zecSend?.amount?.toZecString() ?: "",
|
||||
monetarySeparators = monetarySeparators
|
||||
)
|
||||
)
|
||||
}
|
||||
if (sendArgumentsWrapper?.amount != null) {
|
||||
setAmountState(
|
||||
AmountState.new(
|
||||
context = context,
|
||||
value = sendArgumentsWrapper.amount,
|
||||
monetarySeparators = monetarySeparators
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Memo computation:
|
||||
val (memoState, setMemoState) =
|
||||
rememberSaveable(stateSaver = MemoState.Saver) {
|
||||
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
|
||||
}
|
||||
if (sendArgumentsWrapper?.memo != null) {
|
||||
setMemoState(MemoState.new(sendArgumentsWrapper.memo))
|
||||
}
|
||||
|
||||
val onBackAction = {
|
||||
when (sendStage) {
|
||||
SendStage.Form -> goBack()
|
||||
|
@ -107,7 +166,12 @@ internal fun WrapSend(
|
|||
SendStage.Sending -> { /* no action - wait until the sending is done */ }
|
||||
is SendStage.SendFailure -> setSendStage(SendStage.Form)
|
||||
SendStage.SendSuccessful -> {
|
||||
// Reset Send.Form values
|
||||
setZecSend(null)
|
||||
setRecipientAddressState(RecipientAddressState.new(""))
|
||||
setAmountState(AmountState.new(context, "", monetarySeparators))
|
||||
setMemoState(MemoState.new(""))
|
||||
|
||||
setSendStage(SendStage.Form)
|
||||
goBack()
|
||||
}
|
||||
|
@ -126,7 +190,6 @@ internal fun WrapSend(
|
|||
} else {
|
||||
Send(
|
||||
walletSnapshot = walletSnapshot,
|
||||
sendArgumentsWrapper = sendArgumentsWrapper,
|
||||
sendStage = sendStage,
|
||||
onSendStageChange = setSendStage,
|
||||
zecSend = zecSend,
|
||||
|
@ -134,6 +197,19 @@ internal fun WrapSend(
|
|||
focusManager = focusManager,
|
||||
onBack = onBackAction,
|
||||
onSettings = goSettings,
|
||||
recipientAddressState = recipientAddressState,
|
||||
onRecipientAddressChange = {
|
||||
scope.launch {
|
||||
setRecipientAddressState(
|
||||
RecipientAddressState.new(
|
||||
address = it,
|
||||
// TODO [#342]: Verify Addresses without Synchronizer
|
||||
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
|
||||
type = synchronizer.validateAddress(it)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onCreateAndSend = {
|
||||
scope.launch {
|
||||
Twig.debug { "Sending transaction" }
|
||||
|
@ -148,6 +224,10 @@ internal fun WrapSend(
|
|||
}
|
||||
}
|
||||
},
|
||||
memoState = memoState,
|
||||
setMemoState = setMemoState,
|
||||
amountState = amountState,
|
||||
setAmountState = setAmountState,
|
||||
onQrScannerOpen = goToQrScanner,
|
||||
goBalances = goBalances,
|
||||
hasCameraFeature = hasCameraFeature
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecStringExt
|
||||
import cash.z.ecc.android.sdk.model.fromZecString
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
|
||||
sealed class AmountState(
|
||||
open val value: String,
|
||||
) {
|
||||
data class Valid(
|
||||
override val value: String,
|
||||
val zatoshi: Zatoshi
|
||||
) : AmountState(value)
|
||||
|
||||
data class Invalid(
|
||||
override val value: String,
|
||||
) : AmountState(value)
|
||||
|
||||
companion object {
|
||||
fun new(
|
||||
context: Context,
|
||||
value: String,
|
||||
monetarySeparators: MonetarySeparators
|
||||
): AmountState {
|
||||
// Validate raw input string
|
||||
val validated =
|
||||
runCatching {
|
||||
ZecStringExt.filterContinuous(context, monetarySeparators, value)
|
||||
}.onFailure {
|
||||
Twig.error(it) { "Failed while filtering raw amount characters" }
|
||||
}.getOrDefault(false)
|
||||
|
||||
if (!validated) {
|
||||
return Invalid(value)
|
||||
}
|
||||
|
||||
// Convert the input to Zatoshi type-safe amount representation
|
||||
val zatoshi = (Zatoshi.fromZecString(context, value, monetarySeparators))
|
||||
|
||||
// Note that the 0 funds sending is supported for sending a memo-only transaction
|
||||
return if (zatoshi == null) {
|
||||
Invalid(value)
|
||||
} else {
|
||||
Valid(value, zatoshi)
|
||||
}
|
||||
}
|
||||
|
||||
private const val TYPE_VALID = "valid" // $NON-NLS
|
||||
private const val TYPE_INVALID = "invalid" // $NON-NLS
|
||||
private const val KEY_TYPE = "type" // $NON-NLS
|
||||
private const val KEY_VALUE = "value" // $NON-NLS
|
||||
private const val KEY_ZATOSHI = "zatoshi" // $NON-NLS
|
||||
|
||||
internal val Saver
|
||||
get() =
|
||||
run {
|
||||
mapSaver<AmountState>(
|
||||
save = { it.toSaverMap() },
|
||||
restore = {
|
||||
if (it.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val amountString = (it[KEY_VALUE] as String)
|
||||
val type = (it[KEY_TYPE] as String)
|
||||
when (type) {
|
||||
TYPE_VALID -> Valid(amountString, Zatoshi(it[KEY_ZATOSHI] as Long))
|
||||
TYPE_INVALID -> Invalid(amountString)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun AmountState.toSaverMap(): HashMap<String, Any> {
|
||||
val saverMap = HashMap<String, Any>()
|
||||
when (this) {
|
||||
is Valid -> {
|
||||
saverMap[KEY_TYPE] = TYPE_VALID
|
||||
saverMap[KEY_ZATOSHI] = this.zatoshi.value
|
||||
}
|
||||
is Invalid -> saverMap[KEY_TYPE] = TYPE_INVALID
|
||||
}
|
||||
saverMap[KEY_VALUE] = this.value
|
||||
|
||||
return saverMap
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.model
|
||||
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
|
||||
sealed class MemoState(
|
||||
open val text: String,
|
||||
open val byteSize: Int
|
||||
) {
|
||||
data class Correct(
|
||||
override val text: String,
|
||||
override val byteSize: Int
|
||||
) : MemoState(text, byteSize)
|
||||
|
||||
data class TooLong(
|
||||
override val text: String,
|
||||
override val byteSize: Int
|
||||
) : MemoState(text, byteSize)
|
||||
|
||||
companion object {
|
||||
fun new(memo: String): MemoState {
|
||||
val bytesCount = Memo.countLength(memo)
|
||||
return if (bytesCount > Memo.MAX_MEMO_LENGTH_BYTES) {
|
||||
TooLong(memo, bytesCount)
|
||||
} else {
|
||||
Correct(memo, bytesCount)
|
||||
}
|
||||
}
|
||||
|
||||
private const val TYPE_CORRECT = "correct" // $NON-NLS
|
||||
private const val TYPE_TOO_LONG = "too_long" // $NON-NLS
|
||||
private const val KEY_TYPE = "type" // $NON-NLS
|
||||
private const val KEY_TEXT = "text" // $NON-NLS
|
||||
private const val KEY_LENGTH = "length" // $NON-NLS
|
||||
|
||||
internal val Saver
|
||||
get() =
|
||||
run {
|
||||
mapSaver<MemoState>(
|
||||
save = { it.toSaverMap() },
|
||||
restore = {
|
||||
if (it.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val text = (it[KEY_TEXT] as String)
|
||||
val length = (it[KEY_LENGTH] as Int)
|
||||
val type = (it[KEY_TYPE] as String)
|
||||
when (type) {
|
||||
TYPE_CORRECT -> Correct(text, length)
|
||||
TYPE_TOO_LONG -> TooLong(text, length)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun MemoState.toSaverMap(): HashMap<String, Any> {
|
||||
val saverMap = HashMap<String, Any>()
|
||||
when (this) {
|
||||
is Correct -> saverMap[KEY_TYPE] = TYPE_CORRECT
|
||||
is TooLong -> saverMap[KEY_TYPE] = TYPE_TOO_LONG
|
||||
}
|
||||
saverMap[KEY_TEXT] = this.text
|
||||
saverMap[KEY_LENGTH] = this.byteSize
|
||||
|
||||
return saverMap
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.model
|
||||
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
|
||||
data class RecipientAddressState(
|
||||
val address: String,
|
||||
val type: AddressType?
|
||||
) {
|
||||
companion object {
|
||||
private const val KEY_ADDRESS = "address" // $NON-NLS
|
||||
private const val KEY_TYPE = "type" // $NON-NLS
|
||||
private const val KEY_INVALID_REASON = "invalid_reason" // $NON-NLS
|
||||
private const val TYPE_INVALID = "invalid" // $NON-NLS
|
||||
private const val TYPE_SHIELDED = "shielded" // $NON-NLS
|
||||
private const val TYPE_TRANSPARENT = "transparent" // $NON-NLS
|
||||
private const val TYPE_UNIFIED = "unified" // $NON-NLS
|
||||
|
||||
fun new(
|
||||
address: String,
|
||||
type: AddressType? = null
|
||||
): RecipientAddressState = RecipientAddressState(address, type)
|
||||
|
||||
internal val Saver
|
||||
get() =
|
||||
run {
|
||||
mapSaver(
|
||||
save = { it.toSaverMap() },
|
||||
restore = {
|
||||
if (it.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val address = (it[KEY_ADDRESS] as String)
|
||||
val type = (it[KEY_TYPE] as String?)
|
||||
RecipientAddressState(
|
||||
address,
|
||||
when (type) {
|
||||
TYPE_INVALID ->
|
||||
AddressType.Invalid(
|
||||
(it[KEY_INVALID_REASON] as String)
|
||||
)
|
||||
TYPE_SHIELDED -> AddressType.Shielded
|
||||
TYPE_UNIFIED -> AddressType.Unified
|
||||
TYPE_TRANSPARENT -> AddressType.Transparent
|
||||
else -> null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun RecipientAddressState.toSaverMap(): HashMap<String, Any> {
|
||||
val saverMap = HashMap<String, Any>()
|
||||
|
||||
saverMap[KEY_ADDRESS] = this.address
|
||||
|
||||
if (this.type != null) {
|
||||
saverMap[KEY_TYPE] =
|
||||
when (this.type) {
|
||||
is AddressType.Invalid -> {
|
||||
saverMap[KEY_INVALID_REASON] = this.type.reason
|
||||
TYPE_INVALID
|
||||
}
|
||||
|
||||
AddressType.Unified -> TYPE_UNIFIED
|
||||
AddressType.Transparent -> TYPE_TRANSPARENT
|
||||
AddressType.Shielded -> TYPE_SHIELDED
|
||||
else -> error("Unsupported type: ${this.type}")
|
||||
}
|
||||
}
|
||||
|
||||
return saverMap
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.model
|
||||
|
||||
data class SendArgumentsWrapper(
|
||||
val recipientAddress: String? = null,
|
||||
val recipientAddress: RecipientAddressState? = null,
|
||||
val amount: String? = null,
|
||||
val memo: String? = null
|
||||
)
|
||||
|
|
|
@ -2,30 +2,30 @@
|
|||
|
||||
package co.electriccoin.zcash.ui.screen.send.view
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.QrCodeScanner
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
|
@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -46,33 +47,33 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
|
|||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.ZecSendExt
|
||||
import cash.z.ecc.android.sdk.model.ZecString
|
||||
import cash.z.ecc.android.sdk.model.ZecStringExt
|
||||
import cash.z.ecc.android.sdk.model.fromZecString
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.test.CommonTag
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
import co.electriccoin.zcash.ui.design.component.FormTextField
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.Header
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX
|
||||
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.SendArgumentsWrapper
|
||||
import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.Locale
|
||||
|
@ -84,18 +85,23 @@ private fun PreviewSendForm() {
|
|||
GradientSurface {
|
||||
Send(
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
focusManager = LocalFocusManager.current,
|
||||
sendArgumentsWrapper = null,
|
||||
sendStage = SendStage.Form,
|
||||
onSendStageChange = {},
|
||||
zecSend = null,
|
||||
onZecSendChange = {},
|
||||
onCreateAndSend = {},
|
||||
onQrScannerOpen = {},
|
||||
focusManager = LocalFocusManager.current,
|
||||
onBack = {},
|
||||
onSettings = {},
|
||||
onCreateAndSend = {},
|
||||
onQrScannerOpen = {},
|
||||
goBalances = {},
|
||||
hasCameraFeature = true
|
||||
hasCameraFeature = true,
|
||||
recipientAddressState = RecipientAddressState("invalid_address", AddressType.Invalid()),
|
||||
onRecipientAddressChange = {},
|
||||
setAmountState = {},
|
||||
amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()),
|
||||
setMemoState = {},
|
||||
memoState = MemoState.new("Test message")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +166,6 @@ private fun PreviewSendConfirmation() {
|
|||
@Composable
|
||||
fun Send(
|
||||
walletSnapshot: WalletSnapshot,
|
||||
sendArgumentsWrapper: SendArgumentsWrapper?,
|
||||
sendStage: SendStage,
|
||||
onSendStageChange: (SendStage) -> Unit,
|
||||
zecSend: ZecSend?,
|
||||
|
@ -171,7 +176,13 @@ fun Send(
|
|||
onCreateAndSend: (ZecSend) -> Unit,
|
||||
onQrScannerOpen: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
hasCameraFeature: Boolean
|
||||
hasCameraFeature: Boolean,
|
||||
recipientAddressState: RecipientAddressState,
|
||||
onRecipientAddressChange: (String) -> Unit,
|
||||
setAmountState: (AmountState) -> Unit,
|
||||
amountState: AmountState,
|
||||
setMemoState: (MemoState) -> Unit,
|
||||
memoState: MemoState,
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
SendTopAppBar(
|
||||
|
@ -182,13 +193,18 @@ fun Send(
|
|||
}) { paddingValues ->
|
||||
SendMainContent(
|
||||
walletSnapshot = walletSnapshot,
|
||||
sendArgumentsWrapper = sendArgumentsWrapper,
|
||||
onBack = onBack,
|
||||
focusManager = focusManager,
|
||||
sendStage = sendStage,
|
||||
onSendStageChange = onSendStageChange,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = onZecSendChange,
|
||||
recipientAddressState = recipientAddressState,
|
||||
onRecipientAddressChange = onRecipientAddressChange,
|
||||
amountState = amountState,
|
||||
setAmountState = setAmountState,
|
||||
memoState = memoState,
|
||||
setMemoState = setMemoState,
|
||||
onSendSubmit = onCreateAndSend,
|
||||
onQrScannerOpen = onQrScannerOpen,
|
||||
goBalances = goBalances,
|
||||
|
@ -196,10 +212,10 @@ fun Send(
|
|||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding() + dimens.spacingDefault,
|
||||
bottom = paddingValues.calculateBottomPadding() + dimens.spacingHuge,
|
||||
start = dimens.screenHorizontalSpacingRegular,
|
||||
end = dimens.screenHorizontalSpacingRegular
|
||||
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
|
||||
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingHuge,
|
||||
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
|
||||
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -240,24 +256,33 @@ private fun SendTopAppBar(
|
|||
private fun SendMainContent(
|
||||
walletSnapshot: WalletSnapshot,
|
||||
focusManager: FocusManager,
|
||||
sendArgumentsWrapper: SendArgumentsWrapper?,
|
||||
onBack: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
zecSend: ZecSend?,
|
||||
onZecSendChange: (ZecSend) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
sendStage: SendStage,
|
||||
onSendStageChange: (SendStage) -> Unit,
|
||||
onSendSubmit: (ZecSend) -> Unit,
|
||||
onQrScannerOpen: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
recipientAddressState: RecipientAddressState,
|
||||
onRecipientAddressChange: (String) -> Unit,
|
||||
hasCameraFeature: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
amountState: AmountState,
|
||||
setAmountState: (AmountState) -> Unit,
|
||||
memoState: MemoState,
|
||||
setMemoState: (MemoState) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when {
|
||||
(sendStage == SendStage.Form || null == zecSend) -> {
|
||||
SendForm(
|
||||
walletSnapshot = walletSnapshot,
|
||||
sendArgumentsWrapper = sendArgumentsWrapper,
|
||||
previousZecSend = zecSend,
|
||||
recipientAddressState = recipientAddressState,
|
||||
onRecipientAddressChange = onRecipientAddressChange,
|
||||
amountState = amountState,
|
||||
setAmountState = setAmountState,
|
||||
memoState = memoState,
|
||||
setMemoState = setMemoState,
|
||||
onCreateZecSend = {
|
||||
onSendStageChange(SendStage.Confirmation)
|
||||
onZecSendChange(it)
|
||||
|
@ -304,63 +329,46 @@ private fun SendMainContent(
|
|||
}
|
||||
|
||||
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
|
||||
// TODO [#288]: TextField component can't do long-press backspace.
|
||||
// TODO [#294]: DetektAll failed LongMethod
|
||||
// TODO [#217]: https://github.com/Electric-Coin-Company/zashi-android/issues/217
|
||||
|
||||
// TODO [#1257]: Send.Form TextFields not persisted on a configuration change when the underlying ViewPager is on the
|
||||
// Balances page
|
||||
// TODO [#1257]: https://github.com/Electric-Coin-Company/zashi-android/issues/1257
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod")
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
@Composable
|
||||
private fun SendForm(
|
||||
walletSnapshot: WalletSnapshot,
|
||||
focusManager: FocusManager,
|
||||
sendArgumentsWrapper: SendArgumentsWrapper?,
|
||||
previousZecSend: ZecSend?,
|
||||
recipientAddressState: RecipientAddressState,
|
||||
onRecipientAddressChange: (String) -> Unit,
|
||||
amountState: AmountState,
|
||||
setAmountState: (AmountState) -> Unit,
|
||||
memoState: MemoState,
|
||||
setMemoState: (MemoState) -> Unit,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
onQrScannerOpen: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
hasCameraFeature: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// TODO [#1171]: Remove default MonetarySeparators locale
|
||||
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
||||
val monetarySeparators = MonetarySeparators.current(Locale.US)
|
||||
val allowedCharacters = ZecString.allowedCharacters(monetarySeparators)
|
||||
|
||||
// TODO [#809]: Fix ZEC balance on Send screen
|
||||
// TODO [#809]: https://github.com/Electric-Coin-Company/zashi-android/issues/809
|
||||
var amountZecString by rememberSaveable {
|
||||
mutableStateOf(previousZecSend?.amount?.toZecString() ?: "")
|
||||
}
|
||||
var recipientAddressString by rememberSaveable {
|
||||
mutableStateOf(previousZecSend?.destination?.address ?: "")
|
||||
}
|
||||
var memoString by rememberSaveable { mutableStateOf(previousZecSend?.memo?.value ?: "") }
|
||||
|
||||
var validation by rememberSaveable {
|
||||
mutableStateOf<Set<ZecSendExt.ZecSendValidation.Invalid.ValidationError>>(emptySet())
|
||||
}
|
||||
|
||||
// TODO [#826]: SendArgumentsWrapper object properties validation
|
||||
// TODO [#826]: https://github.com/Electric-Coin-Company/zashi-android/issues/826
|
||||
if (sendArgumentsWrapper?.recipientAddress != null) {
|
||||
recipientAddressString = sendArgumentsWrapper.recipientAddress
|
||||
}
|
||||
if (sendArgumentsWrapper?.amount != null) {
|
||||
amountZecString = sendArgumentsWrapper.amount
|
||||
}
|
||||
if (sendArgumentsWrapper?.memo != null) {
|
||||
memoString = sendArgumentsWrapper.memo
|
||||
}
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.verticalScroll(scrollState)
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
BalanceWidget(
|
||||
walletSnapshot = walletSnapshot,
|
||||
|
@ -368,17 +376,174 @@ private fun SendForm(
|
|||
onReferenceClick = goBalances
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(dimens.spacingXlarge))
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
|
||||
|
||||
// TODO [#1256]: Consider Send.Form TextFields scrolling
|
||||
// TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256
|
||||
|
||||
SendFormAddressTextField(
|
||||
focusManager = focusManager,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
onQrScannerOpen = onQrScannerOpen,
|
||||
recipientAddressState = recipientAddressState,
|
||||
setRecipientAddress = onRecipientAddressChange
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
SendFormAmountTextField(
|
||||
amountSate = amountState,
|
||||
setAmountState = setAmountState,
|
||||
monetarySeparators = monetarySeparators,
|
||||
focusManager = focusManager,
|
||||
walletSnapshot = walletSnapshot,
|
||||
imeAction =
|
||||
if (recipientAddressState.type == AddressType.Transparent) {
|
||||
ImeAction.Done
|
||||
} else {
|
||||
ImeAction.Next
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
SendFormMemoTextField(
|
||||
memoState = memoState,
|
||||
setMemoState = setMemoState,
|
||||
focusManager = focusManager,
|
||||
isMemoFieldAvailable = (
|
||||
recipientAddressState.address.isEmpty() ||
|
||||
recipientAddressState.type is AddressType.Invalid ||
|
||||
(
|
||||
recipientAddressState.type is AddressType.Valid &&
|
||||
recipientAddressState.type !is AddressType.Transparent
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
// Common conditions continuously checked for validity
|
||||
val sendButtonEnabled =
|
||||
recipientAddressState.type !is AddressType.Invalid &&
|
||||
recipientAddressState.address.isNotEmpty() &&
|
||||
amountState is AmountState.Valid &&
|
||||
amountState.value.isNotBlank() &&
|
||||
walletSnapshot.spendableBalance() >= (amountState.zatoshi + ZcashSdk.MINERS_FEE) &&
|
||||
// A valid memo is necessary only for non-transparent recipient
|
||||
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
PrimaryButton(
|
||||
onClick = {
|
||||
// SDK side validations
|
||||
val zecSendValidation =
|
||||
ZecSendExt.new(
|
||||
context = context,
|
||||
destinationString = recipientAddressState.address,
|
||||
zecString = amountState.value,
|
||||
// Take memo for a valid non-transparent receiver only
|
||||
memoString =
|
||||
if (recipientAddressState.type == AddressType.Transparent) {
|
||||
""
|
||||
} else {
|
||||
memoState.text
|
||||
},
|
||||
monetarySeparators = monetarySeparators
|
||||
)
|
||||
|
||||
when (zecSendValidation) {
|
||||
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
|
||||
is ZecSendExt.ZecSendValidation.Invalid -> {
|
||||
// We do not expect this validation to fail, so logging is enough here
|
||||
// An error popup could be reasonable here as well
|
||||
Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" }
|
||||
}
|
||||
}
|
||||
},
|
||||
text = stringResource(id = R.string.send_create),
|
||||
enabled = sendButtonEnabled,
|
||||
modifier = Modifier.testTag(SendTag.SEND_FORM_BUTTON)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
BodySmall(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.send_fee,
|
||||
// TODO [#1047]: Representing Zatoshi amount
|
||||
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
|
||||
@Suppress("MagicNumber")
|
||||
Zatoshi(100_000L).toZecString()
|
||||
),
|
||||
textFontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SendFormAddressTextField(
|
||||
focusManager: FocusManager,
|
||||
hasCameraFeature: Boolean,
|
||||
onQrScannerOpen: () -> Unit,
|
||||
recipientAddressState: RecipientAddressState,
|
||||
setRecipientAddress: (String) -> Unit,
|
||||
) {
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
// Animate error show/hide
|
||||
.animateContentSize()
|
||||
// Scroll TextField above ime keyboard
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
) {
|
||||
Body(text = stringResource(id = R.string.send_address_label))
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
val recipientAddressValue = recipientAddressState.address
|
||||
val recipientAddressError =
|
||||
if (
|
||||
recipientAddressValue.isNotEmpty() &&
|
||||
recipientAddressState.type is AddressType.Invalid
|
||||
) {
|
||||
stringResource(id = R.string.send_address_invalid)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
FormTextField(
|
||||
value = recipientAddressString,
|
||||
value = recipientAddressValue,
|
||||
onValueChange = {
|
||||
recipientAddressString = it
|
||||
setRecipientAddress(it)
|
||||
},
|
||||
label = { Text(stringResource(id = R.string.send_to)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
error = recipientAddressError,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_address_hint),
|
||||
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||
color = ZcashTheme.colors.textFieldHint
|
||||
)
|
||||
},
|
||||
trailingIcon =
|
||||
if (hasCameraFeature) {
|
||||
{
|
||||
|
@ -386,7 +551,7 @@ private fun SendForm(
|
|||
onClick = onQrScannerOpen,
|
||||
content = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.QrCodeScanner,
|
||||
painter = painterResource(id = R.drawable.qr_code_icon),
|
||||
contentDescription = stringResource(R.string.send_scan_content_description)
|
||||
)
|
||||
}
|
||||
|
@ -405,54 +570,152 @@ private fun SendForm(
|
|||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Down)
|
||||
}
|
||||
)
|
||||
),
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(dimens.spacingSmall))
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
fun SendFormAmountTextField(
|
||||
amountSate: AmountState,
|
||||
focusManager: FocusManager,
|
||||
monetarySeparators: MonetarySeparators,
|
||||
setAmountState: (AmountState) -> Unit,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
imeAction: ImeAction,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val zcashCurrency = ZcashCurrency.getLocalizedName(context)
|
||||
|
||||
val amountError =
|
||||
when (amountSate) {
|
||||
is AmountState.Invalid -> {
|
||||
if (amountSate.value.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
stringResource(id = R.string.send_amount_invalid)
|
||||
}
|
||||
}
|
||||
is AmountState.Valid -> {
|
||||
if (walletSnapshot.spendableBalance() < (amountSate.zatoshi + ZcashSdk.MINERS_FEE)) {
|
||||
stringResource(id = R.string.send_amount_insufficient_balance)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
// Animate error show/hide
|
||||
.animateContentSize()
|
||||
// Scroll TextField above ime keyboard
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
) {
|
||||
Body(text = stringResource(id = R.string.send_amount_label))
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
FormTextField(
|
||||
value = amountZecString,
|
||||
value = amountSate.value,
|
||||
onValueChange = { newValue ->
|
||||
val validated =
|
||||
runCatching {
|
||||
ZecStringExt.filterContinuous(context, monetarySeparators, newValue)
|
||||
}.onFailure {
|
||||
Twig.error(it) { "Failed while filtering incoming characters in filterContinuous" }
|
||||
return@FormTextField
|
||||
}.getOrDefault(false)
|
||||
|
||||
if (!validated) {
|
||||
return@FormTextField
|
||||
}
|
||||
|
||||
amountZecString = newValue.filter { allowedCharacters.contains(it) }
|
||||
setAmountState(AmountState.new(context, newValue, monetarySeparators))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
error = amountError,
|
||||
placeholder = {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.send_amount_hint,
|
||||
zcashCurrency
|
||||
),
|
||||
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||
color = ZcashTheme.colors.textFieldHint
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = ImeAction.Next
|
||||
imeAction = imeAction
|
||||
),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Down)
|
||||
if (imeAction == ImeAction.Done) {
|
||||
focusManager.clearFocus(true)
|
||||
} else {
|
||||
focusManager.moveFocus(FocusDirection.Down)
|
||||
}
|
||||
}
|
||||
),
|
||||
label = { Text(stringResource(id = R.string.send_amount)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(dimens.spacingSmall))
|
||||
// TODO [#1259]: Send.Form screen Memo field stroke bubble style
|
||||
// TODO [#1259]: https://github.com/Electric-Coin-Company/zashi-android/issues/1259
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SendFormMemoTextField(
|
||||
focusManager: FocusManager,
|
||||
isMemoFieldAvailable: Boolean,
|
||||
memoState: MemoState,
|
||||
setMemoState: (MemoState) -> Unit,
|
||||
) {
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
// Animate error show/hide
|
||||
.animateContentSize()
|
||||
// Scroll TextField above ime keyboard
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.send_papre_plane),
|
||||
contentDescription = null,
|
||||
tint =
|
||||
if (isMemoFieldAvailable) {
|
||||
ZcashTheme.colors.textCommon
|
||||
} else {
|
||||
ZcashTheme.colors.textDisabled
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
Body(
|
||||
text = stringResource(id = R.string.send_memo_label),
|
||||
color =
|
||||
if (isMemoFieldAvailable) {
|
||||
ZcashTheme.colors.textCommon
|
||||
} else {
|
||||
ZcashTheme.colors.textDisabled
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
// TODO [#810]: Disable Memo UI field in case of Transparent address
|
||||
// TODO [#810]: https://github.com/Electric-Coin-Company/zashi-android/issues/810
|
||||
FormTextField(
|
||||
value = memoString,
|
||||
enabled = isMemoFieldAvailable,
|
||||
value = memoState.text,
|
||||
onValueChange = {
|
||||
if (Memo.isWithinMaxLength(it)) {
|
||||
memoString = it
|
||||
}
|
||||
setMemoState(MemoState.new(it))
|
||||
},
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
|
@ -464,63 +727,39 @@ private fun SendForm(
|
|||
focusManager.clearFocus(true)
|
||||
}
|
||||
),
|
||||
label = { Text(stringResource(id = R.string.send_memo)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_memo_hint),
|
||||
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||
color = ZcashTheme.colors.textFieldHint
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minHeight = ZcashTheme.dimens.textFieldMemoPanelDefaultHeight,
|
||||
)
|
||||
|
||||
if (validation.isNotEmpty()) {
|
||||
/*
|
||||
* Note: this is not localized in that it uses the enum constant name and joins the string
|
||||
* without regard for RTL. This will get resolved once we do proper validation for
|
||||
* the fields.
|
||||
*/
|
||||
Text(
|
||||
text = validation.joinToString(", "),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
if (isMemoFieldAvailable) {
|
||||
Body(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.send_memo_bytes_counter,
|
||||
Memo.MAX_MEMO_LENGTH_BYTES - memoState.byteSize,
|
||||
Memo.MAX_MEMO_LENGTH_BYTES
|
||||
),
|
||||
textFontWeight = FontWeight.Bold,
|
||||
color =
|
||||
if (memoState is MemoState.Correct) {
|
||||
ZcashTheme.colors.textFieldHint
|
||||
} else {
|
||||
ZcashTheme.colors.textFieldError
|
||||
},
|
||||
textAlign = TextAlign.End,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = ZcashTheme.dimens.spacingTiny)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||
|
||||
// Create a send amount that is continuously checked for validity
|
||||
val sendValueCheck = (Zatoshi.fromZecString(context, amountZecString, monetarySeparators))?.value ?: 0L
|
||||
|
||||
// Continuous amount check while user is typing into the amount field
|
||||
// Note: the check for ABBREVIATION_INDEX goes away once proper address validation is in place.
|
||||
// For now, it just prevents a crash on the confirmation screen.
|
||||
val sendButtonEnabled =
|
||||
amountZecString.isNotBlank() &&
|
||||
sendValueCheck > 0L &&
|
||||
walletSnapshot.spendableBalance().value >= (sendValueCheck + ZcashSdk.MINERS_FEE.value) &&
|
||||
recipientAddressString.length > ABBREVIATION_INDEX
|
||||
|
||||
PrimaryButton(
|
||||
onClick = {
|
||||
val zecSendValidation =
|
||||
ZecSendExt.new(
|
||||
context,
|
||||
recipientAddressString,
|
||||
amountZecString,
|
||||
memoString,
|
||||
monetarySeparators
|
||||
)
|
||||
|
||||
when (zecSendValidation) {
|
||||
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
|
||||
is ZecSendExt.ZecSendValidation.Invalid -> validation = zecSendValidation.validationErrors
|
||||
}
|
||||
},
|
||||
text = stringResource(id = R.string.send_create),
|
||||
enabled = sendButtonEnabled,
|
||||
modifier = Modifier.testTag(SendTag.SEND_FORM_BUTTON)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -560,14 +799,14 @@ private fun SendConfirmation(
|
|||
PrimaryButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = dimens.spacingSmall)
|
||||
.padding(top = ZcashTheme.dimens.spacingSmall)
|
||||
.testTag(SendTag.SEND_CONFIRMATION_BUTTON),
|
||||
onClick = onConfirmation,
|
||||
text = stringResource(id = R.string.send_confirmation_button),
|
||||
outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
|
||||
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -614,7 +853,7 @@ private fun Sending(
|
|||
Body(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = dimens.spacingSmall)
|
||||
.padding(vertical = ZcashTheme.dimens.spacingSmall)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(R.string.send_in_progress_wait),
|
||||
textAlign = TextAlign.Center,
|
||||
|
@ -642,7 +881,7 @@ private fun SendSuccessful(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(dimens.spacingDefault)
|
||||
.height(ZcashTheme.dimens.spacingDefault)
|
||||
)
|
||||
|
||||
Body(
|
||||
|
@ -664,14 +903,14 @@ private fun SendSuccessful(
|
|||
PrimaryButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = dimens.spacingSmall)
|
||||
.padding(top = ZcashTheme.dimens.spacingSmall)
|
||||
.testTag(SendTag.SEND_SUCCESS_BUTTON),
|
||||
text = stringResource(R.string.send_successful_button),
|
||||
onClick = onDone,
|
||||
outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
|
||||
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -696,7 +935,7 @@ private fun SendFailure(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(dimens.spacingDefault)
|
||||
.height(ZcashTheme.dimens.spacingDefault)
|
||||
)
|
||||
|
||||
Body(
|
||||
|
@ -712,7 +951,7 @@ private fun SendFailure(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(dimens.spacingDefault)
|
||||
.height(ZcashTheme.dimens.spacingDefault)
|
||||
)
|
||||
|
||||
Body(
|
||||
|
@ -732,13 +971,13 @@ private fun SendFailure(
|
|||
PrimaryButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = dimens.spacingSmall)
|
||||
.padding(top = ZcashTheme.dimens.spacingSmall)
|
||||
.testTag(SendTag.SEND_FAILED_BUTTON),
|
||||
text = stringResource(R.string.send_failure_button),
|
||||
onClick = onDone,
|
||||
outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
|
||||
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ private fun SupportMainContent(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
label = { Text(text = stringResource(id = R.string.support_hint)) }
|
||||
placeholder = { Text(text = stringResource(id = R.string.support_hint)) },
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
<string name="balances_transparent_balance_help_close">I got it!</string>
|
||||
<string name="balances_transparent_help_content_description">Show help</string>
|
||||
<string name="balances_transparent_balance_shield">Shield and consolidate funds</string>
|
||||
<string name="balances_transparent_balance_fee">(Fee < <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
|
||||
<string name="balances_transparent_balance_fee">(Typical fee < <xliff:g id="fee_amount" example="0.001">%1$s
|
||||
</xliff:g>)</string>
|
||||
|
||||
<string name="balances_status_syncing" formatted="true">Syncing…</string>
|
||||
<string name="balances_status_syncing_amount_suffix" formatted="true"><xliff:g id="amount_prefix" example="123$">%1$s</xliff:g> so far</string>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="26dp"
|
||||
android:height="26dp"
|
||||
android:viewportWidth="26"
|
||||
android:viewportHeight="26">
|
||||
<path
|
||||
android:pathData="M16.727,11.88H22.906C24.644,11.88 25.5,11.01 25.5,9.218V3.148C25.5,1.355 24.644,0.5 22.906,0.5H16.727C15.003,0.5 14.134,1.355 14.134,3.148V9.218C14.134,11.011 15.003,11.88 16.727,11.88ZM3.094,11.88H9.286C11.01,11.88 11.88,11.01 11.88,9.218V3.148C11.88,1.355 11.01,0.5 9.286,0.5H3.094C1.369,0.5 0.5,1.355 0.5,3.148V9.218C0.5,11.011 1.369,11.88 3.094,11.88ZM3.121,9.965C2.645,9.965 2.415,9.72 2.415,9.218V3.148C2.415,2.659 2.645,2.415 3.121,2.415H9.245C9.72,2.415 9.965,2.659 9.965,3.148V9.218C9.965,9.721 9.72,9.965 9.245,9.965H3.121ZM16.755,9.965C16.279,9.965 16.049,9.72 16.049,9.218V3.148C16.049,2.659 16.279,2.415 16.755,2.415H22.893C23.354,2.415 23.585,2.659 23.585,3.148V9.218C23.585,9.721 23.354,9.965 22.893,9.965H16.755ZM5.022,7.616H7.344C7.548,7.616 7.629,7.534 7.629,7.304V5.049C7.629,4.832 7.548,4.75 7.344,4.75H5.022C4.819,4.75 4.764,4.832 4.764,5.049V7.304C4.764,7.534 4.819,7.616 5.022,7.616ZM18.737,7.616H21.046C21.25,7.616 21.331,7.534 21.331,7.304V5.049C21.331,4.832 21.25,4.75 21.046,4.75H18.737C18.534,4.75 18.466,4.832 18.466,5.049V7.304C18.466,7.534 18.534,7.616 18.737,7.616ZM3.094,25.5H9.286C11.01,25.5 11.88,24.645 11.88,22.852V16.768C11.88,14.99 11.01,14.12 9.286,14.12H3.094C1.369,14.12 0.5,14.99 0.5,16.768V22.852C0.5,24.645 1.369,25.5 3.094,25.5ZM15.043,17.637H17.365C17.569,17.637 17.65,17.556 17.65,17.325V15.071C17.65,14.853 17.569,14.772 17.365,14.772H15.043C14.84,14.772 14.785,14.853 14.785,15.071V17.325C14.785,17.556 14.84,17.637 15.043,17.637ZM22.254,17.637H24.576C24.78,17.637 24.861,17.556 24.861,17.325V15.071C24.861,14.853 24.78,14.772 24.576,14.772H22.254C22.051,14.772 21.983,14.853 21.983,15.071V17.325C21.983,17.556 22.05,17.637 22.254,17.637ZM3.121,23.585C2.645,23.585 2.415,23.341 2.415,22.852V16.782C2.415,16.28 2.645,16.035 3.121,16.035H9.245C9.72,16.035 9.965,16.28 9.965,16.782V22.852C9.965,23.341 9.72,23.585 9.245,23.585H3.121ZM5.022,21.25H7.344C7.548,21.25 7.629,21.168 7.629,20.924V18.683C7.629,18.465 7.548,18.384 7.344,18.384H5.022C4.819,18.384 4.764,18.465 4.764,18.683V20.924C4.764,21.168 4.819,21.25 5.022,21.25ZM18.683,21.25H21.005C21.209,21.25 21.29,21.168 21.29,20.924V18.683C21.29,18.465 21.209,18.384 21.005,18.384H18.683C18.479,18.384 18.425,18.465 18.425,18.683V20.924C18.425,21.168 18.479,21.25 18.683,21.25ZM15.043,24.848H17.365C17.569,24.848 17.65,24.766 17.65,24.535V22.281C17.65,22.064 17.569,21.983 17.365,21.983H15.043C14.84,21.983 14.785,22.064 14.785,22.281V24.535C14.785,24.766 14.84,24.848 15.043,24.848ZM22.254,24.848H24.576C24.78,24.848 24.861,24.766 24.861,24.535V22.281C24.861,22.064 24.78,21.983 24.576,21.983H22.254C22.051,21.983 21.983,22.064 21.983,22.281V24.535C21.983,24.766 22.05,24.848 22.254,24.848Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="16">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h20v16h-20z"/>
|
||||
<path
|
||||
android:pathData="M19.92,0.07C19.81,-0.03 19.5,0 19.31,0.06C13.04,2.2 6.77,4.34 0.5,6.5C0.31,6.56 0.17,6.75 0,6.88C0.13,7.04 0.24,7.25 0.41,7.35C1.57,8.05 2.76,8.72 3.92,9.43C4.15,9.57 4.37,9.85 4.44,10.11C4.93,11.9 5.38,13.7 5.86,15.49C5.91,15.68 6.05,15.96 6.2,15.99C6.35,16.02 6.6,15.85 6.74,15.7C7.68,14.75 8.62,13.79 9.53,12.82C9.79,12.54 9.99,12.48 10.35,12.66C11.64,13.32 12.95,13.93 14.25,14.56C14.39,14.63 14.53,14.69 14.77,14.79C14.89,14.6 15.04,14.43 15.12,14.23C16.74,9.71 18.35,5.18 19.96,0.65C20.02,0.47 20.04,0.16 19.93,0.06L19.92,0.07ZM4.26,8.81C3.28,8.21 2.28,7.64 1.2,7C6.3,5.25 11.28,3.55 16.26,1.84L16.3,1.92C16.16,2.01 16.03,2.1 15.89,2.18C12.27,4.38 8.66,6.57 5.04,8.78C4.76,8.95 4.55,8.99 4.26,8.8V8.81ZM6.71,10.97C6.53,11.87 6.39,12.77 6.12,13.68C6.02,13.3 5.92,12.93 5.82,12.55C5.6,11.69 5.38,10.84 5.15,9.98C5.09,9.76 5.06,9.58 5.32,9.43C8.56,7.48 11.79,5.51 15.03,3.55C15.07,3.53 15.12,3.53 15.3,3.49C14.16,4.44 13.13,5.3 12.09,6.16C11.63,6.54 8.6,8.99 8.14,9.38C8.07,9.44 8,9.5 7.95,9.57C7.62,9.81 7.3,10.08 7.01,10.37C6.85,10.52 6.75,10.77 6.71,10.98V10.97ZM6.81,14.61C7.01,13.42 7.19,12.38 7.38,11.25C8,11.55 8.58,11.83 9.2,12.13C8.41,12.95 7.65,13.73 6.8,14.61H6.81ZM14.48,13.9C12.24,12.81 10.04,11.75 7.77,10.65C8.09,10.38 8.39,10.18 8.62,9.92C11.01,7.93 15.96,3.88 18.35,1.89C18.49,1.77 18.63,1.66 18.86,1.6C17.41,5.68 15.95,9.77 14.48,13.9Z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -3,10 +3,21 @@
|
|||
<string name="send_back">Back</string>
|
||||
<string name="send_back_content_description">Back</string>
|
||||
<string name="send_scan_content_description">Scan</string>
|
||||
<string name="send_to">Who would you like to send ZEC to?</string>
|
||||
<string name="send_amount">How much?</string>
|
||||
<string name="send_memo">Memo</string>
|
||||
<string name="send_create">Send</string>
|
||||
<string name="send_address_label">To:</string>
|
||||
<string name="send_address_hint">Zcash Address</string>
|
||||
<string name="send_address_invalid">Invalid address</string>
|
||||
<string name="send_amount_label">Amount:</string>
|
||||
<string name="send_amount_hint"><xliff:g id="currency" example="ZEC">%1$s</xliff:g> Amount</string>
|
||||
<string name="send_amount_insufficient_balance">Insufficient funds</string>
|
||||
<string name="send_amount_invalid">Invalid amount</string>
|
||||
<string name="send_memo_label">Message</string>
|
||||
<string name="send_memo_hint">Write private message here…</string>
|
||||
<string name="send_memo_bytes_counter">
|
||||
<xliff:g id="typed_bytes" example="12">%1$s</xliff:g>/
|
||||
<xliff:g id="max_bytes" example="500">%2$s</xliff:g>
|
||||
</string>
|
||||
<string name="send_create">Preview</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_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
|
||||
<string name="send_confirmation_memo_format" formatted="true">Memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>
|
||||
|
|
|
@ -33,6 +33,7 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
|
|||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
|
@ -485,17 +486,22 @@ private fun sendZecScreenshots(
|
|||
// Screenshot: Empty form
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 1")
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_amount)).also {
|
||||
composeTestRule.onNodeWithText(
|
||||
resContext.getString(
|
||||
R.string.send_amount_hint,
|
||||
ZcashCurrency.fromResources(resContext).name
|
||||
)
|
||||
).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
it.performTextInput("0${separators.decimal}123")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_to)).also {
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_address_hint)).also {
|
||||
it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_memo)).also {
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_memo_hint)).also {
|
||||
it.performTextInput(MemoFixture.MEMO_STRING)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue