Updated on 2026-08-14

This commit is contained in:
Tangem 2021-01-12 18:21:10 +03:00
commit b080f0b4e6
28 changed files with 605 additions and 214 deletions

View file

@ -74,6 +74,9 @@ dependencies {
implementation 'com.tangem:blockchain:1.123.0'
implementation 'com.tangem:core:1.90.0'
implementation 'com.tangem:sdk:1.90.0'
implementation 'com.tangem:blockchain:1.123.0'
implementation 'com.tangem:core:1.92.0'
implementation 'com.tangem:sdk:1.92.0'
// WebView
implementation "androidx.browser:browser:1.2.0"

View file

@ -1,18 +1,6 @@
[
{
"name": "isWalletPayIdEnabled",
"value": true
},
{
"name": "isSendingToPayIdEnabled",
"value": true
},
{
"name": "isTopUpEnabled",
"value": true
},
{
"name": "isCreatingTwinCardsAllowed",
"value": true
}
]
{
"isWalletPayIdEnabled": true,
"isTopUpEnabled": true,
"isSendingToPayIdEnabled": true,
"isCreatingTwinCardsAllowed": false
}

View file

@ -1,18 +1,6 @@
[
{
"name": "isWalletPayIdEnabled",
"value": true
},
{
"name": "isSendingToPayIdEnabled",
"value": true
},
{
"name": "isTopUpEnabled",
"value": true
},
{
"name": "isCreatingTwinCardsAllowed",
"value": true
}
]
{
"isWalletPayIdEnabled": true,
"isTopUpEnabled": true,
"isSendingToPayIdEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.common
import android.view.View
import android.view.ViewTreeObserver
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class GlobalLayoutStateHandler<T: View>(
private val view: T,
attachImmediately: Boolean = true
) : ViewTreeObserver.OnGlobalLayoutListener {
var onStateChanged: ((T) -> Unit)? = null
private var isAttached: Boolean = false
init {
if (attachImmediately) attach()
}
fun attach() {
if (isAttached) {
Timber.d("Already attached")
return
}
isAttached = true
view.viewTreeObserver.addOnGlobalLayoutListener(this)
}
fun detach() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
isAttached = false
}
override fun onGlobalLayout() {
onStateChanged?.invoke(view)
}
}

View file

@ -6,12 +6,20 @@ import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.commands.common.card.Card
object FirebaseAnalyticsHandler: AnalyticsHandler {
object FirebaseAnalyticsHandler : AnalyticsHandler {
override fun triggerEvent(event: AnalyticsEvent, card: Card?) {
Firebase.analytics
.logEvent(event.event, setCardData(card))
}
fun logException(name: String, throwable: Throwable) {
Firebase.analytics.logEvent(name, bundleOf(
"message" to (throwable.message ?: "none"),
"cause_message" to (throwable.cause?.message ?: "none"),
"stack_trace" to throwable.stackTraceToString()
))
}
private fun setCardData(card: Card?): Bundle {
if (card == null) return bundleOf()
return bundleOf(

View file

@ -5,6 +5,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Spannable
@ -39,6 +40,14 @@ fun View.getString(@StringRes id: Int): String {
return context.getString(id)
}
fun View.getResourceName(): String {
return try {
resources.getResourceEntryName(id)
} catch (ex: Resources.NotFoundException) {
"Not found"
}
}
fun View.show(show: Boolean, invokeBeforeStateChanged: (() -> Unit)? = null) {
return if (show) this.show(invokeBeforeStateChanged)
else this.hide(invokeBeforeStateChanged)

View file

@ -3,9 +3,13 @@ package com.tangem.tap.common.extensions
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.ViewParent
import androidx.core.view.forEach
import androidx.transition.AutoTransition
import androidx.transition.Transition
import androidx.transition.TransitionManager
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.tangem.tap.common.GlobalLayoutStateHandler
import timber.log.Timber
/**
@ -25,4 +29,19 @@ fun ViewParent?.beginDelayedTransition(transition: Transition = AutoTransition()
fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) {
TransitionManager.beginDelayedTransition(this, transition)
}
fun ChipGroup.fitChipsByGroupWidth() {
val layoutStateHandler = GlobalLayoutStateHandler(this)
layoutStateHandler.onStateChanged = stateHandler@{
if (it.childCount < 2) {
layoutStateHandler.detach()
return@stateHandler
}
val spacingBetweenViews = it.chipSpacingHorizontal * (it.childCount - 1)
val width = (it.width - spacingBetweenViews) / it.childCount
it.forEach { chip -> (chip as? Chip)?.width = width }
layoutStateHandler.detach()
}
}

View file

@ -1,12 +1,11 @@
package com.tangem.tap.domain.config
import android.content.Context
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.wallet.BuildConfig
import timber.log.Timber
@ -30,16 +29,13 @@ class LocalLoader(
override fun loadConfig(onComplete: (ConfigModel) -> Unit) {
val config = try {
val featureType = Types.newParameterizedType(List::class.java, FeatureModel::class.java)
val featureAdapter: JsonAdapter<List<FeatureModel>> = moshi.adapter(featureType)
val valuesType = Types.newParameterizedType(List::class.java, ConfigValueModel::class.java)
val valuesAdapter: JsonAdapter<List<ConfigValueModel>> = moshi.adapter(valuesType)
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = readAssetAsString(ConfigLoader.featuresName)
val jsonConfigValues = readAssetAsString(ConfigLoader.configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures) ?: listOf(),
valuesAdapter.fromJson(jsonConfigValues) ?: listOf())
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {
Timber.e(ex)
ConfigModel.empty()
@ -67,14 +63,13 @@ class RemoteLoader(
onComplete(emptyConfig)
return@addOnCompleteListener
}
val featureType = Types.newParameterizedType(List::class.java, FeatureModel::class.java)
val featureAdapter: JsonAdapter<List<FeatureModel>> = moshi.adapter(featureType)
onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig) ?: listOf(), listOf()))
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig), null))
} else {
onComplete(emptyConfig)
}
}.addOnFailureListener {
FirebaseCrashlytics.getInstance().recordException(it)
FirebaseAnalyticsHandler.logException("remote_config_error", it)
onComplete(emptyConfig)
}
}

View file

@ -27,14 +27,13 @@ class ConfigManager(
fun load(onComplete: VoidCallback? = null) {
localLoader.loadConfig { config ->
config.features?.forEach { setupFeature(it.name, it.value) }
config.configValues?.forEach { setupKey(it.name, it.value) }
setupFeature(config.features)
setupKey(config.configValues)
}
remoteLoader.loadConfig { config ->
setupFeature(config.features)
onComplete?.invoke()
}
// remoteLoader.loadConfig { config ->
// config.features?.forEach { setupFeature(it.name, it.value) }
// onComplete?.invoke()
// }
}
fun turnOff(name: String) {
@ -56,44 +55,35 @@ class ConfigManager(
}
}
private fun setupFeature(name: String, value: Boolean) {
val newValue = value ?: return
private fun setupFeature(featureModel: FeatureModel?) {
val model = featureModel ?: return
when (name) {
isWalletPayIdEnabled -> {
config = config.copy(isWalletPayIdEnabled = newValue)
defaultConfig = defaultConfig.copy(isWalletPayIdEnabled = newValue)
}
isSendingToPayIdEnabled -> {
config = config.copy(isSendingToPayIdEnabled = newValue)
defaultConfig = defaultConfig.copy(isSendingToPayIdEnabled = newValue)
}
isTopUpEnabled -> {
config = config.copy(isTopUpEnabled = newValue)
defaultConfig = defaultConfig.copy(isTopUpEnabled = newValue)
}
isCreatingTwinCardsAllowed -> {
config = config.copy(isCreatingTwinCardsAllowed = newValue)
defaultConfig = defaultConfig.copy(isCreatingTwinCardsAllowed = newValue)
}
}
config = config.copy(
isWalletPayIdEnabled = model.isWalletPayIdEnabled,
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
)
defaultConfig = defaultConfig.copy(
isWalletPayIdEnabled = model.isWalletPayIdEnabled,
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
)
}
private fun setupKey(name: String, value: String) {
when (name) {
coinMarketCapKey -> {
config = config.copy(coinMarketCapKey = value)
defaultConfig = defaultConfig.copy(coinMarketCapKey = value)
}
moonPayApiKey -> {
config = config.copy(moonPayApiKey = value)
defaultConfig = defaultConfig.copy(moonPayApiKey = value)
}
moonPayApiSecretKey -> {
config = config.copy(moonPayApiSecretKey = value)
defaultConfig = defaultConfig.copy(moonPayApiSecretKey = value)
}
}
private fun setupKey(configValues: ConfigValueModel?) {
val values = configValues ?: return
config = config.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey
)
}
companion object {

View file

@ -4,23 +4,21 @@ package com.tangem.tap.domain.config
[REDACTED_AUTHOR]
*/
interface BaseConfigModel<V> {
val name: String
val value: V?
}
class FeatureModel(
override val name: String,
override val value: Boolean
) : BaseConfigModel<Boolean>
val isWalletPayIdEnabled: Boolean,
val isTopUpEnabled: Boolean,
val isSendingToPayIdEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
)
class ConfigValueModel(
override val name: String,
override val value: String
) : BaseConfigModel<String>
val coinMarketCapKey: String,
val moonPayApiKey: String,
val moonPayApiSecretKey: String,
)
class ConfigModel(val features: List<FeatureModel>?, val configValues: List<ConfigValueModel>?) {
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {
companion object {
fun empty(): ConfigModel = ConfigModel(listOf(), listOf())
fun empty(): ConfigModel = ConfigModel(null, null)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux
import com.tangem.Message
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
@ -22,7 +23,7 @@ object ReleaseSendState : Action
data class PrepareSendScreen(
val coinAmount: Amount?,
val tokenAmount: Amount? = null
val tokenAmount: Amount? = null,
) : SendScreenAction
// Address or PayId
@ -33,7 +34,27 @@ sealed class AddressPayIdActionUi : SendScreenActionUi {
object CheckAddressPayId : AddressPayIdActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi()
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean): AddressPayIdActionUi()
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean) : AddressPayIdActionUi()
}
sealed class TransactionExtrasAction : SendScreenActionUi {
data class Prepare(
val blockchain: Blockchain,
val walletAddress: String,
val xrpTag: String?,
) : TransactionExtrasAction()
object Release : TransactionExtrasAction()
sealed class XlmMemo : TransactionExtrasAction() {
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
data class HandleUserInput(val data: String) : XlmMemo()
}
sealed class XrpDestinationTag : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : XrpDestinationTag()
}
}
sealed class AddressPayIdVerifyAction : SendScreenAction {
@ -122,6 +143,7 @@ sealed class SendAction : SendScreenAction {
val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal,
) : Dialog()
object Hide : Dialog()
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -82,22 +83,26 @@ internal class AddressPayIdMiddleware {
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
val address = result.data.getAddress()
if (address == null) {
val addressDetails = result.data.getAddressDetails()
if (addressDetails == null) {
dispatch(SetPayIdError(Error.PAY_ID_NOT_REGISTERED))
return@withContext
}
val address = addressDetails.address
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, address)
if (failReason == null) {
dispatch(SetPayIdWalletAddress(payId, address, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, addressDetails.tag))
dispatch(FeeAction.RequestFee)
} else {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
}
}
is Result.Failure -> {
dispatch(SetPayIdError(Error.PAY_ID_REQUEST_FAILED))
dispatch(TransactionExtrasAction.Release)
}
}
}
@ -110,8 +115,10 @@ internal class AddressPayIdMiddleware {
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress)
if (failReason == null) {
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
} else {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
}
}

View file

@ -33,15 +33,13 @@ class RequestFeeMiddleware {
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return
}
val typedAmount = sendState.amountState.amountToExtract ?: return
val recipientAddress = sendState.addressPayIdState.recipientWalletAddress!!
val cryptoSendToRecipient = sendState.amountState.amountToSendCrypto
val recipientAmount = Amount(typedAmount, cryptoSendToRecipient)
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = walletManager as TransactionSender
scope.launch {
val feeResult = txSender.getFee(recipientAmount, recipientAddress)
val feeResult = txSender.getFee(destinationAmount, destinationAddress)
withContext(Dispatchers.Main) {
when (feeResult) {
is Result.Success -> {

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.send.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.Signer
@ -17,6 +19,7 @@ import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.scope
import com.tangem.tap.tangemSdk
import kotlinx.coroutines.Dispatchers
@ -52,7 +55,7 @@ private fun verifyAndSendTransaction(
val sendState = appState?.sendState ?: return
val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return
val card = appState.globalState.scanNoteResponse.card
val recipientAddress = sendState.addressPayIdState.recipientWalletAddress ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val feeAmount = sendState.feeState.currentFee ?: return
@ -67,14 +70,16 @@ private fun verifyAndSendTransaction(
dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false))
dispatch(AmountActionUi.CheckAmountToSend)
}, sendAllCallback = {
sendTransaction(action, walletManager, amountToSend, feeAmount, recipientAddress, card, dispatch)
sendTransaction(action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, dispatch)
}, reduceAmount))
}
transactionErrors.isNotEmpty() -> {
dispatch(SendAction.SendError(createValidateTransactionError(transactionErrors, walletManager)))
}
else -> {
sendTransaction(action, walletManager, amountToSend, feeAmount, recipientAddress, card, dispatch)
sendTransaction(action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, dispatch)
}
}
}
@ -84,12 +89,17 @@ private fun sendTransaction(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
recipientAddress: String,
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: Card,
dispatch: (Action) -> Unit
) {
dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS))
val txData = walletManager.createTransaction(amountToSend, feeAmount, recipientAddress)
var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress)
transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) }
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it)) }
scope.launch {
walletManager.update()
val isLinkedTerminal = tangemSdk.config.linkedTerminal

View file

@ -42,7 +42,7 @@ class AddressPayIdReducer : SendInternalReducer {
viewFieldValue = InputViewValue(action.payId, action.isUserInput),
normalFieldValue = action.payId,
truncatedFieldValue = state.truncate(action.payId),
recipientWalletAddress = action.payIdWalletAddress,
destinationWalletAddress = action.payIdWalletAddress,
error = null
)
}
@ -51,13 +51,13 @@ class AddressPayIdReducer : SendInternalReducer {
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
recipientWalletAddress = action.address,
destinationWalletAddress = action.address,
error = null
)
}
is AddressPayIdVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressVerification.SetAddressError -> state.copy(error = action.error, recipientWalletAddress = null)
is PayIdVerification.SetPayIdError -> state.copy(error = action.error, recipientWalletAddress = null)
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
is PayIdVerification.SetPayIdError -> state.copy(error = action.error, destinationWalletAddress = null)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}

View file

@ -25,6 +25,7 @@ class SendScreenReducer {
val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer()
is ReceiptAction -> ReceiptReducer()

View file

@ -0,0 +1,105 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction.*
import com.tangem.tap.features.send.redux.states.*
/**
[REDACTED_AUTHOR]
*/
class TransactionExtrasReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
return when (action) {
is Prepare -> handleInitialization(action, sendState)
Release -> handleRelease(action, sendState)
is XlmMemo -> handleMemo(action, sendState, sendState.transactionExtrasState)
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
else -> sendState
}
}
private fun handleInitialization(action: Prepare, sendState: SendState): SendState {
val emptyResult = TransactionExtrasState()
val result = when (action.blockchain) {
Blockchain.XRP -> {
// 'r' - without tag, 'x' - with tag
if (action.walletAddress.startsWith("r", true)) {
val tag = action.xrpTag?.toLongOrNull()
if (tag == null) {
TransactionExtrasState(xrpDestinationTag = XrpDestinationTagState())
} else {
TransactionExtrasState(xrpDestinationTag = XrpDestinationTagState(
InputViewValue("$tag", false), tag)
)
}
} else {
emptyResult
}
}
Blockchain.Stellar -> TransactionExtrasState(xlmMemo = XlmMemoState())
else -> emptyResult
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleRelease(action: SendScreenAction, sendState: SendState): SendState {
val result = TransactionExtrasState()
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleMemo(
action: XlmMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
fun clearMemo(memo: XlmMemoState): XlmMemoState = memo.copy(text = null, id = null)
val result = when (action) {
// is XlmMemo.ChangeSelectedMemo -> {
// val inputViewValue = InputViewValue("", false)
// val memo = infoState.xlmMemo?.copy(
// viewFieldValue = inputViewValue,
// selectedMemoType = action.memoType,
// ) ?: XlmMemoState(inputViewValue, action.memoType)
//
// infoState.copy(xlmMemo = clearMemo(memo))
// }
is XlmMemo.HandleUserInput -> {
val inputViewValue = InputViewValue(action.data, true)
var memo = infoState.xlmMemo?.copy(viewFieldValue = inputViewValue) ?: XlmMemoState(inputViewValue)
memo = clearMemo(memo)
memo = when (infoState.xlmMemo?.selectedMemoType) {
XlmMemoType.TEXT -> memo.copy(text = StellarMemo.Text(action.data))
XlmMemoType.ID -> {
val id = action.data.toIntOrNull()?.toBigInteger()
if (id != null) memo.copy(id = StellarMemo.Id(id)) else memo
}
null -> memo
}
infoState.copy(xlmMemo = memo)
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleXrpTag(
action: XrpDestinationTag,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is XrpDestinationTag.HandleUserInput -> {
val tag = action.data.toLongOrNull()
if (tag != null) {
val tagState = XrpDestinationTagState(InputViewValue(action.data, true), tag)
infoState.copy(xrpDestinationTag = tagState)
} else {
infoState
}
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
}

View file

@ -1,12 +1,13 @@
package com.tangem.tap.features.send.redux.states
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
data class AddressPayIdState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null,
val recipientWalletAddress: String? = null,
val destinationWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false,
@ -17,7 +18,37 @@ data class AddressPayIdState(
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value
fun isReady(): Boolean = error == null && recipientWalletAddress?.isNotEmpty() ?: false
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
fun isPayIdState(): Boolean = recipientWalletAddress != null && recipientWalletAddress != normalFieldValue
}
fun isPayIdState(): Boolean = destinationWalletAddress != null && destinationWalletAddress != normalFieldValue
}
data class TransactionExtrasState(
val xlmMemo: XlmMemoState? = null,
val xrpDestinationTag: XrpDestinationTagState? = null
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
}
enum class XlmMemoType {
TEXT, ID
}
data class XlmMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val selectedMemoType: XlmMemoType = XlmMemoType.ID,
val text: StellarMemo.Text? = null,
val id: StellarMemo.Id? = null,
) {
val memo: StellarMemo?
get() = when (selectedMemoType) {
XlmMemoType.TEXT -> text
XlmMemoType.ID -> id
}
}
// tag must contains only digits
data class XrpDestinationTagState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val tag: Long? = null
)

View file

@ -22,7 +22,7 @@ interface IdStateHolder {
}
enum class StateId {
SEND_SCREEN, ADDRESS_PAY_ID, AMOUNT, FEE, RECEIPT
SEND_SCREEN, ADDRESS_PAY_ID, TRANSACTION_EXTRAS, AMOUNT, FEE, RECEIPT
}
interface SendScreenState : StateType, IdStateHolder
@ -33,6 +33,7 @@ data class SendState(
val tokenConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
val receiptState: ReceiptState = ReceiptState(),

View file

@ -67,6 +67,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
initSendButtonStates()
setupAddressOrPayIdLayout()
setupTransactionExtrasLayout()
setupAmountLayout()
setupFeeLayout()
@ -106,6 +107,32 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun setupTransactionExtrasLayout() {
etMemo.inputtedTextAsFlow()
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xlmMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
.launchIn(mainScope)
// groupMemo.setOnCheckedChangeListener { group, checkedId ->
// if (checkedId == -1) return@setOnCheckedChangeListener
//
// store.dispatch(TransactionExtrasAction.XlmMemo.ChangeSelectedMemo(MemoUiHelper.toType(checkedId)))
// }
etDestinationTag.inputtedTextAsFlow()
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xrpDestinationTag?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
.launchIn(mainScope)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != ScanQrCodeActivity.SCAN_QR_REQUEST_CODE) return
@ -191,7 +218,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
chipGroup.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
store.dispatch(ChangeSelectedFee(FeeUiHelper.idToFee(checkedId)))
store.dispatch(ChangeSelectedFee(FeeUiHelper.toType(checkedId)))
store.dispatch(CheckAmountToSend)
}
swIncludeFee.setOnCheckedChangeListener { btn, isChecked ->
@ -238,7 +265,7 @@ fun EditText.inputtedTextAsFlow(): Flow<String> = callbackFlow {
class FeeUiHelper {
companion object {
fun feeToId(fee: FeeType): Int {
fun toId(fee: FeeType): Int {
return when (fee) {
FeeType.SINGLE -> View.NO_ID
FeeType.LOW -> R.id.chipLow
@ -247,7 +274,7 @@ class FeeUiHelper {
}
}
fun idToFee(id: Int): FeeType {
fun toType(id: Int): FeeType {
return when (id) {
R.id.chipLow -> FeeType.LOW
R.id.chipNormal -> FeeType.NORMAL
@ -258,6 +285,26 @@ class FeeUiHelper {
}
}
//class MemoUiHelper {
// companion object {
// fun toId(memo: MemoType): Int {
// return when (memo) {
// MemoType.TEXT -> R.id.chipMemoText
// MemoType.ID -> R.id.chipMemoId
// }
// }
//
// fun toType(id: Int): MemoType {
// return when (id) {
// R.id.chipMemoText -> MemoType.TEXT
// R.id.chipMemoId -> MemoType.ID
// else -> MemoType.TEXT
// }
// }
// }
//}
private fun ToggleWidget.setupSendButtonStateModifiers(context: Context) {
mainViewModifiers.clear()
mainViewModifiers.add(ReplaceTextStateModifier(context.getString(R.string.send_title), ""))

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.ui.stateSubscribers
import android.app.Dialog
import android.content.Context
import android.text.InputType
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
@ -43,10 +44,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) {
val lastChangedStates = state.lastChangedStates.toList()
state.lastChangedStates.clear()
fg.main_send_container.beginDelayedTransition()
lastChangedStates.forEach {
when (it) {
StateId.SEND_SCREEN -> handleSendScreen(fg, state)
StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
StateId.AMOUNT -> handleAmountState(fg, state.amountState)
StateId.FEE -> handleFeeState(fg, state.feeState)
StateId.RECEIPT -> handleReceiptState(fg, state.receiptState)
@ -54,6 +57,25 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
}
private fun handleTransactionExtrasState(fg: BaseStoreFragment, infoState: TransactionExtrasState) {
fun showView(view: View, info: Any?) {
view.show(info != null)
}
showView(fg.xlmMemoContainer, infoState.xlmMemo)
showView(fg.xrpDestinationTagContainer, infoState.xrpDestinationTag)
infoState.xlmMemo?.let {
fg.etMemo.inputType = when (it.selectedMemoType) {
XlmMemoType.TEXT -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
XlmMemoType.ID -> InputType.TYPE_CLASS_NUMBER
}
if (!it.viewFieldValue.isFromUserInput) fg.etMemo.setText(it.viewFieldValue.value)
}
infoState.xrpDestinationTag?.let {
if (!it.viewFieldValue.isFromUserInput) fg.etDestinationTag.setText(it.viewFieldValue.value)
}
}
private fun handleSendScreen(fg: BaseStoreFragment, state: SendState) {
val sendFragment = (fg as? SendFragment) ?: return
@ -106,14 +128,13 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid
}else {
} else {
R.string.send_destination_hint_address
}
til.hint = til.getString(hintResId)
til.parent?.parent?.beginDelayedTransition()
til.error = parsedError
til.isErrorEnabled = parsedError != null
til.helperText = state.recipientWalletAddress
til.helperText = state.destinationWalletAddress
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
@ -130,10 +151,8 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
else -> context.getString(state.error.localizedMessage)
}
fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(true, message)
} else {
if (fg.tilAmountToSend.isErrorEnabled) fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(false)
}
@ -168,26 +187,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
private fun handleFeeState(fg: BaseStoreFragment, state: FeeState) {
var delayedTransitionScheduled = false
fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.let {
it.show(state.mainLayoutIsVisible) {
(it.parent as? ViewGroup)?.beginDelayedTransition()
delayedTransitionScheduled = true
}
}
fg.chipGroup.fitChipsByGroupWidth()
fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible)
fg.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f
fg.llFeeControlsContainer.show(state.controlsLayoutIsVisible) {
if (!delayedTransitionScheduled) {
fg.llFeeControlsContainer.parent?.parent?.beginDelayedTransition()
}
}
fg.chipGroup.show(state.feeChipGroupIsVisible) {
if (!delayedTransitionScheduled) {
fg.llFeeControlsContainer.parent?.parent?.beginDelayedTransition()
}
}
fg.llFeeControlsContainer.show(state.controlsLayoutIsVisible)
fg.chipGroup.show(state.feeChipGroupIsVisible)
fg.swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled
if (fg.swIncludeFee.isChecked != state.feeIsIncluded) {
@ -200,7 +205,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
}
val chipId = FeeUiHelper.feeToId(state.selectedFeeType)
val chipId = FeeUiHelper.toId(state.selectedFeeType)
if (fg.chipGroup.checkedChipId != chipId && chipId != View.NO_ID) fg.chipGroup.check(chipId)
}

View file

@ -35,7 +35,8 @@ data class WalletState(
val showSegwitAddress: Boolean
get() {
val listOfAddresses = walletAddresses?.list ?: return false
return wallet?.blockchain == Blockchain.Bitcoin && listOfAddresses.size > 1
return (wallet?.blockchain == Blockchain.Bitcoin || wallet?.blockchain == Blockchain.BitcoinTestnet)
&& listOfAddresses.size > 1
}
}

View file

@ -13,9 +13,7 @@ import com.google.android.material.snackbar.Snackbar
import com.squareup.picasso.Picasso
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinCardNumber
@ -236,6 +234,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
tv_address.setPadding(tv_address.paddingStart, tvAddressPaddingTop / 2,
tv_address.paddingEnd, tv_address.paddingBottom)
chip_group_segwit.show()
chip_group_segwit.fitChipsByGroupWidth()
val checkedId = SegwitUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chip_group_segwit.check(checkedId)

View file

@ -23,9 +23,9 @@ data class VerifyPayIdResponse(
val addresses: List<PayIdAddress> = mutableListOf(),
val payId: String? = null,
) {
fun getAddress(): String? {
fun getAddressDetails(): PayIdAddressDetails? {
return if (addresses.isEmpty()) null
else addresses[0].addressDetails.address
else addresses[0].addressDetails
}
}

View file

@ -33,6 +33,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/main_send_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

View file

@ -27,10 +27,9 @@
android:id="@+id/chip_group_segwit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="10dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:visibility="visible"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -1,76 +1,197 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/csRecipientAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAddressOrPayId"
android:layout_width="0dp"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/addressContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_destination_hint_address_payid"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAddressOrPayId"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAddressOrPayId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/send_destination_hint_address_payid"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAddressOrPayId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:inputType="textNoSuggestions"
android:paddingStart="0dp"
android:paddingEnd="98dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="139mrsJgyWnJ **** y9BV" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:id="@+id/flPaste"
android:layout_width="@dimen/btn_rounded_size"
android:layout_height="@dimen/btn_rounded_size"
android:layout_marginEnd="8dp"
android:background="@drawable/shape_ellipse"
app:layout_constraintBottom_toBottomOf="@+id/flQrCode"
app:layout_constraintEnd_toStartOf="@+id/flQrCode"
app:layout_constraintTop_toTopOf="@+id/flQrCode">
<ImageView
android:id="@+id/imvPaste"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:padding="6dp"
app:srcCompat="@drawable/paste_selector" />
</FrameLayout>
<FrameLayout
android:id="@+id/flQrCode"
android:layout_width="@dimen/btn_rounded_size"
android:layout_height="@dimen/btn_rounded_size"
android:layout_marginTop="10dp"
android:background="@drawable/shape_ellipse"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tilAddressOrPayId">
<ImageView
android:id="@+id/imvQrCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:padding="5dp"
app:srcCompat="@drawable/ic_qr_code_scan" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/extrasFieldsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addressContainer">
<LinearLayout
android:id="@+id/xlmMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:inputType="textNoSuggestions"
android:paddingStart="0dp"
android:paddingEnd="98dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="139mrsJgyWnJ **** y9BV" />
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
</com.google.android.material.textfield.TextInputLayout>
<!-- <com.google.android.material.chip.ChipGroup-->
<!-- android:id="@+id/groupMemo"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="8dp"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:selectionRequired="true"-->
<!-- app:singleLine="true"-->
<!-- app:singleSelection="true">-->
<FrameLayout
android:id="@+id/flPaste"
android:layout_width="@dimen/btn_rounded_size"
android:layout_height="@dimen/btn_rounded_size"
android:layout_marginEnd="8dp"
android:background="@drawable/shape_ellipse"
app:layout_constraintBottom_toBottomOf="@+id/flQrCode"
app:layout_constraintEnd_toStartOf="@+id/flQrCode"
app:layout_constraintTop_toTopOf="@+id/flQrCode">
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoText"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Text" />-->
<ImageView
android:id="@+id/imvPaste"
android:layout_width="wrap_content"
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoId"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="ID" />-->
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoHash"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Hash" />-->
<!-- </com.google.android.material.chip.ChipGroup>-->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Some memo" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/xrpDestinationTagContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:padding="6dp"
app:srcCompat="@drawable/paste_selector" />
android:visibility="gone"
tools:visibility="visible">
</FrameLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilDestinationTag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_destination_tag"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null">
<FrameLayout
android:id="@+id/flQrCode"
android:layout_width="@dimen/btn_rounded_size"
android:layout_height="@dimen/btn_rounded_size"
android:layout_marginTop="10dp"
android:background="@drawable/shape_ellipse"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tilAddressOrPayId">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDestinationTag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:inputType="number"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Some destination tag" />
<ImageView
android:id="@+id/imvQrCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:padding="5dp"
app:srcCompat="@drawable/ic_qr_code_scan" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>

View file

@ -33,4 +33,7 @@ this wallet.
<string name="common_start" translatable="false">Start</string>
<string name="common_back" translatable="false">Back</string>
<string name="send_extras_hint_memo" translatable="false">Memo</string>
<string name="send_extras_hint_destination_tag" translatable="false">Destination tag</string>
</resources>