Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-29 11:42:56 +03:00
commit 9abedff8b5
80 changed files with 1567 additions and 3658 deletions

View file

@ -31,12 +31,14 @@ sealed class Basic(
currency: AnalyticsParam.CardCurrency,
batch: String,
signInType: SignInType,
walletsCount: String,
) : Basic(
event = "Signed in",
params = mapOf(
AnalyticsParam.CURRENCY to currency.value,
AnalyticsParam.BATCH to batch,
"Sign in type" to signInType.name,
"Wallets Count" to walletsCount,
),
) {
enum class SignInType {

View file

@ -10,7 +10,6 @@ import com.tangem.wallet.R
fun Blockchain.getGreyedOutIconRes(): Int {
return when (this) {
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> R.drawable.ic_arbitrum_no_color
// Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> R.drawable.ic_bitcoin_no_color
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_no_color
Blockchain.Litecoin -> R.drawable.ic_litecoin_no_color

View file

@ -6,7 +6,6 @@ import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
@ -20,7 +19,6 @@ data class GlobalState(
val onboardingState: OnboardingState = OnboardingState(),
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,

View file

@ -1,57 +0,0 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.tap.network.payid.PayIdVerifyService
import com.tangem.tap.network.payid.VerifyPayIdResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
class PayIdManager {
@Suppress("MagicNumber")
suspend fun verifyPayId(payId: String, blockchain: Blockchain): Result<VerifyPayIdResponse> =
withContext(Dispatchers.IO) {
val splitPayId = payId.split("\$")
val user = splitPayId[0]
val baseUrl = "https://${splitPayId[1]}/"
return@withContext PayIdVerifyService(baseUrl).verifyAddress(user, blockchain.getPayIdNetwork())
}
private fun Blockchain.getPayIdNetwork(): String {
return when (this) {
Blockchain.XRP -> "XRPL"
Blockchain.RSK -> "RSK"
else -> this.currency
}.lowercase(Locale.getDefault())
}
companion object {
private val payIdRegExp = (
"^[a-z0-9!#@%&*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#@%&*+/=?^_`{|}~-]+)*\\\$(?:(?:[a-z0-9]" +
"(?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z-]*[a-z0-9])?|(?:[0-9]{1,3}\\.){3}[0-9]{1,3})\$"
).toRegex()
val payIdSupported: EnumSet<Blockchain> = EnumSet.of(
Blockchain.XRP,
Blockchain.Ethereum,
Blockchain.Bitcoin,
Blockchain.Litecoin,
Blockchain.Stellar,
Blockchain.Cardano,
Blockchain.CardanoShelley,
Blockchain.Ducatus,
Blockchain.BitcoinCash,
Blockchain.Binance,
Blockchain.RSK,
Blockchain.Tezos,
)
fun isPayId(value: String?): Boolean = value?.contains(payIdRegExp) ?: false
}
}
fun Blockchain.isPayIdSupported(): Boolean {
return PayIdManager.payIdSupported.contains(this)
}

View file

@ -1,6 +1,9 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
@ -134,22 +137,13 @@ class TapWalletManager(
fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.cardTypesResolver.getBlockchain()
if (data.cardTypesResolver.isStart2Coin()) {
configManager?.turnOff(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.turnOff(ConfigManager.IS_TOP_UP_ENABLED)
} else if (blockchain == Blockchain.Bitcoin ||
data.walletData?.blockchain == Blockchain.Bitcoin.id
) {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
} else {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
}
}
}
fun Wallet.getFirstToken(): Token? {
return getTokens().toList().getOrNull(0)
}
fun Wallet.getFirstToken(): Token? = getTokens().toList().getOrNull(index = 0)

View file

@ -26,6 +26,11 @@ interface UserWalletsListManager {
* */
val hasUserWallets: Boolean
/**
* Count of saved user wallets
*/
val walletsCount: Int
/**
* Set [UserWallet] with provided [UserWalletId] as selected
*

View file

@ -53,6 +53,9 @@ internal class BiometricUserWalletsListManager(
override val hasUserWallets: Boolean
get() = keysRepository.hasSavedEncryptionKeys()
override val walletsCount: Int
get() = state.value.userWallets.size
override suspend fun unlock(): CompletionResult<UserWallet> {
return unlockWithBiometryInternal()
.mapFailure { error ->

View file

@ -7,13 +7,7 @@ import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.flow.*
@OptIn(ExperimentalCoroutinesApi::class)
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
@ -36,6 +30,12 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
override val hasUserWallets: Boolean
get() = state.value.userWallet != null
/**
* only 1 wallet stored in runtime implementation
*/
override val walletsCount: Int
get() = 1
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet
?.takeIf { it.walletId == userWalletId }

View file

@ -6,7 +6,6 @@ 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
import com.tangem.tap.domain.TapError
@ -34,15 +33,14 @@ data class PrepareSendScreen(
val tokenRate: BigDecimal? = null,
) : SendScreenAction
// Address or PayId
sealed class AddressPayIdActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AddressPayIdActionUi()
data class PasteAddressPayId(val data: String, val sourceType: AddressEntered.SourceType) : AddressPayIdActionUi()
data class CheckClipboard(val data: String?) : 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()
// Address
sealed class AddressActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AddressActionUi()
data class PasteAddress(val data: String, val sourceType: AddressEntered.SourceType) : AddressActionUi()
data class CheckClipboard(val data: String?) : AddressActionUi()
data class CheckAddress(val sourceType: AddressEntered.SourceType?) : AddressActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressActionUi()
}
sealed class TransactionExtrasAction : SendScreenActionUi {
@ -76,27 +74,15 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
}
}
sealed class AddressPayIdVerifyAction : SendScreenAction {
sealed class AddressVerifyAction : SendScreenAction {
enum class Error {
PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN,
PAY_ID_NOT_REGISTERED,
PAY_ID_REQUEST_FAILED,
ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN,
ADDRESS_SAME_AS_WALLET,
}
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressPayIdVerifyAction()
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressVerifyAction()
sealed class PayIdVerification : AddressPayIdVerifyAction() {
data class SetPayIdError(val error: Error?) : PayIdVerification()
data class SetPayIdWalletAddress(
val payId: String,
val payIdWalletAddress: String,
val isUserInput: Boolean,
) : PayIdVerification()
}
sealed class AddressVerification : AddressPayIdVerifyAction() {
sealed class AddressVerification : AddressVerifyAction() {
data class SetAddressError(val error: Error?) : AddressVerification()
data class SetWalletAddress(val address: String, val isUserInput: Boolean) : AddressVerification()
}
@ -155,8 +141,6 @@ sealed class SendAction : SendScreenAction {
override val messageResource: Int = R.string.send_transaction_success
}
data class SendError(override val error: TapError) : SendAction(), ErrorAction
sealed class Dialog : SendAction(), StateDialog {
data class TezosWarningDialog(
val reduceCallback: () -> Unit,

View file

@ -2,49 +2,39 @@ 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.*
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.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
/**
[REDACTED_AUTHOR]
*/
internal class AddressPayIdMiddleware {
internal class AddressMiddleware {
fun handle(action: AddressPayIdActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AddressPayIdActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressPayIdActionUi.PasteAddressPayId -> pasteAddressPayId(action.data, action.sourceType, dispatch)
is AddressPayIdActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressPayIdActionUi.CheckAddressPayId -> verifyAddressPayId(action.sourceType, appState, dispatch)
is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressActionUi.PasteAddress -> pasteAddress(action.data, action.sourceType, dispatch)
is AddressActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressActionUi.CheckAddress -> verifyAddress(action.sourceType, appState, dispatch)
else -> return
}
}
private fun handleUserInput(input: String, appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
if (input == sendState.addressPayIdState.viewFieldValue.value) return
if (input == sendState.addressState.viewFieldValue.value) return
setAddressAndCheck(data = input, sourceType = null, isUserInput = true, dispatch = dispatch)
}
private fun pasteAddressPayId(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
private fun pasteAddress(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
setAddressAndCheck(data = data, sourceType = sourceType, isUserInput = false, dispatch = dispatch)
}
@ -54,74 +44,27 @@ internal class AddressPayIdMiddleware {
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(sourceType))
dispatch(SetWalletAddress(data, isUserInput))
dispatch(AddressActionUi.CheckAddress(sourceType))
}
private fun verifyAddressPayId(
private fun verifyAddress(
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
val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput
val address = sendState.addressState.normalFieldValue ?: return
val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput
if (isPayIdEnabled() && PayIdManager.isPayId(addressPayId)) {
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
} else {
verifyAddress(
address = addressPayId,
wallet = wallet,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
)
}
}
private fun verifyPayId(payId: String, wallet: Wallet, isUserInput: Boolean, dispatch: DispatchFunction) {
val blockchain = wallet.blockchain
if (!blockchain.isPayIdSupported()) {
dispatch(SetPayIdError(Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN))
return
}
scope.launch {
val result = PayIdManager().verifyPayId(payId, blockchain)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
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)
}
}
}
}
verifyAddress(
address = address,
wallet = wallet,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
)
}
private fun verifyAddress(
@ -219,41 +162,33 @@ internal class AddressPayIdMiddleware {
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val addressPayId = input ?: return
val address = input ?: return
val wallet = appState?.sendState?.walletManager?.wallet ?: return
val internalDispatcher: (Action) -> Unit = {
when (it) {
is SetWalletAddress, is SetPayIdWalletAddress -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(true))
is SetWalletAddress -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(true))
}
is SetAddressError, is SetPayIdError -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(false))
is SetAddressError -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(false))
}
}
}
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
verifyPayId(addressPayId, wallet, false, internalDispatcher)
} else {
verifyAddress(
address = addressPayId,
wallet = wallet,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,
)
}
}
private fun isPayIdEnabled(): Boolean {
return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false
verifyAddress(
address = address,
wallet = wallet,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,
)
}
}
fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map<String, String> {
return this.split(firstDelimiter)
return this
.split(firstDelimiter)
.map { it.split(secondDelimiter) }
.map { it.first() to it.last().toString() }
.toMap()
.associate { it.first() to it.last() }
}

View file

@ -38,7 +38,7 @@ class RequestFeeMiddleware {
}
val typedAmount = sendState.amountState.amountToExtract ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!!
val destinationAddress = sendState.addressState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = if (scanResponse.isDemoCard()) {
DemoTransactionSender(walletManager)

View file

@ -55,18 +55,17 @@ class SendMiddleware {
{ nextDispatch ->
{ action ->
when (action) {
is AddressPayIdActionUi -> AddressPayIdMiddleware().handle(action, appState(), dispatch)
is AddressActionUi -> AddressMiddleware().handle(action, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
is SendActionUi.CheckIfTransactionDataWasProvided -> {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(
AddressPayIdVerifyAction.AddressVerification.SetWalletAddress(
AddressVerifyAction.AddressVerification.SetWalletAddress(
address = transactionData.destinationAddress,
isUserInput = false,
),
@ -96,7 +95,7 @@ private fun verifyAndSendTransaction(
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val card = appState.globalState.scanResponse?.card ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress ?: return
val destinationAddress = sendState.addressState.destinationWalletAddress ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val feeAmount = sendState.feeState.currentFee ?: return
@ -391,12 +390,6 @@ fun createValidateTransactionError(
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
}
private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) {
val isSendingToPayIdEnabled =
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}
private fun updateWarnings(dispatch: (Action) -> Unit) {
val warningsManager = store.state.globalState.warningManager ?: return
val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return

View file

@ -1,72 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class AddressPayIdReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AddressPayIdActionUi -> handleUiAction(action, sendState, sendState.addressPayIdState)
is AddressPayIdVerifyAction -> handleAction(action, sendState, sendState.addressPayIdState)
else -> sendState
}
private fun handleUiAction(
action: AddressPayIdActionUi,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is AddressPayIdActionUi.HandleUserInput -> state
is AddressPayIdActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressPayIdActionUi.TruncateOrRestore -> {
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressPayIdActionUi.PasteAddressPayId -> return sendState
is AddressPayIdActionUi.CheckClipboard -> return sendState
is AddressPayIdActionUi.CheckAddressPayId -> return sendState
is AddressPayIdActionUi.ChangePayIdState -> state.copy(sendingToPayIdEnabled = action.sendingToPayIdEnabled)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}
private fun handleAction(
action: AddressPayIdVerifyAction,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is PayIdVerification.SetPayIdWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.payId, action.isUserInput),
normalFieldValue = action.payId,
truncatedFieldValue = state.truncate(action.payId),
destinationWalletAddress = action.payIdWalletAddress,
error = null,
)
}
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressPayIdVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
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

@ -0,0 +1,52 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.tap.features.send.redux.AddressActionUi
import com.tangem.tap.features.send.redux.AddressVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class AddressReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AddressActionUi -> handleUiAction(action, sendState, sendState.addressState)
is AddressVerifyAction -> handleAction(action, sendState, sendState.addressState)
else -> sendState
}
private fun handleUiAction(action: AddressActionUi, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressActionUi.HandleUserInput -> state
is AddressActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressActionUi.TruncateOrRestore -> {
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressActionUi.PasteAddress -> return sendState
is AddressActionUi.CheckClipboard -> return sendState
is AddressActionUi.CheckAddress -> return sendState
}
return updateLastState(sendState.copy(addressState = result), result)
}
private fun handleAction(action: AddressVerifyAction, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
}
return updateLastState(sendState.copy(addressState = result), result)
}
}

View file

@ -25,7 +25,7 @@ object SendScreenReducer {
val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
is AddressActionUi, is AddressVerifyAction -> AddressReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer()
@ -71,7 +71,7 @@ private class SendReducer : SendInternalReducer {
amountState = state.amountState.copy(
inputIsEnabled = false,
),
addressPayIdState = state.addressPayIdState.copy(
addressState = state.addressState.copy(
inputIsEnabled = false,
),
)

View file

@ -2,17 +2,16 @@ package com.tangem.tap.features.send.redux.states
import androidx.core.text.isDigitsOnly
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction
import java.math.BigInteger
data class AddressPayIdState(
data class AddressState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null,
val destinationWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val error: AddressVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false,
val pasteIsEnabled: Boolean = false,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
@ -22,8 +21,6 @@ data class AddressPayIdState(
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
fun isPayIdState(): Boolean = destinationWalletAddress != null && destinationWalletAddress != normalFieldValue
}
data class TransactionExtrasState(
@ -98,11 +95,7 @@ data class BinanceMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: BigInteger? = null,
val error: TransactionExtraError? = null,
) {
companion object {
val MAX_NUMBER: BigInteger = BigInteger("FFFFFFFFFFFFFFFF", 16)
}
}
)
// tag must contains only digits
data class XrpDestinationTagState(

View file

@ -34,7 +34,7 @@ data class SendState(
val coinConverter: CurrencyConverter? = null,
val tokenConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val addressState: AddressState = AddressState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
@ -52,7 +52,7 @@ data class SendState(
MainCurrencyType.CRYPTO -> amountState.amountToExtract?.decimals ?: 0
}
fun convertFiatToCoin(value: BigDecimal): BigDecimal {
private fun convertFiatToCoin(value: BigDecimal): BigDecimal {
return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value)
}
@ -60,14 +60,14 @@ data class SendState(
return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value)
}
fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
private fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.coinIsConvertible()) return value
val converter = coinConverter!!
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
private fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.tokenIsConvertible()) return value
val converter = tokenConverter!!
@ -107,13 +107,13 @@ data class SendState(
}
companion object {
fun addressPayIdIsReady(): Boolean = store.state.sendState.addressPayIdState.isReady()
private fun addressIsReady(): Boolean = store.state.sendState.addressState.isReady()
fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
private fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
fun isReadyToRequestFee(): Boolean = addressPayIdIsReady() && amountIsReady()
fun isReadyToRequestFee(): Boolean = addressIsReady() && amountIsReady()
fun isReadyToSend(): Boolean = addressPayIdIsReady() && amountIsReady() &&
fun isReadyToSend(): Boolean = addressIsReady() && amountIsReady() &&
store.state.sendState.feeState.isReady()
}
}

View file

@ -33,7 +33,7 @@ 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.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AddressActionUi.*
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
@ -80,7 +80,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
etAmountToSend = view.findViewById(R.id.etAmountToSend)
initSendButtonStates()
setupAddressOrPayIdLayout()
setupAddressLayout()
setupTransactionExtrasLayout()
setupAmountLayout()
setupFeeLayout()
@ -99,14 +99,14 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
private fun setupAddressOrPayIdLayout() = with(binding.lSendAddressPayid) {
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
private fun setupAddressLayout() = with(binding.lSendAddress) {
store.dispatch(SetTruncateHandler { etAddress.truncateMiddleWith(it, "...") })
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
etAddressOrPayId.apply {
etAddress.apply {
setOnSystemPasteButtonClickListener {
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PastePopup,
),
@ -119,9 +119,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
store.dispatch(AddressActionUi.HandleUserInput(it))
}
.launchIn(mainScope)
}
@ -129,12 +129,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
imvPaste.setOnClickListener {
Analytics.send(Token.Send.ButtonPaste())
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PasteButton,
),
)
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(TruncateOrRestore(!etAddress.isFocused))
}
imvQrCode.setOnClickListener {
Analytics.send(Token.Send.ButtonQRCode())
@ -145,7 +145,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun setupTransactionExtrasLayout() = with(binding.lSendAddressPayid) {
private fun setupTransactionExtrasLayout() = with(binding.lSendAddress) {
// TODO: [REDACTED_TASK_KEY]
etXlmMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -202,15 +202,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
binding.lSendAddressPayid.imvQrCode.postDelayed(
binding.lSendAddress.imvQrCode.postDelayed(
{
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = scannedCode,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused))
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
},
200,
)

View file

@ -7,30 +7,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.text.bold
import com.tangem.common.extensions.remove
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.enableError
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.update
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.ReceiptLayoutType
import com.tangem.tap.features.send.redux.states.ReceiptState
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.StateId
import com.tangem.tap.features.send.redux.states.TransactionExtraError
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.KaspaWarningDialog
@ -61,7 +46,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
lastChangedStates.forEach {
when (it) {
StateId.SEND_SCREEN -> handleSendScreen(fg, state)
StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState)
StateId.ADDRESS_PAY_ID -> handleAddressState(fg, state.addressState)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
StateId.AMOUNT -> handleAmountState(fg, state.amountState)
StateId.FEE -> handleFeeState(fg, state.feeState)
@ -72,7 +57,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
@Suppress("ComplexMethod")
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
with(fg.binding.lSendAddressPayid) {
with(fg.binding.lSendAddress) {
fun showView(view: View, info: Any?) {
view.show(info != null)
}
@ -192,13 +177,10 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
)
}
private fun handleAddressPayIdState(fg: SendFragment, state: AddressPayIdState) =
with(fg.binding.lSendAddressPayid) {
private fun handleAddressState(fg: SendFragment, state: AddressState) {
with(fg.binding.lSendAddress) {
fun parseError(context: Context, error: Error?): String? {
val resId = when (error) {
Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_error_payid_unsupported_by_blockchain
Error.PAY_ID_NOT_REGISTERED -> R.string.send_error_payid_not_registered
Error.PAY_ID_REQUEST_FAILED -> R.string.send_error_payid_request_failed
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address
Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet
else -> null
@ -208,8 +190,8 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
imvPaste.isEnabled = state.pasteIsEnabled
val et = etAddressOrPayId
val til = tilAddressOrPayId
val et = etAddress
val til = tilAddress
val parsedError = parseError(til.context, state.error)
til.isEnabled = state.inputIsEnabled
@ -218,19 +200,15 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
flPaste.show(state.inputIsEnabled)
flQrCode.show(state.inputIsEnabled)
val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid
} else {
R.string.send_destination_hint_address
}
til.hint = til.getString(hintResId)
til.hint = til.getString(R.string.send_destination_hint_address)
til.error = parsedError
til.isErrorEnabled = parsedError != null
til.helperText = state.destinationWalletAddress
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
til.isHelperTextEnabled = parsedError == null
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
}
}
private fun handleAmountState(fg: SendFragment, state: AmountState) = with(fg.binding.lSendAmount) {
if (state.error != null) {

View file

@ -17,16 +17,8 @@ import com.tangem.tap.features.wallet.ui.analytics.WalletAnalyticsEventsMapper
import com.tangem.tap.store
import com.tangem.tap.walletStoresManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@ -71,6 +63,7 @@ internal class WalletViewModel @Inject constructor(
currency = currency,
batch = scanResponse.card.batchId,
signInType = signInType,
walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(),
),
)
}

View file

@ -98,7 +98,7 @@ internal fun WalletDataModel.getAvailableActions(
internal fun WalletDataModel.shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list.orEmpty()
return listOfAddresses.size > 1 && currency.blockchain != Blockchain.BitcoinCash
return listOfAddresses.size > 1
}
internal fun WalletDataModel.assembleWarnings(

View file

@ -1,40 +0,0 @@
package com.tangem.tap.network.payid
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
/**
[REDACTED_AUTHOR]
*/
interface PayIdVerifyApi {
@GET("{user}")
suspend fun verifyAddress(
@Path("user") user: String,
@Header("Accept") acceptNetworkHeader: String,
@Header("PayID-Version") payIdVersion: String = "1.0",
): VerifyPayIdResponse
}
@JsonClass(generateAdapter = true)
data class VerifyPayIdResponse(
val addresses: List<PayIdAddress> = mutableListOf(),
val payId: String? = null,
) {
fun getAddressDetails(): PayIdAddressDetails? = if (addresses.isNotEmpty()) addresses[0].addressDetails else null
}
@JsonClass(generateAdapter = true)
data class PayIdAddress(
var paymentNetwork: String,
var environment: String,
var addressDetailsType: String,
var addressDetails: PayIdAddressDetails,
)
@JsonClass(generateAdapter = true)
data class PayIdAddressDetails(
var address: String,
var tag: String? = null,
)

View file

@ -1,24 +0,0 @@
package com.tangem.tap.network.payid
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
/**
[REDACTED_AUTHOR]
*/
class PayIdVerifyService(
private val baseUrl: String,
) {
private val api = createRetrofitInstance(
baseUrl = baseUrl,
logEnabled = false,
).create(PayIdVerifyApi::class.java)
suspend fun verifyAddress(user: String, network: String): Result<VerifyPayIdResponse> {
return performRequest { api.verifyAddress(user, createNetworkHeader(network)) }
}
private fun createNetworkHeader(network: String): String = "application/$network-mainnet+json"
}