[#218] Prevent illegal number on ZEC amount
- Prevent illegal input in ZEC Send/Request form. - New SDL-EXT-UI module for UI related SDK helper components. - Regex for continuous validation of the entered ZEC amount. - Added a new unit tests for validation of the regex too. - Using the regex on ZEC Request and ZEC Send screens. - Updated existing and created a new UI tests for validating entered ZEC amount values on Request and Send screens. - Improve code to be validated with DetektAll static analyzation. - Architecture documentation update with the newly added sdk-ext-ui module. - Added run configuration sdk-ext-lib:connectedCheck for AS. - Added check for digits count between grouping separators + tests. - Refactoring test class name and its separator value.
This commit is contained in:
parent
39949d8632
commit
db13435d52
|
@ -0,0 +1,57 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="sdk-ext-ui:connectedCheck" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
||||
<module name="zcash-android-app.sdk-ext-ui" />
|
||||
<option name="TESTING_TYPE" value="0" />
|
||||
<option name="METHOD_NAME" value="" />
|
||||
<option name="CLASS_NAME" value="" />
|
||||
<option name="PACKAGE_NAME" value="" />
|
||||
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
||||
<option name="EXTRA_OPTIONS" value="" />
|
||||
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
||||
<option name="RETENTION_ENABLED" value="No" />
|
||||
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
|
||||
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
|
||||
<option name="CLEAR_LOGCAT" value="false" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
|
||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||
<Auto>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Auto>
|
||||
<Hybrid>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Hybrid>
|
||||
<Java />
|
||||
<Native>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Native>
|
||||
<Profilers>
|
||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
|
||||
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
||||
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
||||
</Profilers>
|
||||
<method v="2">
|
||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
|
@ -33,6 +33,7 @@ The logical components of the app are implemented as a number of Gradle modules.
|
|||
* `preference-api-lib` — Multiplatform interfaces for key-value storage of preferences.
|
||||
* `preference-impl-android-lib` — Android-specific implementation for preference storage.
|
||||
* `sdk-ext-lib` — Contains extensions on top of the to the Zcash SDK. Some of these extensions might be migrated into the SDK eventually, while others might represent Android-centric idioms. Depending on how this module evolves, it could adopt another name such as `wallet-lib` or be split into two.
|
||||
* `sdk-ext-ui` — Place for Zcash SDK components (same as `sdk-ext-lib`), which are related to the UI (e.g. depend on user locale and thus need to be translated via `strings.xml`).
|
||||
* `spackle-lib` — Random utilities, to fill in the cracks in the Kotlin and Android frameworks.
|
||||
|
||||
The following diagram shows a rough depiction of dependencies between the modules. Two notes on this diagram:
|
||||
|
@ -44,8 +45,9 @@ The following diagram shows a rough depiction of dependencies between the module
|
|||
subgraph sdk
|
||||
sdkLib[[sdk-lib]];
|
||||
sdkExtLib[[sdk-ext-lib]];
|
||||
sdkExtUI[[sdk-ext-ui]];
|
||||
end
|
||||
sdkLib[[sdk-lib]] --> sdkExtLib[[sdk-ext-lib]];
|
||||
sdkLib[[sdk-lib]] --> sdkExtLib[[sdk-ext-lib]] --> sdkExtUI[[sdk-ext-ui]];
|
||||
subgraph preference
|
||||
preference-api-lib[[preference-api-lib]];
|
||||
preference-impl-android-lib[[preference-impl-android-lib]];
|
||||
|
|
|
@ -75,6 +75,7 @@ fun Zatoshi.Companion.fromZecString(zecString: String, monetarySeparators: Monet
|
|||
}
|
||||
val localizedPattern = "#${monetarySeparators.grouping}##0${monetarySeparators.decimal}0#"
|
||||
|
||||
// TODO [#321]: https://github.com/zcash/secant-android-wallet/issues/321
|
||||
val decimalFormat = DecimalFormat(localizedPattern, symbols).apply {
|
||||
isParseBigDecimal = true
|
||||
roundingMode = RoundingMode.HALF_EVEN // aka Bankers rounding
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("zcash.android-build-conventions")
|
||||
}
|
||||
|
||||
android {
|
||||
// TODO [#6]: Figure out how to move this into the build-conventions
|
||||
kotlinOptions {
|
||||
jvmTarget = libs.versions.java.get()
|
||||
allWarningsAsErrors = project.property("ZCASH_IS_TREAT_WARNINGS_AS_ERRORS").toString().toBoolean()
|
||||
freeCompilerArgs = freeCompilerArgs.plus("-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.sdkExtLib)
|
||||
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
androidTestImplementation(libs.kotlin.test)
|
||||
|
||||
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
||||
androidTestUtil(libs.androidx.test.orchestrator) {
|
||||
artifact {
|
||||
type = "apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="cash.z.ecc.sdk.ext"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:label="sdk-ext-ui-test"/>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,101 @@
|
|||
package cash.z.ecc.sdk.ext.ui.regex
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.ext.ui.R
|
||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
||||
import cash.z.ecc.sdk.ext.ui.test.getStringResourceWithArgs
|
||||
import cash.z.ecc.sdk.model.MonetarySeparators
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ZecStringExtTest {
|
||||
|
||||
companion object {
|
||||
private val EN_US_SEPARATORS = MonetarySeparators(',', '.')
|
||||
}
|
||||
|
||||
private fun getContinuousRegex(): Regex {
|
||||
return getStringResourceWithArgs(
|
||||
R.string.zec_amount_regex_continuous_filter,
|
||||
arrayOf(
|
||||
EN_US_SEPARATORS.grouping,
|
||||
EN_US_SEPARATORS.decimal
|
||||
)
|
||||
).toRegex()
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_regex_validity() {
|
||||
val regexString = getStringResourceWithArgs(
|
||||
R.string.zec_amount_regex_continuous_filter,
|
||||
arrayOf(
|
||||
EN_US_SEPARATORS.grouping,
|
||||
EN_US_SEPARATORS.decimal
|
||||
)
|
||||
)
|
||||
assertNotNull(regexString)
|
||||
|
||||
val regexAmountChecker = regexString.toRegex()
|
||||
|
||||
regexAmountChecker.also {
|
||||
assertNotNull(regexAmountChecker)
|
||||
assertTrue(regexAmountChecker.pattern.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_regex_functionality_valid_inputs() {
|
||||
getContinuousRegex().also {
|
||||
assertTrue(it.matches(""))
|
||||
assertTrue(it.matches("123"))
|
||||
assertTrue(it.matches("${EN_US_SEPARATORS.decimal}"))
|
||||
assertTrue(it.matches("${EN_US_SEPARATORS.decimal}123"))
|
||||
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}"))
|
||||
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456"))
|
||||
assertTrue(it.matches("123${EN_US_SEPARATORS.decimal}"))
|
||||
assertTrue(it.matches("123${EN_US_SEPARATORS.decimal}456"))
|
||||
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456${EN_US_SEPARATORS.decimal}"))
|
||||
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456${EN_US_SEPARATORS.decimal}789"))
|
||||
assertTrue(it.matches("1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}567${EN_US_SEPARATORS.decimal}00"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_regex_functionality_invalid_inputs() {
|
||||
getContinuousRegex().also {
|
||||
assertFalse(it.matches("aaa"))
|
||||
assertFalse(it.matches("123aaa"))
|
||||
assertFalse(it.matches("${EN_US_SEPARATORS.grouping}"))
|
||||
assertFalse(it.matches("${EN_US_SEPARATORS.grouping}123"))
|
||||
assertFalse(it.matches("123${EN_US_SEPARATORS.grouping}${EN_US_SEPARATORS.grouping}"))
|
||||
assertFalse(it.matches("123${EN_US_SEPARATORS.decimal}${EN_US_SEPARATORS.decimal}"))
|
||||
assertFalse(it.matches("1${EN_US_SEPARATORS.grouping}2${EN_US_SEPARATORS.grouping}3"))
|
||||
assertFalse(it.matches("1${EN_US_SEPARATORS.decimal}2${EN_US_SEPARATORS.decimal}3"))
|
||||
assertFalse(it.matches("1${EN_US_SEPARATORS.decimal}2${EN_US_SEPARATORS.grouping}3"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_digits_between_grouping_separators_valid_test() {
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "123"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}5"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}567${EN_US_SEPARATORS.grouping}8"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_digits_between_grouping_separators_invalid_test() {
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}1${EN_US_SEPARATORS.grouping}2"))
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}12${EN_US_SEPARATORS.grouping}3"))
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}1234${EN_US_SEPARATORS.grouping}"))
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}123${EN_US_SEPARATORS.grouping}4${EN_US_SEPARATORS.grouping}"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package cash.z.ecc.sdk.ext.ui.test
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import java.util.Locale
|
||||
|
||||
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
|
||||
|
||||
fun getStringResourceWithArgs(@StringRes resId: Int, formatArgs: Array<Any>) = ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs)
|
||||
|
||||
fun isLocaleRTL(locale: Locale) = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="cash.z.ecc.sdk.ext.ui">
|
||||
|
||||
<application />
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,57 @@
|
|||
package cash.z.ecc.sdk.ext.ui
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.sdk.model.MonetarySeparators
|
||||
|
||||
object ZecStringExt {
|
||||
|
||||
private const val DIGITS_BETWEEN_GROUP_SEPARATORS = 3
|
||||
|
||||
/**
|
||||
* Builds filter with current local monetary separators for continuous input checking. Solution
|
||||
* is build upon regex validation and character checking.
|
||||
*
|
||||
* Regex example: ^([0-9]*([0-9]+([,]$|[,][0-9]+))*([.]$|[.][0-9]+)?)?$
|
||||
* Inputs may differ according to user locale.
|
||||
*
|
||||
* Valid amounts: "" . | .123 | 123, | 123. | 123,456 | 123.456 | 123,456.789 | 123,456,789 | 123,456,789.123 | etc.
|
||||
* Invalid amounts: 123,, | 123,. | 123.. | .123 | ,123 | 123.456.789 | etc.
|
||||
*
|
||||
* @param context used for loading localized pattern from strings.xml
|
||||
* @param separators which consist of localized monetary separators
|
||||
* @param zecString to be validated
|
||||
*
|
||||
* @return true in case of validation success, false otherwise
|
||||
*/
|
||||
fun filterContinuous(context: Context, separators: MonetarySeparators, zecString: String): Boolean {
|
||||
if (!context.getString(
|
||||
R.string.zec_amount_regex_continuous_filter,
|
||||
separators.grouping,
|
||||
separators.decimal
|
||||
).toRegex().matches(zecString) || !checkFor3Digits(separators, zecString)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for at least 3 digits between grouping separators.
|
||||
*
|
||||
* @param separators which consist of localized monetary separators
|
||||
* @param zecString to be validated
|
||||
*
|
||||
* @return true in case of validation success, false otherwise
|
||||
*/
|
||||
fun checkFor3Digits(separators: MonetarySeparators, zecString: String): Boolean {
|
||||
if (zecString.count { it == separators.grouping } >= 2) {
|
||||
val groups = zecString.split(separators.grouping)
|
||||
for (i in 1 until (groups.size - 1)) {
|
||||
if (groups[i].length != DIGITS_BETWEEN_GROUP_SEPARATORS) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="zec_amount_regex_continuous_filter" formatted="true" tools:ignore="TypographyDashes">^([0-9]*([0-9]+([<xliff:g id="group" example=",">%1$s</xliff:g>]$|[<xliff:g id="group" example=",">%1$s</xliff:g>][0-9]+))*([<xliff:g id="dec" example=".">%2$s</xliff:g>]$|[<xliff:g id="dec" example=".">%2$s</xliff:g>][0-9]+)?)?$</string>
|
||||
</resources>
|
|
@ -210,6 +210,7 @@ include("build-info-lib")
|
|||
include("preference-api-lib")
|
||||
include("preference-impl-android-lib")
|
||||
include("sdk-ext-lib")
|
||||
include("sdk-ext-ui")
|
||||
include("spackle-lib")
|
||||
include("ui-design-lib")
|
||||
include("ui-lib")
|
||||
|
|
|
@ -63,6 +63,7 @@ dependencies {
|
|||
implementation(projects.preferenceApiLib)
|
||||
implementation(projects.preferenceImplAndroidLib)
|
||||
implementation(projects.sdkExtLib)
|
||||
implementation(projects.sdkExtUi)
|
||||
implementation(projects.spackleLib)
|
||||
implementation(projects.uiDesignLib)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createComposeRule
|
|||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
|
@ -20,7 +21,6 @@ import co.electriccoin.zcash.ui.test.getStringResource
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
@ -38,9 +38,7 @@ class RequestViewTest {
|
|||
@Suppress("UNUSED_VARIABLE")
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.assertIsNotEnabled()
|
||||
}
|
||||
composeTestRule.assertSendDisabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -52,22 +50,16 @@ class RequestViewTest {
|
|||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastCreateZecRequest())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
composeTestRule.setValidAmount()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
composeTestRule.clickCreateAndSend()
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
testSetup.getLastCreateZecRequest().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.address)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertTrue(it.message.value.isEmpty())
|
||||
}
|
||||
}
|
||||
|
@ -81,49 +73,91 @@ class RequestViewTest {
|
|||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastCreateZecRequest())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
composeTestRule.setValidAmount()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
}
|
||||
composeTestRule.setValidMessage()
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_message)).also {
|
||||
it.performTextInput(ZecRequestFixture.MESSAGE.value)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
composeTestRule.clickCreateAndSend()
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
testSetup.getLastCreateZecRequest().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.address)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.message.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("https://github.com/zcash/secant-android-wallet/issues/218")
|
||||
fun create_request_illegal_input() {
|
||||
fun check_regex_functionality_valid_inputs() {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastCreateZecRequest())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
composeTestRule.setAmount("123")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
it.performTextInput("{${separators.decimal}}1{${separators.decimal}}2{${separators.decimal}}3{${separators.decimal}}4")
|
||||
}
|
||||
// e.g. 123,
|
||||
composeTestRule.setAmount("123${separators.grouping}")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(2, testSetup.getOnCreateCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
// e.g. 123.
|
||||
composeTestRule.setAmount("123${separators.decimal}")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(3, testSetup.getOnCreateCount())
|
||||
|
||||
// e.g. 123,456.
|
||||
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(4, testSetup.getOnCreateCount())
|
||||
|
||||
// e.g. 123,456.789
|
||||
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}789")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(5, testSetup.getOnCreateCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun check_regex_functionality_invalid_inputs() {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastCreateZecRequest())
|
||||
|
||||
composeTestRule.setAmount("aaa")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
|
||||
composeTestRule.setAmount("123aaa")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
|
||||
// e.g. ,.
|
||||
composeTestRule.setAmount("${separators.grouping}${separators.decimal}")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
|
||||
// e.g. 123,.
|
||||
composeTestRule.setAmount("123${separators.grouping}${separators.decimal}")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
|
||||
// e.g. 1,2,3
|
||||
composeTestRule.setAmount("1${separators.grouping}2${separators.grouping}3")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
|
||||
// e.g. 1.2.3
|
||||
composeTestRule.setAmount("1${separators.decimal}2${separators.decimal}3")
|
||||
composeTestRule.clickCreateAndSend()
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
}
|
||||
|
||||
|
@ -133,32 +167,24 @@ class RequestViewTest {
|
|||
fun max_message_length() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
composeTestRule.setValidAmount()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_message)).also {
|
||||
val input = buildString {
|
||||
repeat(ZecRequestMessage.MAX_MESSAGE_LENGTH + 1) { _ ->
|
||||
append("$it")
|
||||
composeTestRule.setMessage(
|
||||
buildString {
|
||||
repeat(ZecRequestMessage.MAX_MESSAGE_LENGTH + 1) { number ->
|
||||
append("$number")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.performTextInput(input)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
composeTestRule.clickCreateAndSend()
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
testSetup.getLastCreateZecRequest().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.address)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertTrue(it.message.value.isEmpty())
|
||||
}
|
||||
}
|
||||
|
@ -170,9 +196,7 @@ class RequestViewTest {
|
|||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.request_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
composeTestRule.clickBack()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
@ -217,3 +241,50 @@ class RequestViewTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickBack() {
|
||||
onNodeWithContentDescription(getStringResource(R.string.request_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setValidAmount() {
|
||||
onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
it.performTextClearance()
|
||||
it.performTextInput("123${separators.decimal}456")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setAmount(amount: String) {
|
||||
onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(amount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setValidMessage() {
|
||||
onNodeWithText(getStringResource(R.string.request_message)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(ZecRequestFixture.MESSAGE.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setMessage(message: String) {
|
||||
onNodeWithText(getStringResource(R.string.request_message)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickCreateAndSend() {
|
||||
onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.assertSendDisabled() {
|
||||
onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.assertIsNotEnabled()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.view
|
||||
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
|
@ -21,7 +23,6 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
|||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
@ -65,7 +66,7 @@ class SendViewTest {
|
|||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertTrue(it.memo.value.isEmpty())
|
||||
}
|
||||
}
|
||||
|
@ -81,10 +82,7 @@ class SendViewTest {
|
|||
|
||||
composeTestRule.setValidAmount()
|
||||
composeTestRule.setValidAddress()
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextInput(MemoFixture.MEMO_STRING)
|
||||
}
|
||||
composeTestRule.setValidMemo()
|
||||
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
|
@ -95,31 +93,96 @@ class SendViewTest {
|
|||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("https://github.com/zcash/secant-android-wallet/issues/218")
|
||||
fun create_request_illegal_amount() {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun check_regex_functionality_valid_inputs() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}1{${separators.decimal}}2{${separators.decimal}}3{${separators.decimal}}4")
|
||||
}
|
||||
|
||||
composeTestRule.setValidAmount()
|
||||
composeTestRule.setValidAddress()
|
||||
composeTestRule.setValidMemo()
|
||||
composeTestRule.assertSendEnabled()
|
||||
|
||||
composeTestRule.setAmount("123")
|
||||
composeTestRule.assertSendEnabled()
|
||||
|
||||
// e.g. 123,
|
||||
composeTestRule.setAmount("123${separators.grouping}")
|
||||
composeTestRule.assertSendEnabled()
|
||||
|
||||
// e.g. 123.
|
||||
composeTestRule.setAmount("123${separators.decimal}")
|
||||
composeTestRule.assertSendEnabled()
|
||||
|
||||
// e.g. 123,456.
|
||||
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}")
|
||||
composeTestRule.assertSendEnabled()
|
||||
|
||||
// e.g. 123,456.789
|
||||
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}789")
|
||||
composeTestRule.assertSendEnabled()
|
||||
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
composeTestRule.clickConfirmation()
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun check_regex_functionality_invalid_inputs() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
composeTestRule.setAmount("aaa")
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
composeTestRule.setAmount("123aaa")
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
// e.g. ,.
|
||||
composeTestRule.setAmount("${separators.grouping}${separators.decimal}")
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
// e.g. 123,.
|
||||
composeTestRule.setAmount("123${separators.grouping}${separators.decimal}")
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
// e.g. 1,2,3
|
||||
composeTestRule.setAmount("1${separators.grouping}2${separators.grouping}3")
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
// e.g. 1.2.3
|
||||
composeTestRule.setAmount("1${separators.decimal}2${separators.decimal}3")
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
composeTestRule.assertSendDisabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -137,9 +200,7 @@ class SendViewTest {
|
|||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextInput(input)
|
||||
}
|
||||
composeTestRule.setMemo(input)
|
||||
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
|
@ -150,7 +211,7 @@ class SendViewTest {
|
|||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertTrue(it.memo.value.isEmpty())
|
||||
}
|
||||
}
|
||||
|
@ -235,17 +296,39 @@ private fun ComposeContentTestRule.clickBack() {
|
|||
private fun ComposeContentTestRule.setValidAmount() {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
it.performTextClearance()
|
||||
it.performTextInput("123${separators.decimal}456")
|
||||
}
|
||||
}
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
private fun ComposeContentTestRule.setAmount(amount: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(amount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setValidAddress() {
|
||||
onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setValidMemo() {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(MemoFixture.MEMO_STRING)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setMemo(memo: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(memo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickCreateAndSend() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.performClick()
|
||||
|
@ -269,3 +352,15 @@ private fun ComposeContentTestRule.assertOnConfirmation() {
|
|||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.assertSendEnabled() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertIsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.assertSendDisabled() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertIsNotEnabled()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,3 +5,5 @@ import androidx.annotation.StringRes
|
|||
import androidx.test.core.app.ApplicationProvider
|
||||
|
||||
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
|
||||
|
||||
fun getStringResourceWithArgs(@StringRes resId: Int, formatArgs: Array<Any>) = ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs)
|
||||
|
|
|
@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk
|
|||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.sdk.model.Zatoshi
|
||||
|
||||
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
||||
data class WalletSnapshot(
|
||||
val status: Synchronizer.Status,
|
||||
val processorInfo: CompactBlockProcessor.ProcessorInfo,
|
||||
|
|
|
@ -42,6 +42,7 @@ import kotlinx.coroutines.withContext
|
|||
|
||||
// To make this more multiplatform compatible, we need to remove the dependency on Context
|
||||
// for loading the preferences.
|
||||
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
||||
class WalletViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val walletCoordinator = co.electriccoin.zcash.global.WalletCoordinator.getInstance(application)
|
||||
|
||||
|
|
|
@ -19,9 +19,11 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
|
@ -90,11 +92,13 @@ 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,
|
||||
onCreateAndSend: (ZecRequest) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val monetarySeparators = MonetarySeparators.current()
|
||||
val allowedCharacters = ZecString.allowedCharacters(monetarySeparators)
|
||||
|
||||
|
@ -102,10 +106,13 @@ private fun RequestMainContent(
|
|||
var message by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
// TODO [#289]: Crash occurs while typed more than some acceptable amount to this field.
|
||||
TextField(
|
||||
value = amountZecString,
|
||||
onValueChange = { newValue ->
|
||||
// TODO [#218]: this doesn't prevent illegal input. So users could still type `1.2.3.4`
|
||||
if (!ZecStringExt.filterContinuous(context, monetarySeparators, newValue)) {
|
||||
return@TextField
|
||||
}
|
||||
amountZecString = newValue.filter { allowedCharacters.contains(it) }
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
|
|
|
@ -247,6 +247,7 @@ private fun ChipGridWithText(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO [#288]: TextField component can't do long-press backspace.
|
||||
@Composable
|
||||
private fun NextWordTextField(modifier: Modifier = Modifier, text: String, setText: (String) -> Unit) {
|
||||
/*
|
||||
|
|
|
@ -22,10 +22,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import cash.z.ecc.sdk.model.Memo
|
||||
import cash.z.ecc.sdk.model.MonetarySeparators
|
||||
|
@ -134,12 +136,16 @@ 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
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SendForm(
|
||||
myBalance: Zatoshi,
|
||||
previousZecSend: ZecSend?,
|
||||
onCreateAndSend: (ZecSend) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val monetarySeparators = MonetarySeparators.current()
|
||||
val allowedCharacters = ZecString.allowedCharacters(monetarySeparators)
|
||||
|
||||
|
@ -161,7 +167,9 @@ private fun SendForm(
|
|||
TextField(
|
||||
value = amountZecString,
|
||||
onValueChange = { newValue ->
|
||||
// TODO [#218]: this doesn't prevent illegal input. So users could still type `1.2.3.4`
|
||||
if (!ZecStringExt.filterContinuous(context, monetarySeparators, newValue)) {
|
||||
return@TextField
|
||||
}
|
||||
amountZecString = newValue.filter { allowedCharacters.contains(it) }
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
|
|
Loading…
Reference in New Issue