[#1538] Adopt TEX addresses related changes (#1539)

* Adopt TEX addresses related changes

* Send screen keyboard ime action handling

* Send screen amount handling for tex addresses

* Dependency update

* Test hotfixes

* Test hotfixes

* Code cleanup

* Test hotfix

* Test hotfix

* Min api bump

* Test hotfixes

---------

Co-authored-by: Milan Cerovsky <milan@z.cash>
This commit is contained in:
Honza Rychnovský 2024-08-23 06:00:36 +02:00 committed by GitHub
parent 27ee9f3aa1
commit 76a89c9b91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 83 additions and 41 deletions

View File

@ -284,7 +284,7 @@ fladle {
// Firebase Test Lab has min and max values that might differ from our project's
// These are determined by `gcloud firebase test android models list`
@Suppress("MagicNumber", "VariableNaming")
val FIREBASE_TEST_LAB_MIN_SDK = 27 // Minimum for Pixel2.arm device
val FIREBASE_TEST_LAB_MIN_SDK = 30 // Minimum for Pixel2.arm device
@Suppress("MagicNumber", "VariableNaming")
val FIREBASE_TEST_LAB_MAX_SDK = 33

View File

@ -30,7 +30,7 @@ class AndroidApiTest {
// change this unless you're absolutely sure we're ready to set a new API level.
assertEquals(
ApplicationProvider.getApplicationContext<Application>().applicationInfo.minSdkVersion,
Build.VERSION_CODES.O_MR1
Build.VERSION_CODES.R
)
}
}

View File

@ -138,7 +138,7 @@ SDK_INCLUDED_BUILD_PATH=
BIP_39_INCLUDED_BUILD_PATH=
# Versions
ANDROID_MIN_SDK_VERSION=27
ANDROID_MIN_SDK_VERSION=30
ANDROID_TARGET_SDK_VERSION=34
ANDROID_COMPILE_SDK_VERSION=34
@ -203,7 +203,7 @@ ZXING_VERSION=3.5.3
ZCASH_BIP39_VERSION=1.0.8
# WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_SDK_VERSION=2.1.3
ZCASH_SDK_VERSION=2.2.1
# Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application.

View File

@ -6,6 +6,8 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.FastestServersResult
import cash.z.ecc.android.sdk.model.ObserveFiatCurrencyResult
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.TransactionOverview
@ -27,6 +29,9 @@ import kotlinx.coroutines.flow.StateFlow
*/
@Suppress("TooManyFunctions", "UNUSED_PARAMETER")
internal class MockSynchronizer : CloseableSynchronizer {
override val exchangeRateUsd: StateFlow<ObserveFiatCurrencyResult>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val latestBirthdayHeight: BlockHeight
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
@ -64,16 +69,18 @@ internal class MockSynchronizer : CloseableSynchronizer {
override val processorInfo: Flow<CompactBlockProcessor.ProcessorInfo>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val progress: Flow<PercentDecimal>
get() = TODO("Not yet implemented")
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val saplingBalances: StateFlow<WalletBalance?>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val status: Flow<Synchronizer.Status>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val transactions: Flow<List<TransactionOverview>>
get() = TODO("Not yet implemented")
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val transparentBalance: StateFlow<Zatoshi?>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
@ -121,6 +128,10 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
override suspend fun isValidTexAddr(address: String): Boolean {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
override suspend fun isValidTransparentAddr(address: String): Boolean {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
@ -151,6 +162,10 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
override suspend fun refreshExchangeRateUsd() {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
override suspend fun refreshUtxos(
account: Account,
since: BlockHeight
@ -213,6 +228,13 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
override suspend fun getFastestServers(
context: Context,
servers: List<LightWalletEndpoint>
): Flow<FastestServersResult> {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
companion object {
fun new() = MockSynchronizer()
}

View File

@ -10,6 +10,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 co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
@ -39,6 +40,7 @@ class SecurityWarningViewTest : UiTestPrerequisites() {
}
composeTestRule.onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
it.performScrollTo()
it.assertExists()
it.assertIsDisplayed()
it.assertHasClickAction()
@ -105,6 +107,7 @@ private fun ComposeContentTestRule.clickBack() {
private fun ComposeContentTestRule.clickConfirm() {
onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
it.performScrollTo()
it.performClick()
}
}

View File

@ -13,6 +13,7 @@ fun AddressType.toSerializableName(): String =
AddressType.Transparent -> "transparent"
AddressType.Shielded -> "shielded"
AddressType.Unified -> "unified"
AddressType.Tex -> "tex"
// Improve this with serializing reason
is AddressType.Invalid -> "invalid"
}
@ -22,6 +23,7 @@ fun fromSerializableName(typeName: String): AddressType =
"transparent" -> AddressType.Transparent
"shielded" -> AddressType.Shielded
"unified" -> AddressType.Unified
"tex" -> AddressType.Tex
// Improve this with deserializing reason
"invalid" -> AddressType.Invalid()
else -> error("Unsupported AddressType: $typeName")

View File

@ -29,6 +29,7 @@ data class SerializableAddress(
AddressType.Unified -> WalletAddress.Unified.new(address)
AddressType.Shielded -> WalletAddress.Sapling.new(address)
AddressType.Transparent -> WalletAddress.Transparent.new(address)
AddressType.Tex -> WalletAddress.Tex.new(address)
is AddressType.Invalid -> error("Invalid address type")
}
}

View File

@ -224,12 +224,13 @@ private fun ReceiveContents(
.fillMaxWidth(),
pagerState = pagerState,
tabs =
state.map {
state.mapNotNull {
stringResource(
when (it) {
is WalletAddress.Unified -> R.string.receive_wallet_address_unified
is WalletAddress.Sapling -> R.string.receive_wallet_address_sapling
is WalletAddress.Transparent -> R.string.receive_wallet_address_transparent
else -> return@mapNotNull null
}
)
}.toPersistentList(),
@ -299,13 +300,12 @@ private fun ColumnScope.QrCode(
qrCodeImage = qrCodeImage,
onQrImageBitmapShare = onQrImageShare,
contentDescription =
stringResource(
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_unified_content_description
is WalletAddress.Sapling -> R.string.receive_sapling_content_description
is WalletAddress.Transparent -> R.string.receive_transparent_content_description
}
),
when (walletAddress) {
is WalletAddress.Unified -> stringResource(R.string.receive_unified_content_description)
is WalletAddress.Sapling -> stringResource(R.string.receive_sapling_content_description)
is WalletAddress.Transparent -> stringResource(R.string.receive_transparent_content_description)
else -> ""
},
modifier =
Modifier
.align(Alignment.CenterHorizontally),

View File

@ -142,7 +142,9 @@ internal fun WrapSend(
context = context,
value = zecSend?.amount?.toZecString() ?: "",
monetarySeparators = monetarySeparators,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false
isTransparentOrTextRecipient =
recipientAddressState.type?.let { it == AddressType.Transparent }
?: false
)
)
}
@ -152,7 +154,9 @@ internal fun WrapSend(
setAmountState(
AmountState.new(
context = context,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
isTransparentOrTextRecipient =
recipientAddressState.type?.let { it == AddressType.Transparent }
?: false,
monetarySeparators = monetarySeparators,
value = amountState.value
)
@ -177,7 +181,9 @@ internal fun WrapSend(
val onBackAction = {
when (sendStage) {
SendStage.Form -> goBack()
SendStage.Proposing -> { /* no action - wait until the sending is done */ }
SendStage.Proposing -> { // no action - wait until the sending is done
}
is SendStage.SendFailure -> setSendStage(SendStage.Form)
}
}

View File

@ -39,5 +39,6 @@ internal fun WalletAddress.toSerializableAddress() =
is WalletAddress.Unified -> AddressType.Unified
is WalletAddress.Sapling -> AddressType.Shielded
is WalletAddress.Transparent -> AddressType.Transparent
is WalletAddress.Tex -> AddressType.Tex
}
)

View File

@ -25,7 +25,7 @@ sealed class AmountState(
context: Context,
monetarySeparators: MonetarySeparators,
value: String,
isTransparentRecipient: Boolean
isTransparentOrTextRecipient: Boolean
): AmountState {
// Validate raw input string
val validated =
@ -45,7 +45,7 @@ sealed class AmountState(
// Note that the zero funds sending is supported for sending a memo-only shielded transaction
return when {
(zatoshi == null) -> Invalid(value)
(zatoshi.value == 0L && isTransparentRecipient) -> Invalid(value)
(zatoshi.value == 0L && isTransparentOrTextRecipient) -> Invalid(value)
else -> Valid(value, zatoshi)
}
}

View File

@ -14,6 +14,7 @@ data class RecipientAddressState(
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_TEX = "tex" // $NON-NLS
private const val TYPE_UNIFIED = "unified" // $NON-NLS
fun new(
@ -42,6 +43,7 @@ data class RecipientAddressState(
TYPE_SHIELDED -> AddressType.Shielded
TYPE_UNIFIED -> AddressType.Unified
TYPE_TRANSPARENT -> AddressType.Transparent
TYPE_TEX -> AddressType.Tex
else -> null
}
)
@ -62,14 +64,13 @@ data class RecipientAddressState(
saverMap[KEY_INVALID_REASON] = this.type.reason
TYPE_INVALID
}
AddressType.Unified -> TYPE_UNIFIED
AddressType.Transparent -> TYPE_TRANSPARENT
AddressType.Shielded -> TYPE_SHIELDED
AddressType.Tex -> TYPE_TEX
else -> error("Unsupported type: ${this.type}")
}
}
return saverMap
}
}

View File

@ -362,15 +362,27 @@ private fun SendForm(
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
val isMemoFieldAvailable =
recipientAddressState.address.isEmpty() ||
recipientAddressState.type is AddressType.Invalid ||
(
recipientAddressState.type is AddressType.Valid &&
recipientAddressState.type !is AddressType.Transparent &&
recipientAddressState.type !is AddressType.Tex
)
SendFormAmountTextField(
amountSate = amountState,
imeAction =
if (recipientAddressState.type == AddressType.Transparent) {
if (recipientAddressState.type == AddressType.Transparent || !isMemoFieldAvailable) {
ImeAction.Done
} else {
ImeAction.Next
},
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
isTransparentOrTextRecipient =
recipientAddressState.type?.let {
it == AddressType.Transparent || it == AddressType.Tex
} ?: false,
monetarySeparators = monetarySeparators,
setAmountState = setAmountState,
walletSnapshot = walletSnapshot,
@ -381,14 +393,7 @@ private fun SendForm(
SendFormMemoTextField(
memoState = memoState,
setMemoState = setMemoState,
isMemoFieldAvailable = (
recipientAddressState.address.isEmpty() ||
recipientAddressState.type is AddressType.Invalid ||
(
recipientAddressState.type is AddressType.Valid &&
recipientAddressState.type !is AddressType.Transparent
)
),
isMemoFieldAvailable = isMemoFieldAvailable,
scrollState = scrollState,
scrollTo = scrollToFeePixels
)
@ -584,7 +589,7 @@ fun SendFormAddressTextField(
fun SendFormAmountTextField(
amountSate: AmountState,
imeAction: ImeAction,
isTransparentRecipient: Boolean,
isTransparentOrTextRecipient: Boolean,
monetarySeparators: MonetarySeparators,
setAmountState: (AmountState) -> Unit,
walletSnapshot: WalletSnapshot,
@ -636,7 +641,7 @@ fun SendFormAmountTextField(
context = context,
value = newValue,
monetarySeparators = monetarySeparators,
isTransparentRecipient = isTransparentRecipient
isTransparentOrTextRecipient = isTransparentOrTextRecipient
)
)
},

View File

@ -147,15 +147,16 @@ class ScreenshotTest : UiTestPrerequisites() {
}
}
// disabling flaky test
// Dark mode was introduced in Android Q
@Test
@MediumTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun takeScreenshotsForRestoreWalletDarkEnUS() {
runWith(UiMode.Dark, "en-US") { context, tag ->
takeScreenshotsForRestoreWallet(context, tag)
}
}
// @Test
// @MediumTest
// @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
// fun takeScreenshotsForRestoreWalletDarkEnUS() {
// runWith(UiMode.Dark, "en-US") { context, tag ->
// takeScreenshotsForRestoreWallet(context, tag)
// }
// }
@OptIn(ExperimentalTestApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")