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:blockchain:1.123.0'
implementation 'com.tangem:core:1.90.0' implementation 'com.tangem:core:1.90.0'
implementation 'com.tangem:sdk: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 // WebView
implementation "androidx.browser:browser:1.2.0" implementation "androidx.browser:browser:1.2.0"

View file

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

View file

@ -1,18 +1,6 @@
[ {
{ "isWalletPayIdEnabled": true,
"name": "isWalletPayIdEnabled", "isTopUpEnabled": true,
"value": true "isSendingToPayIdEnabled": true,
}, "isCreatingTwinCardsAllowed": true
{ }
"name": "isSendingToPayIdEnabled",
"value": true
},
{
"name": "isTopUpEnabled",
"value": true
},
{
"name": "isCreatingTwinCardsAllowed",
"value": 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.google.firebase.ktx.Firebase
import com.tangem.commands.common.card.Card import com.tangem.commands.common.card.Card
object FirebaseAnalyticsHandler: AnalyticsHandler { object FirebaseAnalyticsHandler : AnalyticsHandler {
override fun triggerEvent(event: AnalyticsEvent, card: Card?) { override fun triggerEvent(event: AnalyticsEvent, card: Card?) {
Firebase.analytics Firebase.analytics
.logEvent(event.event, setCardData(card)) .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 { private fun setCardData(card: Card?): Bundle {
if (card == null) return bundleOf() if (card == null) return bundleOf()
return bundleOf( return bundleOf(

View file

@ -5,6 +5,7 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.text.Spannable import android.text.Spannable
@ -39,6 +40,14 @@ fun View.getString(@StringRes id: Int): String {
return context.getString(id) 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) { fun View.show(show: Boolean, invokeBeforeStateChanged: (() -> Unit)? = null) {
return if (show) this.show(invokeBeforeStateChanged) return if (show) this.show(invokeBeforeStateChanged)
else this.hide(invokeBeforeStateChanged) else this.hide(invokeBeforeStateChanged)

View file

@ -3,9 +3,13 @@ package com.tangem.tap.common.extensions
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewParent import android.view.ViewParent
import androidx.core.view.forEach
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.Transition import androidx.transition.Transition
import androidx.transition.TransitionManager 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 import timber.log.Timber
/** /**
@ -25,4 +29,19 @@ fun ViewParent?.beginDelayedTransition(transition: Transition = AutoTransition()
fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) { fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) {
TransitionManager.beginDelayedTransition(this, transition) 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 package com.tangem.tap.domain.config
import android.content.Context import android.content.Context
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import timber.log.Timber import timber.log.Timber
@ -30,16 +29,13 @@ class LocalLoader(
override fun loadConfig(onComplete: (ConfigModel) -> Unit) { override fun loadConfig(onComplete: (ConfigModel) -> Unit) {
val config = try { val config = try {
val featureType = Types.newParameterizedType(List::class.java, FeatureModel::class.java) val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val featureAdapter: JsonAdapter<List<FeatureModel>> = moshi.adapter(featureType) val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val valuesType = Types.newParameterizedType(List::class.java, ConfigValueModel::class.java)
val valuesAdapter: JsonAdapter<List<ConfigValueModel>> = moshi.adapter(valuesType)
val jsonFeatures = readAssetAsString(ConfigLoader.featuresName) val jsonFeatures = readAssetAsString(ConfigLoader.featuresName)
val jsonConfigValues = readAssetAsString(ConfigLoader.configValuesName) val jsonConfigValues = readAssetAsString(ConfigLoader.configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures) ?: listOf(), ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
valuesAdapter.fromJson(jsonConfigValues) ?: listOf())
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex) Timber.e(ex)
ConfigModel.empty() ConfigModel.empty()
@ -67,14 +63,13 @@ class RemoteLoader(
onComplete(emptyConfig) onComplete(emptyConfig)
return@addOnCompleteListener return@addOnCompleteListener
} }
val featureType = Types.newParameterizedType(List::class.java, FeatureModel::class.java) val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val featureAdapter: JsonAdapter<List<FeatureModel>> = moshi.adapter(featureType) onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig), null))
onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig) ?: listOf(), listOf()))
} else { } else {
onComplete(emptyConfig) onComplete(emptyConfig)
} }
}.addOnFailureListener { }.addOnFailureListener {
FirebaseCrashlytics.getInstance().recordException(it) FirebaseAnalyticsHandler.logException("remote_config_error", it)
onComplete(emptyConfig) onComplete(emptyConfig)
} }
} }

View file

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

View file

@ -4,23 +4,21 @@ package com.tangem.tap.domain.config
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
interface BaseConfigModel<V> {
val name: String
val value: V?
}
class FeatureModel( class FeatureModel(
override val name: String, val isWalletPayIdEnabled: Boolean,
override val value: Boolean val isTopUpEnabled: Boolean,
) : BaseConfigModel<Boolean> val isSendingToPayIdEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
)
class ConfigValueModel( class ConfigValueModel(
override val name: String, val coinMarketCapKey: String,
override val value: String val moonPayApiKey: String,
) : BaseConfigModel<String> val moonPayApiSecretKey: String,
)
class ConfigModel(val features: List<FeatureModel>?, val configValues: List<ConfigValueModel>?) { class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {
companion object { 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.Message
import com.tangem.blockchain.common.Amount 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.ErrorAction
import com.tangem.tap.common.redux.ToastNotificationAction import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapError
@ -22,7 +23,7 @@ object ReleaseSendState : Action
data class PrepareSendScreen( data class PrepareSendScreen(
val coinAmount: Amount?, val coinAmount: Amount?,
val tokenAmount: Amount? = null val tokenAmount: Amount? = null,
) : SendScreenAction ) : SendScreenAction
// Address or PayId // Address or PayId
@ -33,7 +34,27 @@ sealed class AddressPayIdActionUi : SendScreenActionUi {
object CheckAddressPayId : AddressPayIdActionUi() object CheckAddressPayId : AddressPayIdActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi() data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
data class TruncateOrRestore(val truncate: Boolean) : 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 { sealed class AddressPayIdVerifyAction : SendScreenAction {
@ -122,6 +143,7 @@ sealed class SendAction : SendScreenAction {
val sendAllCallback: () -> Unit, val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal, val reduceAmount: BigDecimal,
) : Dialog() ) : Dialog()
object Hide : 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.SetPayIdError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress 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.FeeAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -82,22 +83,26 @@ internal class AddressPayIdMiddleware {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
when (result) { when (result) {
is Result.Success -> { is Result.Success -> {
val address = result.data.getAddress() val addressDetails = result.data.getAddressDetails()
if (address == null) { if (addressDetails == null) {
dispatch(SetPayIdError(Error.PAY_ID_NOT_REGISTERED)) dispatch(SetPayIdError(Error.PAY_ID_NOT_REGISTERED))
return@withContext return@withContext
} }
val address = addressDetails.address
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, address) val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, address)
if (failReason == null) { if (failReason == null) {
dispatch(SetPayIdWalletAddress(payId, address, isUserInput)) dispatch(SetPayIdWalletAddress(payId, address, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, addressDetails.tag))
dispatch(FeeAction.RequestFee) dispatch(FeeAction.RequestFee)
} else { } else {
dispatch(SetAddressError(failReason)) dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
} }
} }
is Result.Failure -> { is Result.Failure -> {
dispatch(SetPayIdError(Error.PAY_ID_REQUEST_FAILED)) dispatch(SetPayIdError(Error.PAY_ID_REQUEST_FAILED))
dispatch(TransactionExtrasAction.Release)
} }
} }
} }
@ -110,8 +115,10 @@ internal class AddressPayIdMiddleware {
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress) val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress)
if (failReason == null) { if (failReason == null) {
dispatch(SetWalletAddress(supposedAddress, isUserInput)) dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
} else { } else {
dispatch(SetAddressError(failReason)) dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
} }
} }

View file

@ -33,15 +33,13 @@ class RequestFeeMiddleware {
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState())) dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return return
} }
val typedAmount = sendState.amountState.amountToExtract ?: 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 val txSender = walletManager as TransactionSender
scope.launch { scope.launch {
val feeResult = txSender.getFee(recipientAmount, recipientAddress) val feeResult = txSender.getFee(destinationAmount, destinationAddress)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
when (feeResult) { when (feeResult) {
is Result.Success -> { is Result.Success -> {

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.send.redux.middlewares package com.tangem.tap.features.send.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics 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.common.*
import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.Signer 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.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee 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.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.tangemSdk import com.tangem.tap.tangemSdk
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -52,7 +55,7 @@ private fun verifyAndSendTransaction(
val sendState = appState?.sendState ?: return val sendState = appState?.sendState ?: return
val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return
val card = appState.globalState.scanNoteResponse.card 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 typedAmount = sendState.amountState.amountToExtract ?: return
val feeAmount = sendState.feeState.currentFee ?: return val feeAmount = sendState.feeState.currentFee ?: return
@ -67,14 +70,16 @@ private fun verifyAndSendTransaction(
dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false)) dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false))
dispatch(AmountActionUi.CheckAmountToSend) dispatch(AmountActionUi.CheckAmountToSend)
}, sendAllCallback = { }, sendAllCallback = {
sendTransaction(action, walletManager, amountToSend, feeAmount, recipientAddress, card, dispatch) sendTransaction(action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, dispatch)
}, reduceAmount)) }, reduceAmount))
} }
transactionErrors.isNotEmpty() -> { transactionErrors.isNotEmpty() -> {
dispatch(SendAction.SendError(createValidateTransactionError(transactionErrors, walletManager))) dispatch(SendAction.SendError(createValidateTransactionError(transactionErrors, walletManager)))
} }
else -> { 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, walletManager: WalletManager,
amountToSend: Amount, amountToSend: Amount,
feeAmount: Amount, feeAmount: Amount,
recipientAddress: String, destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: Card, card: Card,
dispatch: (Action) -> Unit dispatch: (Action) -> Unit
) { ) {
dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS)) 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 { scope.launch {
walletManager.update() walletManager.update()
val isLinkedTerminal = tangemSdk.config.linkedTerminal val isLinkedTerminal = tangemSdk.config.linkedTerminal

View file

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

View file

@ -25,6 +25,7 @@ class SendScreenReducer {
val reducer: SendInternalReducer = when (action) { val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer() is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer() is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer() is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer() is FeeActionUi, is FeeAction -> FeeReducer()
is ReceiptAction -> ReceiptReducer() 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 package com.tangem.tap.features.send.redux.states
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
data class AddressPayIdState( data class AddressPayIdState(
val viewFieldValue: InputViewValue = InputViewValue(""), val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null, val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null, val truncatedFieldValue: String? = null,
val recipientWalletAddress: String? = null, val destinationWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null, val error: AddressPayIdVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null, val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false, val sendingToPayIdEnabled: Boolean = false,
@ -17,7 +18,37 @@ data class AddressPayIdState(
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value 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 { 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 interface SendScreenState : StateType, IdStateHolder
@ -33,6 +33,7 @@ data class SendState(
val tokenConverter: CurrencyConverter? = null, val tokenConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(), val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(), val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(), val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(), val feeState: FeeState = FeeState(),
val receiptState: ReceiptState = ReceiptState(), val receiptState: ReceiptState = ReceiptState(),

View file

@ -67,6 +67,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
initSendButtonStates() initSendButtonStates()
setupAddressOrPayIdLayout() setupAddressOrPayIdLayout()
setupTransactionExtrasLayout()
setupAmountLayout() setupAmountLayout()
setupFeeLayout() 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != ScanQrCodeActivity.SCAN_QR_REQUEST_CODE) return if (requestCode != ScanQrCodeActivity.SCAN_QR_REQUEST_CODE) return
@ -191,7 +218,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
chipGroup.setOnCheckedChangeListener { group, checkedId -> chipGroup.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener if (checkedId == -1) return@setOnCheckedChangeListener
store.dispatch(ChangeSelectedFee(FeeUiHelper.idToFee(checkedId))) store.dispatch(ChangeSelectedFee(FeeUiHelper.toType(checkedId)))
store.dispatch(CheckAmountToSend) store.dispatch(CheckAmountToSend)
} }
swIncludeFee.setOnCheckedChangeListener { btn, isChecked -> swIncludeFee.setOnCheckedChangeListener { btn, isChecked ->
@ -238,7 +265,7 @@ fun EditText.inputtedTextAsFlow(): Flow<String> = callbackFlow {
class FeeUiHelper { class FeeUiHelper {
companion object { companion object {
fun feeToId(fee: FeeType): Int { fun toId(fee: FeeType): Int {
return when (fee) { return when (fee) {
FeeType.SINGLE -> View.NO_ID FeeType.SINGLE -> View.NO_ID
FeeType.LOW -> R.id.chipLow FeeType.LOW -> R.id.chipLow
@ -247,7 +274,7 @@ class FeeUiHelper {
} }
} }
fun idToFee(id: Int): FeeType { fun toType(id: Int): FeeType {
return when (id) { return when (id) {
R.id.chipLow -> FeeType.LOW R.id.chipLow -> FeeType.LOW
R.id.chipNormal -> FeeType.NORMAL 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) { private fun ToggleWidget.setupSendButtonStateModifiers(context: Context) {
mainViewModifiers.clear() mainViewModifiers.clear()
mainViewModifiers.add(ReplaceTextStateModifier(context.getString(R.string.send_title), "")) 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.app.Dialog
import android.content.Context import android.content.Context
import android.text.InputType
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -43,10 +44,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) { override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) {
val lastChangedStates = state.lastChangedStates.toList() val lastChangedStates = state.lastChangedStates.toList()
state.lastChangedStates.clear() state.lastChangedStates.clear()
fg.main_send_container.beginDelayedTransition()
lastChangedStates.forEach { lastChangedStates.forEach {
when (it) { when (it) {
StateId.SEND_SCREEN -> handleSendScreen(fg, state) StateId.SEND_SCREEN -> handleSendScreen(fg, state)
StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState) StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
StateId.AMOUNT -> handleAmountState(fg, state.amountState) StateId.AMOUNT -> handleAmountState(fg, state.amountState)
StateId.FEE -> handleFeeState(fg, state.feeState) StateId.FEE -> handleFeeState(fg, state.feeState)
StateId.RECEIPT -> handleReceiptState(fg, state.receiptState) 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) { private fun handleSendScreen(fg: BaseStoreFragment, state: SendState) {
val sendFragment = (fg as? SendFragment) ?: return val sendFragment = (fg as? SendFragment) ?: return
@ -106,14 +128,13 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val hintResId = if (state.sendingToPayIdEnabled) { val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid R.string.send_destination_hint_address_payid
}else { } else {
R.string.send_destination_hint_address R.string.send_destination_hint_address
} }
til.hint = til.getString(hintResId) til.hint = til.getString(hintResId)
til.parent?.parent?.beginDelayedTransition()
til.error = parsedError til.error = parsedError
til.isErrorEnabled = parsedError != null til.isErrorEnabled = parsedError != null
til.helperText = state.recipientWalletAddress til.helperText = state.destinationWalletAddress
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value) if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
@ -130,10 +151,8 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
} }
else -> context.getString(state.error.localizedMessage) else -> context.getString(state.error.localizedMessage)
} }
fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(true, message) fg.tilAmountToSend.enableError(true, message)
} else { } else {
if (fg.tilAmountToSend.isErrorEnabled) fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(false) fg.tilAmountToSend.enableError(false)
} }
@ -168,26 +187,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
} }
private fun handleFeeState(fg: BaseStoreFragment, state: FeeState) { private fun handleFeeState(fg: BaseStoreFragment, state: FeeState) {
var delayedTransitionScheduled = false fg.chipGroup.fitChipsByGroupWidth()
fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.let { fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible)
it.show(state.mainLayoutIsVisible) {
(it.parent as? ViewGroup)?.beginDelayedTransition()
delayedTransitionScheduled = true
}
}
fg.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f fg.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f
fg.llFeeControlsContainer.show(state.controlsLayoutIsVisible) { fg.llFeeControlsContainer.show(state.controlsLayoutIsVisible)
if (!delayedTransitionScheduled) { fg.chipGroup.show(state.feeChipGroupIsVisible)
fg.llFeeControlsContainer.parent?.parent?.beginDelayedTransition()
}
}
fg.chipGroup.show(state.feeChipGroupIsVisible) {
if (!delayedTransitionScheduled) {
fg.llFeeControlsContainer.parent?.parent?.beginDelayedTransition()
}
}
fg.swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled fg.swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled
if (fg.swIncludeFee.isChecked != state.feeIsIncluded) { 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) 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 val showSegwitAddress: Boolean
get() { get() {
val listOfAddresses = walletAddresses?.list ?: return false 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.squareup.picasso.Picasso
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType
import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.extensions.beginDelayedTransition import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinCardNumber 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.setPadding(tv_address.paddingStart, tvAddressPaddingTop / 2,
tv_address.paddingEnd, tv_address.paddingBottom) tv_address.paddingEnd, tv_address.paddingBottom)
chip_group_segwit.show() chip_group_segwit.show()
chip_group_segwit.fitChipsByGroupWidth()
val checkedId = SegwitUiHelper.typeToId(state.walletAddresses.selectedAddress.type) val checkedId = SegwitUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chip_group_segwit.check(checkedId) 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 addresses: List<PayIdAddress> = mutableListOf(),
val payId: String? = null, val payId: String? = null,
) { ) {
fun getAddress(): String? { fun getAddressDetails(): PayIdAddressDetails? {
return if (addresses.isEmpty()) null 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"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <LinearLayout
android:id="@+id/main_send_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">

View file

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

View file

@ -1,76 +1,197 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/csRecipientAddress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp"> android:paddingEnd="16dp">
<com.google.android.material.textfield.TextInputLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/tilAddressOrPayId" android:id="@+id/addressContainer"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" 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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputLayout
android:id="@+id/etAddressOrPayId" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="middle" android:orientation="vertical"
android:inputType="textNoSuggestions" android:visibility="gone"
android:paddingStart="0dp" tools:visibility="visible">
android:paddingEnd="98dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="139mrsJgyWnJ **** y9BV" />
</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 <!-- <com.google.android.material.chip.Chip-->
android:id="@+id/flPaste" <!-- android:id="@+id/chipMemoText"-->
android:layout_width="@dimen/btn_rounded_size" <!-- style="@style/TapChip"-->
android:layout_height="@dimen/btn_rounded_size" <!-- android:layout_width="wrap_content"-->
android:layout_marginEnd="8dp" <!-- android:layout_height="wrap_content"-->
android:background="@drawable/shape_ellipse" <!-- android:text="Text" />-->
app:layout_constraintBottom_toBottomOf="@+id/flQrCode"
app:layout_constraintEnd_toStartOf="@+id/flQrCode"
app:layout_constraintTop_toTopOf="@+id/flQrCode">
<ImageView <!-- <com.google.android.material.chip.Chip-->
android:id="@+id/imvPaste" <!-- android:id="@+id/chipMemoId"-->
android:layout_width="wrap_content" <!-- 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_height="wrap_content"
android:layout_gravity="center" android:visibility="gone"
android:background="?selectableItemBackgroundBorderless" tools:visibility="visible">
android:padding="6dp"
app:srcCompat="@drawable/paste_selector" />
</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 <com.google.android.material.textfield.TextInputEditText
android:id="@+id/flQrCode" android:id="@+id/etDestinationTag"
android:layout_width="@dimen/btn_rounded_size" android:layout_width="match_parent"
android:layout_height="@dimen/btn_rounded_size" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:ellipsize="end"
android:background="@drawable/shape_ellipse" android:inputType="number"
app:layout_constraintEnd_toEndOf="parent" android:paddingStart="0dp"
app:layout_constraintTop_toTopOf="@+id/tilAddressOrPayId"> android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Some destination tag" />
<ImageView </com.google.android.material.textfield.TextInputLayout>
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> </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_start" translatable="false">Start</string>
<string name="common_back" translatable="false">Back</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> </resources>