[#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 // 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` // These are determined by `gcloud firebase test android models list`
@Suppress("MagicNumber", "VariableNaming") @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") @Suppress("MagicNumber", "VariableNaming")
val FIREBASE_TEST_LAB_MAX_SDK = 33 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. // change this unless you're absolutely sure we're ready to set a new API level.
assertEquals( assertEquals(
ApplicationProvider.getApplicationContext<Application>().applicationInfo.minSdkVersion, 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= BIP_39_INCLUDED_BUILD_PATH=
# Versions # Versions
ANDROID_MIN_SDK_VERSION=27 ANDROID_MIN_SDK_VERSION=30
ANDROID_TARGET_SDK_VERSION=34 ANDROID_TARGET_SDK_VERSION=34
ANDROID_COMPILE_SDK_VERSION=34 ANDROID_COMPILE_SDK_VERSION=34
@ -203,7 +203,7 @@ ZXING_VERSION=3.5.3
ZCASH_BIP39_VERSION=1.0.8 ZCASH_BIP39_VERSION=1.0.8
# WARNING: Ensure a non-snapshot version is used before releasing to production # 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 # Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application. # 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.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight 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.PercentDecimal
import cash.z.ecc.android.sdk.model.Proposal import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.TransactionOverview
@ -27,6 +29,9 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
@Suppress("TooManyFunctions", "UNUSED_PARAMETER") @Suppress("TooManyFunctions", "UNUSED_PARAMETER")
internal class MockSynchronizer : CloseableSynchronizer { internal class MockSynchronizer : CloseableSynchronizer {
override val exchangeRateUsd: StateFlow<ObserveFiatCurrencyResult>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val latestBirthdayHeight: BlockHeight override val latestBirthdayHeight: BlockHeight
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
@ -64,16 +69,18 @@ internal class MockSynchronizer : CloseableSynchronizer {
override val processorInfo: Flow<CompactBlockProcessor.ProcessorInfo> override val processorInfo: Flow<CompactBlockProcessor.ProcessorInfo>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val progress: Flow<PercentDecimal> 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?> override val saplingBalances: StateFlow<WalletBalance?>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val status: Flow<Synchronizer.Status> override val status: Flow<Synchronizer.Status>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
override val transactions: Flow<List<TransactionOverview>> 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?> override val transparentBalance: StateFlow<Zatoshi?>
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") 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.") 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 { override suspend fun isValidTransparentAddr(address: String): Boolean {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") 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.") 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( override suspend fun refreshUtxos(
account: Account, account: Account,
since: BlockHeight since: BlockHeight
@ -213,6 +228,13 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") 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 { companion object {
fun new() = MockSynchronizer() 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.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
@ -39,6 +40,7 @@ class SecurityWarningViewTest : UiTestPrerequisites() {
} }
composeTestRule.onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also { composeTestRule.onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
it.performScrollTo()
it.assertExists() it.assertExists()
it.assertIsDisplayed() it.assertIsDisplayed()
it.assertHasClickAction() it.assertHasClickAction()
@ -105,6 +107,7 @@ private fun ComposeContentTestRule.clickBack() {
private fun ComposeContentTestRule.clickConfirm() { private fun ComposeContentTestRule.clickConfirm() {
onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also { onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
it.performScrollTo()
it.performClick() it.performClick()
} }
} }

View File

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

View File

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

View File

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

View File

@ -142,7 +142,9 @@ internal fun WrapSend(
context = context, context = context,
value = zecSend?.amount?.toZecString() ?: "", value = zecSend?.amount?.toZecString() ?: "",
monetarySeparators = monetarySeparators, 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( setAmountState(
AmountState.new( AmountState.new(
context = context, context = context,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false, isTransparentOrTextRecipient =
recipientAddressState.type?.let { it == AddressType.Transparent }
?: false,
monetarySeparators = monetarySeparators, monetarySeparators = monetarySeparators,
value = amountState.value value = amountState.value
) )
@ -177,7 +181,9 @@ internal fun WrapSend(
val onBackAction = { val onBackAction = {
when (sendStage) { when (sendStage) {
SendStage.Form -> goBack() 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) is SendStage.SendFailure -> setSendStage(SendStage.Form)
} }
} }

View File

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

View File

@ -25,7 +25,7 @@ sealed class AmountState(
context: Context, context: Context,
monetarySeparators: MonetarySeparators, monetarySeparators: MonetarySeparators,
value: String, value: String,
isTransparentRecipient: Boolean isTransparentOrTextRecipient: Boolean
): AmountState { ): AmountState {
// Validate raw input string // Validate raw input string
val validated = val validated =
@ -45,7 +45,7 @@ sealed class AmountState(
// Note that the zero funds sending is supported for sending a memo-only shielded transaction // Note that the zero funds sending is supported for sending a memo-only shielded transaction
return when { return when {
(zatoshi == null) -> Invalid(value) (zatoshi == null) -> Invalid(value)
(zatoshi.value == 0L && isTransparentRecipient) -> Invalid(value) (zatoshi.value == 0L && isTransparentOrTextRecipient) -> Invalid(value)
else -> Valid(value, zatoshi) 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_INVALID = "invalid" // $NON-NLS
private const val TYPE_SHIELDED = "shielded" // $NON-NLS private const val TYPE_SHIELDED = "shielded" // $NON-NLS
private const val TYPE_TRANSPARENT = "transparent" // $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 private const val TYPE_UNIFIED = "unified" // $NON-NLS
fun new( fun new(
@ -42,6 +43,7 @@ data class RecipientAddressState(
TYPE_SHIELDED -> AddressType.Shielded TYPE_SHIELDED -> AddressType.Shielded
TYPE_UNIFIED -> AddressType.Unified TYPE_UNIFIED -> AddressType.Unified
TYPE_TRANSPARENT -> AddressType.Transparent TYPE_TRANSPARENT -> AddressType.Transparent
TYPE_TEX -> AddressType.Tex
else -> null else -> null
} }
) )
@ -62,14 +64,13 @@ data class RecipientAddressState(
saverMap[KEY_INVALID_REASON] = this.type.reason saverMap[KEY_INVALID_REASON] = this.type.reason
TYPE_INVALID TYPE_INVALID
} }
AddressType.Unified -> TYPE_UNIFIED AddressType.Unified -> TYPE_UNIFIED
AddressType.Transparent -> TYPE_TRANSPARENT AddressType.Transparent -> TYPE_TRANSPARENT
AddressType.Shielded -> TYPE_SHIELDED AddressType.Shielded -> TYPE_SHIELDED
AddressType.Tex -> TYPE_TEX
else -> error("Unsupported type: ${this.type}") else -> error("Unsupported type: ${this.type}")
} }
} }
return saverMap return saverMap
} }
} }

View File

@ -362,15 +362,27 @@ private fun SendForm(
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault)) 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( SendFormAmountTextField(
amountSate = amountState, amountSate = amountState,
imeAction = imeAction =
if (recipientAddressState.type == AddressType.Transparent) { if (recipientAddressState.type == AddressType.Transparent || !isMemoFieldAvailable) {
ImeAction.Done ImeAction.Done
} else { } else {
ImeAction.Next ImeAction.Next
}, },
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false, isTransparentOrTextRecipient =
recipientAddressState.type?.let {
it == AddressType.Transparent || it == AddressType.Tex
} ?: false,
monetarySeparators = monetarySeparators, monetarySeparators = monetarySeparators,
setAmountState = setAmountState, setAmountState = setAmountState,
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
@ -381,14 +393,7 @@ private fun SendForm(
SendFormMemoTextField( SendFormMemoTextField(
memoState = memoState, memoState = memoState,
setMemoState = setMemoState, setMemoState = setMemoState,
isMemoFieldAvailable = ( isMemoFieldAvailable = isMemoFieldAvailable,
recipientAddressState.address.isEmpty() ||
recipientAddressState.type is AddressType.Invalid ||
(
recipientAddressState.type is AddressType.Valid &&
recipientAddressState.type !is AddressType.Transparent
)
),
scrollState = scrollState, scrollState = scrollState,
scrollTo = scrollToFeePixels scrollTo = scrollToFeePixels
) )
@ -584,7 +589,7 @@ fun SendFormAddressTextField(
fun SendFormAmountTextField( fun SendFormAmountTextField(
amountSate: AmountState, amountSate: AmountState,
imeAction: ImeAction, imeAction: ImeAction,
isTransparentRecipient: Boolean, isTransparentOrTextRecipient: Boolean,
monetarySeparators: MonetarySeparators, monetarySeparators: MonetarySeparators,
setAmountState: (AmountState) -> Unit, setAmountState: (AmountState) -> Unit,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
@ -636,7 +641,7 @@ fun SendFormAmountTextField(
context = context, context = context,
value = newValue, value = newValue,
monetarySeparators = monetarySeparators, 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 // Dark mode was introduced in Android Q
@Test // @Test
@MediumTest // @MediumTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) // @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun takeScreenshotsForRestoreWalletDarkEnUS() { // fun takeScreenshotsForRestoreWalletDarkEnUS() {
runWith(UiMode.Dark, "en-US") { context, tag -> // runWith(UiMode.Dark, "en-US") { context, tag ->
takeScreenshotsForRestoreWallet(context, tag) // takeScreenshotsForRestoreWallet(context, tag)
} // }
} // }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")