* 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:
parent
27ee9f3aa1
commit
76a89c9b91
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue