Updated on 2026-08-14
This commit is contained in:
parent
cf69bb5975
commit
c97ae7e446
10 changed files with 208 additions and 71 deletions
|
|
@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Amount
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
|
||||
import com.tangem.tap.common.redux.ErrorAction
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.ToastNotificationAction
|
||||
|
|
@ -36,9 +37,9 @@ data class PrepareSendScreen(
|
|||
// Address or PayId
|
||||
sealed class AddressPayIdActionUi : SendScreenActionUi {
|
||||
data class HandleUserInput(val data: String) : AddressPayIdActionUi()
|
||||
data class PasteAddressPayId(val data: String) : AddressPayIdActionUi()
|
||||
data class PasteAddressPayId(val data: String, val sourceType: AddressEntered.SourceType) : AddressPayIdActionUi()
|
||||
data class CheckClipboard(val data: String?) : AddressPayIdActionUi()
|
||||
object CheckAddressPayId : AddressPayIdActionUi()
|
||||
data class CheckAddressPayId(val sourceType: AddressEntered.SourceType?) : AddressPayIdActionUi()
|
||||
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
|
||||
data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi()
|
||||
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean) : AddressPayIdActionUi()
|
||||
|
|
|
|||
|
|
@ -3,20 +3,17 @@ package com.tangem.tap.features.send.redux.middlewares
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.PayIdManager
|
||||
import com.tangem.tap.domain.isPayIdSupported
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetAddressError
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetWalletAddress
|
||||
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.AmountAction
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
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
|
||||
|
|
@ -33,9 +30,9 @@ internal class AddressPayIdMiddleware {
|
|||
fun handle(action: AddressPayIdActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
when (action) {
|
||||
is AddressPayIdActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
|
||||
is AddressPayIdActionUi.PasteAddressPayId -> pasteAddressPayId(action.data, dispatch)
|
||||
is AddressPayIdActionUi.PasteAddressPayId -> pasteAddressPayId(action.data, action.sourceType, dispatch)
|
||||
is AddressPayIdActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
|
||||
is AddressPayIdActionUi.CheckAddressPayId -> verifyAddressPayId(appState, dispatch)
|
||||
is AddressPayIdActionUi.CheckAddressPayId -> verifyAddressPayId(action.sourceType, appState, dispatch)
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
|
@ -44,24 +41,33 @@ internal class AddressPayIdMiddleware {
|
|||
val sendState = appState?.sendState ?: return
|
||||
if (input == sendState.addressPayIdState.viewFieldValue.value) return
|
||||
|
||||
setAddressAndCheck(input, true, dispatch)
|
||||
setAddressAndCheck(data = input, sourceType = null, isUserInput = true, dispatch = dispatch)
|
||||
}
|
||||
|
||||
private fun pasteAddressPayId(data: String, dispatch: (Action) -> Unit) {
|
||||
setAddressAndCheck(data, false, dispatch)
|
||||
private fun pasteAddressPayId(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
|
||||
setAddressAndCheck(data = data, sourceType = sourceType, isUserInput = false, dispatch = dispatch)
|
||||
}
|
||||
|
||||
private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) {
|
||||
private fun setAddressAndCheck(
|
||||
data: String,
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
isUserInput: Boolean,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
val potentialPayId = data.lowercase()
|
||||
if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) {
|
||||
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
|
||||
} else {
|
||||
dispatch(SetWalletAddress(data, isUserInput))
|
||||
}
|
||||
dispatch(AddressPayIdActionUi.CheckAddressPayId)
|
||||
dispatch(AddressPayIdActionUi.CheckAddressPayId(sourceType))
|
||||
}
|
||||
|
||||
private fun verifyAddressPayId(appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
private fun verifyAddressPayId(
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
appState: AppState?,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val wallet = sendState.walletManager?.wallet ?: return
|
||||
val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return
|
||||
|
|
@ -70,7 +76,13 @@ internal class AddressPayIdMiddleware {
|
|||
if (isPayIdEnabled() && PayIdManager.isPayId(addressPayId)) {
|
||||
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
|
||||
} else {
|
||||
verifyAddress(addressPayId, wallet, isUserInput, dispatch)
|
||||
verifyAddress(
|
||||
address = addressPayId,
|
||||
wallet = wallet,
|
||||
isUserInput = isUserInput,
|
||||
dispatch = dispatch,
|
||||
sourceType = sourceType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,24 +124,45 @@ internal class AddressPayIdMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) {
|
||||
private fun verifyAddress(
|
||||
address: String,
|
||||
wallet: Wallet,
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
isUserInput: Boolean,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
val addressSchemeSplit = when (wallet.blockchain) {
|
||||
Blockchain.BitcoinCash,
|
||||
Blockchain.Kaspa,
|
||||
-> listOf(address)
|
||||
Blockchain.BitcoinCash, Blockchain.Kaspa -> listOf(address)
|
||||
else -> address.split(":")
|
||||
}
|
||||
|
||||
val noSchemeAddress = when (addressSchemeSplit.size) {
|
||||
1 -> address // no scheme
|
||||
2 -> { // scheme
|
||||
if (wallet.blockchain.validateShareScheme(addressSchemeSplit[0])) {
|
||||
addressSchemeSplit[1]
|
||||
} else {
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Fail,
|
||||
),
|
||||
)
|
||||
}
|
||||
dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN))
|
||||
return
|
||||
}
|
||||
}
|
||||
else -> { // invalid URI
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Fail,
|
||||
),
|
||||
)
|
||||
}
|
||||
dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN))
|
||||
return
|
||||
}
|
||||
|
|
@ -146,9 +179,25 @@ internal class AddressPayIdMiddleware {
|
|||
dispatch(SetWalletAddress(supposedAddress, isUserInput))
|
||||
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
|
||||
dispatch(FeeAction.RequestFee)
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Success,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
dispatch(SetAddressError(failReason))
|
||||
dispatch(TransactionExtrasAction.Release)
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Fail,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +236,13 @@ internal class AddressPayIdMiddleware {
|
|||
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
|
||||
verifyPayId(addressPayId, wallet, false, internalDispatcher)
|
||||
} else {
|
||||
verifyAddress(addressPayId, wallet, false, internalDispatcher)
|
||||
verifyAddress(
|
||||
address = addressPayId,
|
||||
wallet = wallet,
|
||||
sourceType = null,
|
||||
isUserInput = false,
|
||||
dispatch = internalDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ import com.tangem.domain.models.scan.CardDTO
|
|||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.analytics.events.Token.Send.SelectedCurrency.CurrencyType
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -120,6 +123,7 @@ private fun verifyAndSendTransaction(
|
|||
transactionExtras = sendState.transactionExtrasState,
|
||||
card = card,
|
||||
externalTransactionData = sendState.externalTransactionData,
|
||||
mainCurrencyType = sendState.amountState.mainCurrency.type,
|
||||
dispatch = dispatch,
|
||||
)
|
||||
},
|
||||
|
|
@ -138,6 +142,7 @@ private fun verifyAndSendTransaction(
|
|||
transactionExtras = sendState.transactionExtrasState,
|
||||
card = card,
|
||||
externalTransactionData = sendState.externalTransactionData,
|
||||
mainCurrencyType = sendState.amountState.mainCurrency.type,
|
||||
dispatch = dispatch,
|
||||
)
|
||||
}
|
||||
|
|
@ -155,6 +160,7 @@ private fun sendTransaction(
|
|||
transactionExtras: TransactionExtrasState,
|
||||
card: CardDTO,
|
||||
externalTransactionData: ExternalTransactionData?,
|
||||
mainCurrencyType: MainCurrencyType,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS))
|
||||
|
|
@ -241,7 +247,20 @@ private fun sendTransaction(
|
|||
dispatch(SendAction.SendSuccess)
|
||||
|
||||
if (externalTransactionData != null) {
|
||||
Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Sell))
|
||||
Analytics.send(
|
||||
Basic.TransactionSent(
|
||||
sentFrom = AnalyticsParam.TxSentFrom.Sell,
|
||||
memoType = if (txData.extras != null) MemoType.Full else MemoType.Empty,
|
||||
),
|
||||
)
|
||||
Analytics.send(
|
||||
Token.Send.SelectedCurrency(
|
||||
currency = when (mainCurrencyType) {
|
||||
MainCurrencyType.FIAT -> CurrencyType.AppCurrency
|
||||
MainCurrencyType.CRYPTO -> CurrencyType.AppCurrency
|
||||
},
|
||||
),
|
||||
)
|
||||
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
|
||||
} else {
|
||||
Analytics.send(
|
||||
|
|
@ -251,6 +270,7 @@ private fun sendTransaction(
|
|||
token = amountToSend.currencySymbol,
|
||||
feeType = feeType.convertToAnalyticsFeeType(),
|
||||
),
|
||||
memoType = if (txData.extras != null) MemoType.Full else MemoType.Empty,
|
||||
),
|
||||
)
|
||||
dispatch(NavigationAction.PopBackTo())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.features.send.ui
|
||||
|
||||
import android.R
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
class EditTextCustomPaste @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : TextInputEditText(context, attrs, defStyleAttr) {
|
||||
|
||||
private var onSystemPasteButtonClickListener: (() -> Unit)? = null
|
||||
|
||||
fun setOnSystemPasteButtonClickListener(callback: () -> Unit) {
|
||||
onSystemPasteButtonClickListener = callback
|
||||
}
|
||||
|
||||
override fun onTextContextMenuItem(id: Int): Boolean {
|
||||
val isConsumed = super.onTextContextMenuItem(id)
|
||||
|
||||
if (id == R.id.paste) onSystemPasteButtonClickListener?.invoke()
|
||||
|
||||
return isConsumed
|
||||
}
|
||||
}
|
||||
|
|
@ -32,25 +32,10 @@ import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
|
|||
import com.tangem.tap.common.toggleWidget.ViewStateWidget
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.CheckClipboard
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.PasteAddressPayId
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.SetTruncateHandler
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.TruncateOrRestore
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.SetMaxAmount
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.ToggleMainCurrency
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeIncludeFee
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeSelectedFee
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.ToggleControlsVisibility
|
||||
import com.tangem.tap.features.send.redux.ReceiptAction
|
||||
import com.tangem.tap.features.send.redux.ReleaseSendState
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.send.redux.SendActionUi
|
||||
import com.tangem.tap.features.send.redux.TransactionExtrasAction
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.*
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.*
|
||||
import com.tangem.tap.features.send.redux.states.FeeType
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
|
||||
|
|
@ -62,12 +47,7 @@ import com.tangem.wallet.R
|
|||
import com.tangem.wallet.databinding.FragmentSendBinding
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
|
||||
|
|
@ -123,20 +103,37 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
|
||||
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
|
||||
|
||||
etAddressOrPayId.setOnFocusChangeListener { _, hasFocus ->
|
||||
store.dispatch(TruncateOrRestore(!hasFocus))
|
||||
}
|
||||
etAddressOrPayId.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
|
||||
.onEach {
|
||||
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
|
||||
etAddressOrPayId.apply {
|
||||
setOnSystemPasteButtonClickListener {
|
||||
store.dispatch(
|
||||
PasteAddressPayId(
|
||||
data = requireContext().getFromClipboard()?.toString() ?: "",
|
||||
sourceType = Token.Send.AddressEntered.SourceType.PastePopup,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(mainScope)
|
||||
|
||||
setOnFocusChangeListener { _, hasFocus ->
|
||||
store.dispatch(TruncateOrRestore(!hasFocus))
|
||||
}
|
||||
|
||||
inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
|
||||
.onEach {
|
||||
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
|
||||
}
|
||||
.launchIn(mainScope)
|
||||
}
|
||||
|
||||
imvPaste.setOnClickListener {
|
||||
Analytics.send(Token.Send.ButtonPaste())
|
||||
store.dispatch(PasteAddressPayId(requireContext().getFromClipboard()?.toString() ?: ""))
|
||||
store.dispatch(
|
||||
PasteAddressPayId(
|
||||
data = requireContext().getFromClipboard()?.toString() ?: "",
|
||||
sourceType = Token.Send.AddressEntered.SourceType.PasteButton,
|
||||
),
|
||||
)
|
||||
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
|
||||
}
|
||||
imvQrCode.setOnClickListener {
|
||||
|
|
@ -207,7 +204,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
// inserting an incorrect amount by shareUri
|
||||
binding.lSendAddressPayid.imvQrCode.postDelayed(
|
||||
{
|
||||
store.dispatch(PasteAddressPayId(scannedCode))
|
||||
store.dispatch(
|
||||
PasteAddressPayId(
|
||||
data = scannedCode,
|
||||
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
|
||||
),
|
||||
)
|
||||
store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused))
|
||||
},
|
||||
200,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue