From edcd763786e2f71667a8be2bbc8e2ffd9bd3806c Mon Sep 17 00:00:00 2001 From: nikita-kladov Date: Fri, 21 Apr 2023 14:59:15 +0300 Subject: [PATCH] add currency mask --- .../div/core/util/mask/CurrencyInputMask.kt | 206 +++++++++++ .../core/util/mask/FixedLengthInputMask.kt | 4 +- .../div/core/view2/divs/DivInputBinder.kt | 63 +++- .../div/core/util/mask/CurrencyMaskTest.kt | 328 ++++++++++++++++++ .../util/mask/TestFixedLengthInputMask.kt | 2 +- .../currency_input_mask.json | 105 ++++++ test_data/regression_test_data/index.json | 10 + 7 files changed, 708 insertions(+), 10 deletions(-) create mode 100644 client/android/div/src/main/java/com/yandex/div/core/util/mask/CurrencyInputMask.kt create mode 100644 client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt create mode 100644 test_data/regression_test_data/currency_input_mask.json diff --git a/client/android/div/src/main/java/com/yandex/div/core/util/mask/CurrencyInputMask.kt b/client/android/div/src/main/java/com/yandex/div/core/util/mask/CurrencyInputMask.kt new file mode 100644 index 000000000..4e4c6e059 --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/util/mask/CurrencyInputMask.kt @@ -0,0 +1,206 @@ +package com.yandex.div.core.util.mask + +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.text.NumberFormat +import java.util.Locale +import kotlin.math.abs + +internal class CurrencyInputMask( + locale: Locale, + private val onError: ((Exception) -> Unit) +) : BaseInputMask(MaskData("", emptyList(), false)) { + private val currencyKey = '¤' + + private val separators = listOf('.', ',') + + private var currencyFormatter: NumberFormat = NumberFormat + .getCurrencyInstance(locale) + .clearFormatter() + + private val decimalFormatSymbols: DecimalFormatSymbols + get() = (currencyFormatter as DecimalFormat).decimalFormatSymbols + + private val String.withNbsp: String + get() = replace(' ', ' ') + + fun updateCurrencyParams(locale: Locale) { + val currentValue = currencyFormatter.parse(value) ?: 0 + + currencyFormatter = NumberFormat.getCurrencyInstance(locale).clearFormatter() + + invalidateMaskDataForFormatted(currentValue) + + applyChangeFrom(currencyFormatter.format(currentValue)) + } + + private fun invalidateMaskDataForFormatted(forValue: Number) { + val formatted = currencyFormatter.format(forValue) + val maskPattern = formatPattern(formatted) + + val decoding = listOf( + MaskKey( + key = '#', + filter = "\\d", + placeholder = '0' + ), + MaskKey( + key = decimalFormatSymbols.decimalSeparator, + filter = "[${decimalFormatSymbols.decimalSeparator}]", + placeholder = decimalFormatSymbols.decimalSeparator + ) + ) + + updateMaskData(MaskData(maskPattern, decoding, maskData.alwaysVisible), false) + } + + override fun applyChangeFrom(newValue: String, position: Int?) { + val diff = TextDiff.build(value, newValue.withNbsp) + + val decimalSeparator = decimalFormatSymbols.decimalSeparator + val oldSeparatorIndex = value.indexOfLast { it == decimalSeparator } + val newSeparatorIndex = newValue.indexOfLast { it == decimalSeparator } + val needInvalidateMask = oldSeparatorIndex != newSeparatorIndex || + (oldSeparatorIndex == -1 && newSeparatorIndex == -1) + + val clearedValue = newValue.toValidFormat(diff) + + cleanup(diff) + + val rawValue = currencyFormatter.parse(clearedValue) ?: 0 + + if (needInvalidateMask) { + invalidateMaskDataForFormatted(rawValue) + } + + replaceChars(clearedValue, 0) + + cursorPosition = + if (value.length > diff.start && value[diff.start] == decimalFormatSymbols.groupingSeparator) { + position ?: cursorPosition + } else { + abs(value.length - (newValue.length - (position ?: cursorPosition))) + } + } + + private fun NumberFormat.clearFormatter(): NumberFormat { + (this as? DecimalFormat)?.apply { + val pattern = toPattern().filter { it != currencyKey }.trim() + applyPattern(pattern) + } + + return this + } + + private fun String.toValidFormat(diff: TextDiff): String { + if (isBlank()) return decimalFormatSymbols.zeroDigit.toString() + + val separatorChar = decimalFormatSymbols.decimalSeparator + + var separatorOutOfDiffIndex: Int = -1 + + var index = 0 + + while (index < this.length) { + if (this[index] == separatorChar && !inDiff(diff, index)) { + separatorOutOfDiffIndex = index + + break + } + + index++ + } + + val replaceCharInDiff = + if (diff.added == 1 && diff.removed == 0) { + val diffChar = this[diff.start] + + if (diffChar in separators) diff.start else -1 + } else -1 + + val maxSeparatorOffset = currencyFormatter.maximumFractionDigits + + var leftToInsert = maxSeparatorOffset + + if (separatorOutOfDiffIndex != -1) { + index = separatorOutOfDiffIndex + + while (index < length) { + if (this[index].isDigit() && !inDiff(diff, index)) leftToInsert-- + index++ + } + } else { + var oldSeparatorLeft = false + + forEachIndexed { i, char -> + val inDiff = inDiff(diff, i) + + when { + char == separatorChar -> { + oldSeparatorLeft = true + } + !inDiff && oldSeparatorLeft && char.isDigit() -> { + leftToInsert-- + } + } + } + } + + val containsSeparator = contains(separatorChar) || replaceCharInDiff != -1 + + return buildString { + index = this@toValidFormat.length - 1 + + var separatorInserted = false + + while (index >= 0) { + val char = this@toValidFormat[index] + + val canInsertSeparator = length <= maxSeparatorOffset + + if (char.isDigit()) { + if (inDiff(diff, index) && !separatorInserted && containsSeparator) { + if (leftToInsert > 0) { + append(char) + + leftToInsert-- + } + } else { + append(char) + } + } else if (canInsertSeparator && separatorOutOfDiffIndex == -1 && index == replaceCharInDiff) { + append(separatorChar) + + separatorInserted = true + } else if (canInsertSeparator && char == separatorChar && (separatorOutOfDiffIndex == index || separatorOutOfDiffIndex == -1)) { + append(separatorChar) + + separatorInserted = true + separatorOutOfDiffIndex = index + } + + index-- + } + } + .reversed() + .trimStart(decimalFormatSymbols.zeroDigit) + } + + private fun inDiff(diff: TextDiff, index: Int): Boolean = + diff.start <= index && index < diff.start + diff.added + + private fun formatPattern(pattern: String): String { + return buildString { + pattern.forEach { char -> + when { + char.isDigit() -> append('#') + else -> append(char) + } + } + } + } + + override fun onException(exception: Exception) { + onError.invoke(exception) + } +} diff --git a/client/android/div/src/main/java/com/yandex/div/core/util/mask/FixedLengthInputMask.kt b/client/android/div/src/main/java/com/yandex/div/core/util/mask/FixedLengthInputMask.kt index 11f0eb860..187c42879 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/util/mask/FixedLengthInputMask.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/util/mask/FixedLengthInputMask.kt @@ -2,9 +2,9 @@ package com.yandex.div.core.util.mask internal open class FixedLengthInputMask( initialMaskData: MaskData, - private val onError: ((Exception) -> Unit)? = null + private val onError: ((Exception) -> Unit) ) : BaseInputMask(initialMaskData) { override fun onException(exception: Exception) { - onError?.invoke(exception) + onError.invoke(exception) } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivInputBinder.kt b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivInputBinder.kt index 4fc12151e..1ffabb5f3 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivInputBinder.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivInputBinder.kt @@ -2,12 +2,14 @@ package com.yandex.div.core.view2.divs import android.graphics.drawable.Drawable import android.text.InputType +import android.text.method.DigitsKeyListener import android.view.View import android.widget.EditText import android.widget.TextView import com.yandex.div.core.dagger.DivScope import com.yandex.div.core.expression.variables.TwoWayStringVariableBinder import com.yandex.div.core.util.mask.BaseInputMask +import com.yandex.div.core.util.mask.CurrencyInputMask import com.yandex.div.core.util.mask.FixedLengthInputMask import com.yandex.div.core.util.toIntSafely import com.yandex.div.core.view2.Div2View @@ -16,9 +18,11 @@ import com.yandex.div.core.view2.DivViewBinder import com.yandex.div.core.view2.divs.widgets.DivInputView import com.yandex.div.core.view2.errors.ErrorCollectors import com.yandex.div.json.expressions.ExpressionResolver +import com.yandex.div2.DivCurrencyInputMask import com.yandex.div2.DivFixedLengthInputMask import com.yandex.div2.DivInput import com.yandex.div2.DivSizeUnit +import java.util.Locale import java.util.regex.PatternSyntaxException import javax.inject.Inject @@ -278,11 +282,24 @@ internal class DivInputBinder @Inject constructor( val errorCollector = errorCollectors.getOrCreate(divView.dataTag, divView.divData) + val defaultKeyListener = keyListener + + val catchCommonMaskException = { exception: Exception, other: () -> Unit -> + when (exception) { + is PatternSyntaxException -> errorCollector.logError( + IllegalArgumentException("Invalid regex pattern '${exception.pattern}'.") + ) + else -> other() + } + } + val updateMaskData = { _: Any -> val divInputMask = div.mask?.value() inputMask = when (divInputMask) { is DivFixedLengthInputMask -> { + keyListener = defaultKeyListener + val maskData = BaseInputMask.MaskData( divInputMask.pattern.evaluate(resolver), divInputMask.patternElements.map { @@ -296,14 +313,41 @@ internal class DivInputBinder @Inject constructor( ) inputMask?.apply { updateMaskData(maskData) } ?: FixedLengthInputMask(maskData) { - when (it) { - is PatternSyntaxException -> errorCollector.logError( - IllegalArgumentException("Invalid regex pattern '${it.pattern}'.") - ) - } + catchCommonMaskException(it) { } } } - else -> null + is DivCurrencyInputMask -> { + val evaluatedLocaleTag = divInputMask.locale?.evaluate(resolver) + + val locale = if (evaluatedLocaleTag != null) { + Locale.forLanguageTag(evaluatedLocaleTag) + .apply { + val finalLanguageTag = toLanguageTag() + + if (finalLanguageTag != evaluatedLocaleTag) { + val exception = IllegalArgumentException("Original locale tag '$evaluatedLocaleTag' is not equals to final one '$finalLanguageTag'") + + errorCollector.logWarning(exception) + } + } + } else { + Locale.getDefault() + } + + keyListener = DigitsKeyListener.getInstance("1234567890.,") + + inputMask?.apply { + (inputMask as CurrencyInputMask) + .updateCurrencyParams(locale) + } ?: CurrencyInputMask(locale) { + catchCommonMaskException(it) { } + } + } + else -> { + keyListener = defaultKeyListener + + null + } } onMaskUpdate(inputMask) @@ -311,7 +355,7 @@ internal class DivInputBinder @Inject constructor( when (val inputMask = div.mask?.value()) { is DivFixedLengthInputMask -> { - addSubscription(inputMask.pattern.observeAndGet(resolver, updateMaskData)) + addSubscription(inputMask.pattern.observe(resolver, updateMaskData)) inputMask.patternElements.forEach { patternElement -> addSubscription(patternElement.key.observe(resolver, updateMaskData)) patternElement.regex?.let { addSubscription(it.observe(resolver, updateMaskData)) } @@ -319,7 +363,12 @@ internal class DivInputBinder @Inject constructor( } addSubscription(inputMask.alwaysVisible.observe(resolver, updateMaskData)) } + is DivCurrencyInputMask -> { + inputMask.locale?.observe(resolver, updateMaskData)?.let { addSubscription(it) } + } else -> Unit } + + updateMaskData(Unit) } } diff --git a/client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt b/client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt new file mode 100644 index 000000000..b3543e81f --- /dev/null +++ b/client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt @@ -0,0 +1,328 @@ +package com.yandex.div.core.util.mask + +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.util.Locale + +@RunWith(RobolectricTestRunner::class) +class CurrencyMaskTest { + private val russianLocale = Locale.forLanguageTag("ru-RU") + private val deutschLocale = Locale.forLanguageTag("de-DE") + private val englishLocale = Locale.forLanguageTag("en-US") + + @Test + fun `insert decimal to empty string`() { + val currencyMask = createCurrencyMask() + + currencyMask.applyChangesFromWithNbsp("1", 1) + + currencyMask.assertMask("1", 1) + } + + @Test + fun `insert multiple decimals at once to empty string`() { + val currencyMask = createCurrencyMask() + + currencyMask.applyChangesFromWithNbsp("12345", 5) + + currencyMask.assertMask("12 345", 6) + } + + @Test + fun `insert one decimal`() { + val currencyMask = createCurrencyMask().withValue("1") + + currencyMask.applyChangesFromWithNbsp("12", 2) + + currencyMask.assertMask("12", 2) + } + + @Test + fun `insert multiple decimals`() { + val currencyMask = createCurrencyMask().withValue("1") + + currencyMask.applyChangesFromWithNbsp("12345", 5) + + currencyMask.assertMask("12 345", 6) + } + + @Test + fun `remove one decimal`() { + val currencyMask = createCurrencyMask().withValue("12345") + + currencyMask.applyChangesFromWithNbsp("12 45", 3) + + currencyMask.assertMask("1 245", 3) + } + + @Test + fun `remove multiple decimals`() { + val currencyMask = createCurrencyMask().withValue("123456") + + currencyMask.applyChangesFromWithNbsp("12 56", 3) + + currencyMask.assertMask("1 256", 3) + } + + @Test + fun `deutsch currency mask`() { + val currencyMask = createCurrencyMask(deutschLocale) + + currencyMask.applyChangesFromWithNbsp("1.234,56", 8) + + currencyMask.assertMask("1.234,56", 8) + } + + @Test + fun `english currency mask`() { + val currencyMask = createCurrencyMask(englishLocale) + + currencyMask.applyChangesFromWithNbsp("1,234.56", 8) + + currencyMask.assertMask("1,234.56", 8) + } + + @Test + fun `insert comma english currency mask`() { + val currencyMask = createCurrencyMask(englishLocale).withValue("123456") + + currencyMask.applyChangesFromWithNbsp("123,4,56", 6) + + currencyMask.assertMask("1,234.56", 6) + } + + @Test + fun `input separator`() { + val currencyMask = createCurrencyMask().withValue("1234") + + currencyMask.applyChangesFromWithNbsp("1 234,", 6) + + currencyMask.assertMask("1 234,", 6) + } + + @Test + fun `remove separator`() { + val currencyMask = createCurrencyMask().withValue("1234,") + + currencyMask.applyChangesFromWithNbsp("1 234", 5) + + currencyMask.assertMask("1 234", 5) + } + + @Test + fun `insert decimal after separator`() { + val currencyMask = createCurrencyMask().withValue("1234,") + + currencyMask.applyChangesFromWithNbsp("1 234,5", 7) + + currencyMask.assertMask("1 234,5", 7) + } + + @Test + fun `remove decimal after separator`() { + val currencyMask = createCurrencyMask().withValue("1234,56") + + currencyMask.applyChangesFromWithNbsp("1 234,6", 6) + + currencyMask.assertMask("1 234,6", 6) + } + + @Test + fun `insert separator with overflow not allowed`() { + val currencyMask = createCurrencyMask().withValue("12345678") + + currencyMask.applyChangesFromWithNbsp("12 34,5 678", 6) + + currencyMask.assertMask("12 345 678", 5) + } + + @Test + fun `insert separator without overflow allowed`() { + val currencyMask = createCurrencyMask().withValue("123456") + + currencyMask.applyChangesFromWithNbsp("123 4,56", 6) + + currencyMask.assertMask("1 234,56", 6) + } + + @Test + fun `remove separator from a middle`() { + val currencyMask = createCurrencyMask().withValue("1234,56") + + currencyMask.applyChangesFromWithNbsp("1 23456", 5) + + currencyMask.assertMask("123 456", 5) + } + + @Test + fun `change locale with value`() { + val currencyMask = createCurrencyMask().withValue("1 234,56") + + currencyMask.assertMask("1 234,56", 0) + + currencyMask.updateCurrencyParams(deutschLocale) + + currencyMask.assertMask("1.234,56", 0) + } + + @Test + fun `input dot as separator for deutsch locale`() { + val currencyMask = createCurrencyMask(deutschLocale).withValue("1234567") + + currencyMask.applyChangesFromWithNbsp("1.234.5.67", 8) + + currencyMask.assertMask("12.345,67", 7) + } + + @Test + fun `input dot as separator for english locale`() { + val currencyMask = createCurrencyMask(englishLocale).withValue("1234567") + + currencyMask.applyChangesFromWithNbsp("1,234,5.67", 8) + + currencyMask.assertMask("12,345.67", 7) + } + + @Test + fun `input comma as separator for english locale`() { + val currencyMask = createCurrencyMask(englishLocale).withValue("1234567") + + currencyMask.applyChangesFromWithNbsp("1,234,5,67", 8) + + currencyMask.assertMask("12,345.67", 7) + } + + @Test + fun `input invalid character`() { + val currencyMask = createCurrencyMask().withValue("1234,5") + + currencyMask.applyChangesFromWithNbsp("1 23a4,5", 5) + + currencyMask.assertMask("1 234,5", 4) + } + + @Test + fun `input second separator before one`() { + val currencyMask = createCurrencyMask().withValue("1234,56") + + currencyMask.applyChangesFromWithNbsp("1 2,34,56", 4) + + currencyMask.assertMask("1 234,56", 3) + } + + @Test + fun `input second separator after one`() { + val currencyMask = createCurrencyMask().withValue("1234,56") + + currencyMask.applyChangesFromWithNbsp("1 234,5,6", 8) + + currencyMask.assertMask("1 234,56", 7) + } + + @Test + fun `fully clear field`() { + val currencyMask = createCurrencyMask().withValue("1234,56") + + currencyMask.applyChangesFromWithNbsp("", 0) + + currencyMask.assertMask("0", 1) + } + + @Test + fun `type digit after leading zero`() { + val currencyMask = createCurrencyMask() + + currencyMask.applyChangesFromWithNbsp("01", 1) + + currencyMask.assertMask("1", 0) + } + + @Test + fun `type zero digits as leading`() { + val currencyMask = createCurrencyMask().withValue("1 234") + + currencyMask.applyChangesFromWithNbsp("0 001 234", 4) + + currencyMask.assertMask("1 234", 0) + } + + @Test + fun `type char at end with overflow`() { + val currencyMask = createCurrencyMask().withValue("123,45") + + currencyMask.applyChangesFromWithNbsp("123,465", 6) + + currencyMask.assertMask("123,45", 5) + } + + @Test + fun `type char and separator at end with overflow`() { + val currencyMask = createCurrencyMask().withValue("12345") + + currencyMask.applyChangesFromWithNbsp("12 3,645", 6) + + currencyMask.assertMask("123,45", 4) + } + + @Test + fun `type char and separator at end without overflow`() { + val currencyMask = createCurrencyMask().withValue("123,4") + + currencyMask.applyChangesFromWithNbsp("123,45", 6) + + currencyMask.assertMask("123,45", 6) + } + + @Test + fun `add character in repeated substring`() { + val currencyMask = createCurrencyMask().withValue("11111") + + currencyMask.applyChangesFromWithNbsp("11 1111", 5) + + currencyMask.assertMask("111 111", 5) + } + + @Test + fun `remove character in repeated substring`() { + val currencyMask = createCurrencyMask().withValue("11111") + + currencyMask.applyChangesFromWithNbsp("11 11", 4) + + currencyMask.assertMask("1 111", 4) + } + + @Test + fun `remove cluster separator`() { + val currencyMask = createCurrencyMask().withValue("12345") + + currencyMask.applyChangesFromWithNbsp("12345", 2) + + currencyMask.assertMask("12 345", 2) + } + + private fun createCurrencyMask(locale: Locale = russianLocale): CurrencyInputMask { + return CurrencyInputMask(locale) { } + } + + private fun CurrencyInputMask.applyChangesFromWithNbsp(value: String, position: Int?) { + applyChangeFrom(value.withNbsp(), position) + } + + private fun CurrencyInputMask.withValue(value: String): CurrencyInputMask { + applyChangeFrom(value.withNbsp()) + + return this + } + + private fun CurrencyInputMask.assertMask( + expectedMaskedValue: String, + expectedCursorPosition: Int + ) { + Assert.assertEquals(expectedMaskedValue.withNbsp(), value) + Assert.assertEquals(expectedCursorPosition, cursorPosition) + } + + private fun String.withNbsp(): String = replace(' ', ' ') +} diff --git a/client/android/div/src/test/java/com/yandex/div/core/util/mask/TestFixedLengthInputMask.kt b/client/android/div/src/test/java/com/yandex/div/core/util/mask/TestFixedLengthInputMask.kt index eecb61aea..96a44241c 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/util/mask/TestFixedLengthInputMask.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/util/mask/TestFixedLengthInputMask.kt @@ -2,7 +2,7 @@ package com.yandex.div.core.util.mask internal class TestFixedLengthInputMask( initialMaskData: MaskData -) : FixedLengthInputMask(initialMaskData) { +) : FixedLengthInputMask(initialMaskData, { }) { fun publicCalculateInsertableSubstring(substring: String, start: Int): String = calculateInsertableSubstring(substring, start) diff --git a/test_data/regression_test_data/currency_input_mask.json b/test_data/regression_test_data/currency_input_mask.json new file mode 100644 index 000000000..48dc49212 --- /dev/null +++ b/test_data/regression_test_data/currency_input_mask.json @@ -0,0 +1,105 @@ +{ + "templates": { + "pretty_input": { + "type": "input", + "height": { + "type": "wrap_content" + }, + "background": [ + { + "type": "solid", + "color": "#0e000000" + } + ], + "border": { + "corner_radius": 8 + }, + "margins": { + "left": 16, + "top": 8, + "right": 16, + "bottom": 8 + }, + "paddings": { + "left": 16, + "top": 10, + "right": 16, + "bottom": 10 + }, + "font_size": 16, + "font_weight": "medium", + "text_color": "#000000", + "hint_color": "#888888", + "highlight_color": "#e0bae3", + "line_height": 22 + } + }, + "card": { + "log_id": "ui_test_card", + "variables": [ + { + "name": "input", + "type": "string", + "value": "1234567" + }, + { + "name": "raw_input", + "type": "string", + "value": "12345" + }, + { + "name": "locale", + "type": "string", + "value": "en-US" + } + ], + "states": [ + { + "state_id": 0, + "div": { + "type": "container", + "orientation": "vertical", + "width": { + "type": "match_parent" + }, + "height": { + "type": "wrap_content" + }, + "paddings": { + "top": 8, + "bottom": 8 + }, + "items": [ + { + "type": "text", + "text": "Raw value: @{input}", + "font_size": 20, + "margins": { + "left": 16, + "top": 8, + "right": 16, + "bottom": 8 + } + }, + { + "type": "pretty_input", + "text_variable": "input", + "hint_text": "0", + "keyboard_type": "number", + "mask": { + "type": "currency", + "raw_text_variable": "raw_input", + "locale": "@{locale == '' ? 'en-US' : locale}" + } + }, + { + "type": "pretty_input", + "text_variable": "locale", + "hint_text": "Currency locale" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/test_data/regression_test_data/index.json b/test_data/regression_test_data/index.json index 959ee96d7..092cd6c9d 100644 --- a/test_data/regression_test_data/index.json +++ b/test_data/regression_test_data/index.json @@ -1742,6 +1742,16 @@ "DivInput" ], "file": "phone_input_mask.json" + }, + { + "title": "Currency input mask", + "platforms": [ + "android" + ], + "tags": [ + "DivInput" + ], + "file": "currency_input_mask.json" } ] }