Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-04 13:10:48 +03:00
commit 38572c0a4e
89 changed files with 738 additions and 390 deletions

View file

@ -1,14 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class DetailsScreen(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Details Screen", event, params) {
class ScreenOpened : DetailsScreen("Details Screen Opened")
}

View file

@ -40,19 +40,19 @@ object AmountStatePreviewData {
appCurrency = AppCurrency.Default,
tokenName = stringReference("Tether"),
amountTextField = AmountFieldModel(
value = "",
value = "2 130,81231238",
onValueChange = {},
keyboardOptions = KeyboardOptions.Default,
keyboardActions = KeyboardActions.Default,
cryptoAmount = Amount(
currencySymbol = "USDT",
value = BigDecimal.ZERO,
value = "2112312330.81212331238".toBigDecimal(),
decimals = 18,
type = AmountType.CoinType,
),
fiatAmount = Amount(
currencySymbol = "$",
value = BigDecimal.ZERO,
value = "2111232330.81".toBigDecimal(),
decimals = 2,
type = AmountType.CoinType,
),

View file

@ -121,7 +121,7 @@ private fun AmountBlockV2(
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(top = 8.dp),
modifier = Modifier.padding(top = 8.dp).weight(1f),
) {
ResizableText(
text = firstAmount,

View file

@ -17,7 +17,7 @@
},
{
"name": "STAKING_CARDANO_ENABLED",
"version": "5.28.0"
"version": "undefined"
},
{
"name": "WALLET_CONNECT_REDESIGN_ENABLED",

View file

@ -66,6 +66,7 @@ interface TangemExpressApi {
@Query("requestId") requestId: String,
@Query("refundAddress") refundAddress: String?, // for cex only
@Query("refundExtraId") refundExtraId: String?, // for cex only
@Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")

View file

@ -394,6 +394,7 @@
<string name="domain_receive_assets_default_address">Standardadresse</string>
<string name="domain_receive_assets_legacy_address">Legacy adresse</string>
<string name="domain_receive_assets_navigation_title">Empfangen von Vermögenswerten</string>
<string name="domain_receive_assets_network_name_address">%s Adresse</string>
<string name="domain_receive_assets_onboarding_description">Das Senden von Vermögenswerten in anderen Netzwerken führt zu dauerhaftem Verlust.</string>
<string name="domain_receive_assets_onboarding_network_name">%s Netzwerk</string>
<string name="domain_receive_assets_onboarding_title">Sende Geld nur mit</string>

View file

@ -332,6 +332,7 @@
<string name="domain_receive_assets_default_address">Dirección por defecto</string>
<string name="domain_receive_assets_legacy_address">Dirección Legacy</string>
<string name="domain_receive_assets_navigation_title">Recibir activos</string>
<string name="domain_receive_assets_network_name_address">%s dirección</string>
<string name="domain_receive_assets_onboarding_description">Enviar activos a otras redes resultará en una pérdida permanente.</string>
<string name="domain_receive_assets_onboarding_title">Envíe fondos utilizando solo</string>
<string name="email_preface_wc_error">Hola equipo de soporte, he encontrado un error con el código: %s</string>

View file

@ -309,6 +309,7 @@
<string name="domain_receive_assets_default_address">Adresse par défaut</string>
<string name="domain_receive_assets_legacy_address">Legacy adresse</string>
<string name="domain_receive_assets_navigation_title">Recevoir des actifs</string>
<string name="domain_receive_assets_network_name_address">%s adresse</string>
<string name="domain_receive_assets_onboarding_description">Lenvoi dactifs sur dautres réseaux entraînera une perte définitive.</string>
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
<string name="email_preface_wc_error">Bonjour équipe de support, jai rencontré une erreur avec le code : %s</string>

View file

@ -314,6 +314,7 @@
<string name="domain_receive_assets_default_address">Основной адрес</string>
<string name="domain_receive_assets_legacy_address">Legacy адрес</string>
<string name="domain_receive_assets_navigation_title">Получить активы</string>
<string name="domain_receive_assets_network_name_address">%s адрес</string>
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
<string name="domain_receive_assets_onboarding_network_name">%s сети</string>
<string name="domain_receive_assets_onboarding_title">Отправляйте средства, используя только</string>

View file

@ -305,6 +305,7 @@
<string name="domain_receive_assets_default_address">Основна адреса</string>
<string name="domain_receive_assets_legacy_address">Legacy адреса</string>
<string name="domain_receive_assets_navigation_title">Отримати активи</string>
<string name="domain_receive_assets_network_name_address">%s адреса</string>
<string name="domain_receive_assets_onboarding_description">Надсилання активів в інші мережі призведе до безповоротної втрати.</string>
<string name="domain_receive_assets_onboarding_title">Надсилайте кошти, використовуючи лише</string>
<string name="email_preface_wc_error">Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s</string>

View file

@ -395,6 +395,7 @@
<string name="domain_receive_assets_default_address">Default Address</string>
<string name="domain_receive_assets_legacy_address">Legacy Address</string>
<string name="domain_receive_assets_navigation_title">Receive assets</string>
<string name="domain_receive_assets_network_name_address">%s address</string>
<string name="domain_receive_assets_onboarding_description">Sending assets in other networks will result in permanent loss.</string>
<string name="domain_receive_assets_onboarding_network_name">%s network</string>
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>

View file

@ -5,12 +5,14 @@ import com.tangem.datasource.api.tangemTech.models.GeoResponse
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.GB_COUNTRY
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
@ -56,7 +58,14 @@ internal class DefaultSettingsRepository(
appLogsStore.deleteDeprecatedLogs(maxSize)
}
override suspend fun isSendTapHelpPreviewEnabled(): Boolean {
override fun isSendTapHelpPreviewEnabled(): Flow<Boolean> {
return appPreferencesStore.get(
key = PreferencesKeys.SEND_TAP_HELP_PREVIEW_KEY,
default = true,
)
}
override suspend fun isSendTapHelpPreviewEnabledSync(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.SEND_TAP_HELP_PREVIEW_KEY,
default = true,

View file

@ -17,10 +17,7 @@ import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.express.models.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkStatus
@ -96,13 +93,15 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
it.currency.network.backendId == pair.to.network
}
if (statusFrom != null && statusTo != null) {
val mappedProviders = pair.providers.mapNotNull {
mappedProviders[it.providerId]
}
if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) {
SwapPairModel(
from = statusFrom,
to = statusTo,
providers = pair.providers.mapNotNull {
mappedProviders[it.providerId]
},
providers = mappedProviders,
)
} else {
null
@ -151,13 +150,15 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await())
val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await())
if (currencyStatusFrom != null && currencyStatusTo != null) {
val mappedProvider = pair.providers.mapNotNull {
mappedProviders[it.providerId]
}
if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) {
SwapPairModel(
from = currencyStatusFrom,
to = currencyStatusTo,
providers = pair.providers.mapNotNull {
mappedProviders[it.providerId]
},
providers = mappedProvider,
)
} else {
null
@ -208,6 +209,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
toAddress: String,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
): SwapDataModel = withContext(coroutineDispatcher.io) {
val requestId = UUID.randomUUID().toString()
val fromCryptoCurrency = fromCryptoCurrencyStatus.currency
@ -239,6 +241,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
refundAddress = refundData?.refundAddress,
refundExtraId = refundData?.refundExtraId,
userWalletId = userWallet.walletId.stringValue,
partnerOperationType = expressOperationType.value,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,

View file

@ -356,10 +356,6 @@ internal class DefaultCurrenciesRepository(
getMultiCurrencyWalletCurrencies(userWallet)
.onEach { send(it) }
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh = false)
}
}
}

View file

@ -100,7 +100,12 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
).map { settledSession ->
val newSession = WcSession(
wallet = sessionForApprove.wallet,
sdkModel = WcSdkSessionConverter.convert(settledSession.session),
sdkModel = WcSdkSessionConverter.convert(
value = WcSdkSessionConverter.Input(
originUrl = sdkVerifyContext.origin,
session = settledSession.session,
),
),
securityStatus = proposalState.dAppSession.securityStatus,
networks = sessionForApprove.network.toSet(),
connectingTime = DateTime.now().millis,
@ -109,8 +114,9 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
sessionsManager.saveSession(newSession)
analytics.send(
WcAnalyticEvents.DAppConnected(
proposalState.dAppSession,
sessionForApprove,
sessionProposal = proposalState.dAppSession,
sessionForApprove = sessionForApprove,
securityStatus = proposalState.dAppSession.securityStatus,
),
)
newSession

View file

@ -69,8 +69,7 @@ internal class WcPairSdkDelegate : WcSdkObserver {
ifRight = { result ->
when (result) {
is Wallet.Model.SettledSessionResponse.Result -> result.right()
is Wallet.Model.SettledSessionResponse.Error ->
ApprovalFailed(result.errorMessage).left()
is Wallet.Model.SettledSessionResponse.Error -> ApprovalFailed(result.errorMessage).left()
}
},
)
@ -113,8 +112,9 @@ internal class WcPairSdkDelegate : WcSdkObserver {
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
val sessionProposalWithRealUrl = sessionProposal.copy(url = verifyContext.origin)
// Triggered when wallet receives the session proposal sent by a Dapp
onSessionProposal.trySend(sessionProposal to verifyContext)
onSessionProposal.trySend(sessionProposalWithRealUrl to verifyContext)
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
@ -147,7 +147,7 @@ internal class WcPairSdkDelegate : WcSdkObserver {
else -> WcPairError.PairingFailed(this.localizedMessage.orEmpty())
}
private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left()
private fun Throwable.toApproveError() = ApprovalFailed(this.localizedMessage.orEmpty()).left()
companion object {
private const val CALLBACK_TIMEOUT = 15

View file

@ -26,7 +26,12 @@ internal class DefaultWcRequestService(
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
val sr = WcSdkSessionRequestConverter.convert(sessionRequest)
val sr = WcSdkSessionRequestConverter.convert(
WcSdkSessionRequestConverter.Input(
originUrl = verifyContext.origin,
sessionRequest = sessionRequest,
),
)
Timber.tag(WC_TAG).i("handle request $sr")
val name = requestConverters.firstNotNullOfOrNull { it.toWcMethodName(sr) }
?: WcMethodName.Unsupported(sr.request.method)

View file

@ -70,6 +70,7 @@ internal class DefaultWcSessionsManager(
WcSessionDTO(
topic = session.sdkModel.topic,
walletId = session.wallet.walletId,
url = session.sdkModel.appMetaData.url,
securityStatus = session.securityStatus,
connectingTime = session.connectingTime ?: DateTime.now().millis,
),
@ -105,8 +106,15 @@ internal class DefaultWcSessionsManager(
.map { walletId ->
flow {
emit(
legacyStore.loadSessions(walletId.stringValue).map {
WcSessionDTO(it.topic, walletId, CheckDAppResult.FAILED_TO_VERIFY)
legacyStore.loadSessions(walletId.stringValue).mapNotNull { legacySession ->
val url =
inSdk.find { it.topic == legacySession.topic }?.metaData?.url ?: return@mapNotNull null
WcSessionDTO(
topic = legacySession.topic,
walletId = walletId,
url = url,
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
)
},
)
}
@ -130,9 +138,15 @@ internal class DefaultWcSessionsManager(
val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null
val networks = wcNetworksConverter.findWalletNetworks(wallet, sdkSession)
val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: ""
WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
sdkModel = WcSdkSessionConverter.convert(
value = WcSdkSessionConverter.Input(
originUrl = originUrl,
session = sdkSession,
),
),
securityStatus = storeSession.securityStatus,
networks = networks,
connectingTime = storeSession.connectingTime,

View file

@ -75,6 +75,7 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
WcAnalyticEvents.SignatureRequestHandled(
rawRequest = context.rawSdkRequest,
network = context.network,
securityStatus = context.session.securityStatus,
)
},
)

View file

@ -4,7 +4,7 @@ import com.reown.android.Core
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.utils.converter.Converter
internal object WcAppMetaDataConverter : Converter<Core.Model.AppMetaData, WcAppMetaData> {
internal object WcAppMetaDataConverter : Converter<WcAppMetaDataConverter.Input, WcAppMetaData> {
val empty
get() = WcAppMetaData(
@ -18,16 +18,18 @@ internal object WcAppMetaDataConverter : Converter<Core.Model.AppMetaData, WcApp
verifyUrl = null,
)
override fun convert(value: Core.Model.AppMetaData): WcAppMetaData {
override fun convert(value: Input): WcAppMetaData {
return WcAppMetaData(
name = value.name,
description = value.description,
url = value.url,
icons = value.icons,
redirect = value.redirect,
appLink = value.appLink,
linkMode = value.linkMode,
verifyUrl = value.verifyUrl,
name = value.peerMetaData.name,
description = value.peerMetaData.description,
url = value.originUrl,
icons = value.peerMetaData.icons,
redirect = value.peerMetaData.redirect,
appLink = value.peerMetaData.appLink,
linkMode = value.peerMetaData.linkMode,
verifyUrl = value.peerMetaData.verifyUrl,
)
}
internal data class Input(val originUrl: String, val peerMetaData: Core.Model.AppMetaData)
}

View file

@ -4,13 +4,22 @@ import com.reown.walletkit.client.Wallet
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.utils.converter.Converter
internal object WcSdkSessionConverter : Converter<Wallet.Model.Session, WcSdkSession> {
internal object WcSdkSessionConverter : Converter<WcSdkSessionConverter.Input, WcSdkSession> {
override fun convert(value: Wallet.Model.Session): WcSdkSession {
override fun convert(value: Input): WcSdkSession {
return WcSdkSession(
topic = value.topic,
appMetaData = value.metaData?.let { WcAppMetaDataConverter.convert(it) } ?: WcAppMetaDataConverter.empty,
namespaces = value.namespaces.mapValues { (_, session) ->
topic = value.session.topic,
appMetaData = value.session.metaData
?.let {
WcAppMetaDataConverter.convert(
value = WcAppMetaDataConverter.Input(
originUrl = value.originUrl,
peerMetaData = it,
),
)
}
?: WcAppMetaDataConverter.empty,
namespaces = value.session.namespaces.mapValues { (_, session) ->
WcSdkSession.Session(
chains = session.chains ?: listOf(),
accounts = session.accounts,
@ -20,4 +29,6 @@ internal object WcSdkSessionConverter : Converter<Wallet.Model.Session, WcSdkSes
},
)
}
internal data class Input(val originUrl: String, val session: Wallet.Model.Session)
}

View file

@ -4,15 +4,25 @@ import com.reown.walletkit.client.Wallet
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.utils.converter.Converter
internal object WcSdkSessionRequestConverter : Converter<Wallet.Model.SessionRequest, WcSdkSessionRequest> {
internal object WcSdkSessionRequestConverter : Converter<WcSdkSessionRequestConverter.Input, WcSdkSessionRequest> {
override fun convert(value: Wallet.Model.SessionRequest): WcSdkSessionRequest {
override fun convert(value: Input): WcSdkSessionRequest {
return WcSdkSessionRequest(
topic = value.topic,
chainId = value.chainId,
dAppMetaData = value.peerMetaData?.let { WcAppMetaDataConverter.convert(it) }
topic = value.sessionRequest.topic,
chainId = value.sessionRequest.chainId,
dAppMetaData = value.sessionRequest.peerMetaData
?.let {
WcAppMetaDataConverter.convert(
value = WcAppMetaDataConverter.Input(
originUrl = value.originUrl,
peerMetaData = it,
),
)
}
?: WcAppMetaDataConverter.empty,
request = JSONRPCRequestConverter.convert(value.request),
request = JSONRPCRequestConverter.convert(value.sessionRequest.request),
)
}
internal data class Input(val originUrl: String, val sessionRequest: Wallet.Model.SessionRequest)
}

View file

@ -60,7 +60,7 @@ internal class DefaultWcPairUseCaseTest {
private val sdkVerifyContext: Wallet.Model.VerifyContext
get() = Wallet.Model.VerifyContext(
id = 9259,
origin = "vituperata",
origin = "https://react-app.walletconnect.com/",
validation = Wallet.Model.Validation.VALID,
verifyUrl = "https://search.yahoo.com/search?p=id",
isScam = false,
@ -68,6 +68,7 @@ internal class DefaultWcPairUseCaseTest {
private val unsupportedDAppUrl = "dydx.trade"
private val unsupportedSdkProposal get() = sdkProposal.copy(url = unsupportedDAppUrl)
private val unsupportedSdkVerifyContext get() = sdkVerifyContext.copy(origin = unsupportedDAppUrl)
private val sessionForApprove: WcSessionApprove
get() = WcSessionApprove(
@ -100,7 +101,12 @@ internal class DefaultWcPairUseCaseTest {
private val Wallet.Model.Session.sessionForSave: WcSession
get() = WcSession(
wallet = sessionForApprove.wallet,
sdkModel = WcSdkSessionConverter.convert(this),
sdkModel = WcSdkSessionConverter.convert(
value = WcSdkSessionConverter.Input(
originUrl = sdkVerifyContext.origin,
session = this,
),
),
securityStatus = CheckDAppResult.SAFE,
networks = setOf(),
connectingTime = null,
@ -138,7 +144,7 @@ internal class DefaultWcPairUseCaseTest {
assertEquals(loading, awaitItem())
coVerifyOrder {
sdkDelegate.pair(url)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin))
}
assert(awaitItem() is WcPairState.Proposal)
expectNoEvents()
@ -161,7 +167,7 @@ internal class DefaultWcPairUseCaseTest {
assertEquals(loading, awaitItem())
coVerifyOrder {
sdkDelegate.pair(url)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin))
}
assert(awaitItem() is WcPairState.Proposal)
useCase.approve(sessionForApprove)
@ -194,7 +200,7 @@ internal class DefaultWcPairUseCaseTest {
assertEquals(loading, awaitItem())
coVerifyOrder {
sdkDelegate.pair(url)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin))
}
assert(awaitItem() is WcPairState.Proposal)
useCase.reject()
@ -207,7 +213,7 @@ internal class DefaultWcPairUseCaseTest {
@Test
fun `success pair and reject unsupported dApp`() = runTest {
coEvery { sdkDelegate.pair(url) } returns (unsupportedSdkProposal to sdkVerifyContext).right()
coEvery { sdkDelegate.pair(url) } returns (unsupportedSdkProposal to unsupportedSdkVerifyContext).right()
val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp(unsupportedSdkProposal.name))
val useCase = useCaseFactory()
@ -255,7 +261,7 @@ internal class DefaultWcPairUseCaseTest {
coVerifyOrder {
sdkDelegate.pair(url)
associateNetworksDelegate.associate(sdkProposal)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin))
}
assert(awaitItem() is WcPairState.Proposal)
useCase.approve(sessionForApprove)

View file

@ -0,0 +1,6 @@
package com.tangem.domain.express.models
enum class ExpressOperationType(val value: String) {
SWAP("swap"),
SEND_WITH_SWAP("swap-and-send"),
}

View file

@ -10,5 +10,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository
*/
class IsSendTapHelpEnabledUseCase(private val settingsRepository: SettingsRepository) {
suspend operator fun invoke() = Either.catch { settingsRepository.isSendTapHelpPreviewEnabled() }
operator fun invoke() = Either.catch { settingsRepository.isSendTapHelpPreviewEnabled() }
suspend fun invokeSync() = Either.catch { settingsRepository.isSendTapHelpPreviewEnabledSync() }
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.settings.repositories
import com.tangem.domain.settings.usercountry.models.UserCountry
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@Suppress("TooManyFunctions")
@ -16,7 +17,9 @@ interface SettingsRepository {
fun deleteDeprecatedLogs(maxSize: Int)
suspend fun isSendTapHelpPreviewEnabled(): Boolean
fun isSendTapHelpPreviewEnabled(): Flow<Boolean>
suspend fun isSendTapHelpPreviewEnabledSync(): Boolean
suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean)

View file

@ -1,5 +1,6 @@
package com.tangem.domain.swap
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.express.models.ExpressRateType
@ -74,6 +75,7 @@ interface SwapRepositoryV2 {
* @param toAddress destination address
* @param expressProvider selected swap provider
* @param rateType selected provider rate type
* @param expressOperationType operation type swap or send with swap
*/
suspend fun getSwapData(
userWallet: UserWallet,
@ -83,6 +85,7 @@ interface SwapRepositoryV2 {
toAddress: String,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
): SwapDataModel
/**

View file

@ -2,6 +2,7 @@ package com.tangem.domain.swap.usecase
import arrow.core.Either
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
@ -25,6 +26,7 @@ class GetSwapDataUseCase(
toAddress: String,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
): Either<ExpressError, SwapDataModel> = Either.catch {
swapRepositoryV2.getSwapData(
userWallet = userWallet,
@ -34,6 +36,7 @@ class GetSwapDataUseCase(
toAddress = toAddress,
expressProvider = expressProvider,
rateType = rateType,
expressOperationType = expressOperationType,
)
}.mapLeft { throwable ->
swapErrorResolver.resolve(throwable)

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens.model.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION
import com.tangem.core.analytics.models.AnalyticsParam.Key.BALANCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
@ -16,11 +17,26 @@ sealed class TokenScreenAnalyticsEvent(
) : AnalyticsEvent("Token", event, params) {
/** Legacy event. It has a unique category, but it also is sent on TokenScreen */
class DetailsScreenOpened(token: String) : AnalyticsEvent(
class DetailsScreenOpened(
blockchain: String,
token: String,
tokenBalance: TokenBalance,
) : AnalyticsEvent(
category = "Details Screen",
event = "Details Screen Opened",
params = mapOf("Token" to token),
)
params = mapOf(
BLOCKCHAIN to blockchain,
TOKEN_PARAM to token,
BALANCE to tokenBalance.name,
),
) {
sealed class TokenBalance(val name: String) {
data object Full : TokenBalance("Full")
data object Error : TokenBalance("Error")
data object Empty : TokenBalance("Empty")
data object NoAddress : TokenBalance("No Address")
}
}
class ButtonRemoveToken(token: String) : TokenScreenAnalyticsEvent(
"Button - Remove Token",

View file

@ -5,16 +5,17 @@ sealed class WcPairError(
override val message: String = "",
) : Exception() {
data class UriAlreadyUsed(override val message: String) : WcPairError("107 001 001")
data class PairingFailed(override val message: String) : WcPairError("107 001 002")
data object InvalidDomainURL : WcPairError("107 001 003")
data class UnsupportedDApp(val appName: String) : WcPairError("107 001 004")
data class UnsupportedBlockchains(val chains: Set<String>, val appName: String) : WcPairError("107 001 005")
data class UriAlreadyUsed(override val message: String) : WcPairError(code = "107 001 001")
data class PairingFailed(override val message: String) : WcPairError(code = "107 001 002")
data object InvalidDomainURL : WcPairError(code = "107 001 003")
data object InvalidConnectionRequest : WcPairError("107 002 001")
data object ProposalExpired : WcPairError("107 002 002")
data class ApprovalFailed(override val message: String) : WcPairError("107 002 003")
data object RejectionFailed : WcPairError("107 002 004")
data class Unknown(override val message: String) : WcPairError(message)
data class TimeoutException(override val message: String) : WcPairError(message)
data class UnsupportedDApp(val appName: String) : WcPairError(code = "107 001 004")
data class UnsupportedBlockchains(val chains: Set<String>, val appName: String) : WcPairError(code = "107 001 005")
data object InvalidConnectionRequest : WcPairError(code = "107 002 001")
data object ProposalExpired : WcPairError(code = "107 002 002")
data class ApprovalFailed(override val message: String) : WcPairError(code = "107 002 003")
data object RejectionFailed : WcPairError(code = "107 002 004")
data class Unknown(override val message: String) : WcPairError(code = "107 003 001")
data class TimeoutException(override val message: String) : WcPairError(code = "107 003 002")
}

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWalletId
data class WcSessionDTO(
val topic: String,
val walletId: UserWalletId,
val url: String?,
val securityStatus: CheckDAppResult = CheckDAppResult.FAILED_TO_VERIFY,
val connectingTime: Long? = null,
)

View file

@ -5,6 +5,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult.*
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.WcAnalyticEvents.ButtonDisconnectAll.toAnalyticVerificationStatus
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
@ -42,11 +43,7 @@ sealed class WcAnalyticEvents(
event = "dApp Connection Requested",
params = mapOf(
NETWORKS to network.joinToString(",") { it.name },
DOMAIN_VERIFICATION to when (domainVerification) {
SAFE -> "Verified"
UNSAFE -> "Risky"
FAILED_TO_VERIFY -> "Unknown"
},
DOMAIN_VERIFICATION to domainVerification.toAnalyticVerificationStatus(),
),
)
@ -62,12 +59,14 @@ sealed class WcAnalyticEvents(
class DAppConnected(
sessionProposal: WcSessionProposal,
sessionForApprove: WcSessionApprove,
securityStatus: CheckDAppResult,
) : WcAnalyticEvents(
event = "dApp Connected",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to sessionProposal.dAppMetaData.name,
AnalyticsParam.Key.DAPP_URL to sessionProposal.dAppMetaData.url,
AnalyticsParam.Key.BLOCKCHAIN to sessionForApprove.network.joinToString(",") { it.name },
DOMAIN_VERIFICATION to securityStatus.toAnalyticVerificationStatus(),
),
)
@ -105,6 +104,7 @@ sealed class WcAnalyticEvents(
rawRequest: WcSdkSessionRequest,
network: Network,
emulationStatus: EmulationStatus?,
securityStatus: CheckDAppResult?,
) : WcAnalyticEvents(
event = "Signature Request Received",
params = mapOf(
@ -113,6 +113,7 @@ sealed class WcAnalyticEvents(
AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method,
AnalyticsParam.Key.BLOCKCHAIN to network.name,
AnalyticsParam.Key.EMULATION_STATUS to emulationStatus?.status,
AnalyticsParam.Key.TYPE to securityStatus?.toAnalyticVerificationStatus(),
).mapNotNullValues { it.value },
) {
enum class EmulationStatus(val status: String) {
@ -125,6 +126,7 @@ sealed class WcAnalyticEvents(
class SignatureRequestHandled(
rawRequest: WcSdkSessionRequest,
network: Network,
securityStatus: CheckDAppResult,
) : WcAnalyticEvents(
event = "Signature Request Handled",
params = mapOf(
@ -132,6 +134,7 @@ sealed class WcAnalyticEvents(
AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url,
AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method,
AnalyticsParam.Key.BLOCKCHAIN to network.name,
AnalyticsParam.Key.TYPE to securityStatus.toAnalyticVerificationStatus(),
),
)
@ -215,18 +218,26 @@ sealed class WcAnalyticEvents(
Source.Domain -> "Domain"
Source.SmartContract -> "Smart Contract"
},
AnalyticsParam.Key.TYPE to when (securityStatus) {
SAFE -> "Verified"
UNSAFE -> "Risky"
FAILED_TO_VERIFY -> "Unknown"
},
AnalyticsParam.Key.TYPE to securityStatus.toAnalyticVerificationStatus(),
),
) {
enum class Source { Domain, SmartContract }
}
enum class DAppVerificationStatus(val status: String) {
Verified("Verified"),
Risky("Risky"),
Unknown("Unknown"),
}
fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) {
SAFE -> DAppVerificationStatus.Verified
UNSAFE -> DAppVerificationStatus.Risky
FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown
}.status
companion object {
const val NETWORKS = "Networks"
const val DOMAIN_VERIFICATION = "Domain Verification"
const val WC_CATEGORY_NAME = "Wallet Connect"

View file

@ -7,21 +7,22 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.scan.isRing
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateWalletUseCase
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@ -53,6 +54,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val userWalletsListManager: UserWalletsListManager,
private val saveWalletUseCase: SaveWalletUseCase,
private val updateWalletUseCase: UpdateWalletUseCase,
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
private val walletsRepository: WalletsRepository,
@ -77,7 +79,8 @@ internal class MultiWalletFinalizeModel @Inject constructor(
// sets proper artwork state for initial step
// (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY])
when (getInitialStep()) {
MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ }
MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */
}
MultiWalletFinalizeUM.Step.BackupDevice1 -> {
onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded)
}
@ -249,11 +252,13 @@ internal class MultiWalletFinalizeModel @Inject constructor(
}
?: userWalletCreated
saveWalletUseCase(
userWallet = userWallet.requireColdWallet().copy(
scanResponse = scanResponse.updateScanResponseAfterBackup(),
),
canOverride = true,
updateWalletUseCase(
userWalletId = userWallet.walletId,
update = {
it.requireColdWallet().copy(
scanResponse = scanResponse.updateScanResponseAfterBackup(),
)
},
)
userWallet

View file

@ -20,6 +20,6 @@ interface SendComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, SendComponent>
interface ModelCallback {
fun onConvertToAnotherToken(lastAmount: String)
fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean)
}
}

View file

@ -18,6 +18,7 @@ sealed class DestinationUM {
val isValidating: Boolean = false,
val isInitialized: Boolean = false,
val isRedesignEnabled: Boolean = false,
val isRecentHidden: Boolean,
) : DestinationUM()
data class Empty(

View file

@ -30,9 +30,11 @@ internal class SendEntryPointModel @Inject constructor(
ChooseManagedTokensComponent.ModelCallback {
private var lastSavedAmount = ""
private var isEnterInFiat = false
override fun onConvertToAnotherToken(lastAmount: String) {
override fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean) {
lastSavedAmount = lastAmount
isEnterInFiat = isEnterInFiatSelected
modelScope.launch {
val showSendViaSwapNotification = shouldShowNotificationUseCase(
NotificationId.SendViaSwapTokenSelectorNotification.key,
@ -45,12 +47,11 @@ internal class SendEntryPointModel @Inject constructor(
}
}
override fun onCloseSwap(lastAmount: String) {
override fun onCloseSwap(lastAmount: String, isEnterInFiatSelected: Boolean) {
lastSavedAmount = lastAmount
isEnterInFiat = isEnterInFiatSelected
modelScope.launch {
if (lastAmount.isNotBlank()) {
sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount)
}
sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount, isEnterInFiat)
router.replaceAll(SendEntryRoute.Send)
}
}
@ -58,9 +59,7 @@ internal class SendEntryPointModel @Inject constructor(
@Suppress("MagicNumber")
override fun onResult() {
modelScope.launch {
if (lastSavedAmount.isNotBlank()) {
swapAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount)
}
swapAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount, isEnterInFiat)
// Workaround in order to execute correct exit animation on SendEntryRoute.ChooseToken
router.pop()
delay(10L)
@ -70,7 +69,7 @@ internal class SendEntryPointModel @Inject constructor(
override fun onBack() {
modelScope.launch {
sendAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount)
sendAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount, isEnterInFiat)
router.pop()
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted
@ -46,6 +47,7 @@ internal class FeeSelectorModel @Inject constructor(
private val urlOpener: UrlOpener,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
private val feeSelectorReloadListener: FeeSelectorReloadListener,
private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener,
private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger,
@ -188,7 +190,10 @@ internal class FeeSelectorModel @Inject constructor(
source = SendScreenSource.Fee,
),
)
feeSelectorBottomSheet.activate(Unit)
modelScope.launch {
neverShowTapHelpUseCase()
feeSelectorBottomSheet.activate(Unit)
}
}
private fun subscribeOnFeeReloadTriggerUpdates() {

View file

@ -303,12 +303,13 @@ internal class DefaultSendComponent @AssistedInject constructor(
val isEmptyStack = childStack.value.backStack.isEmpty()
val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success
val isStubComponent = childStack.value.active.instance is StubComponent
val isSendingInProgress = (model.uiState.value.confirmUM as? ConfirmUM.Content)?.isSending == true
val isPopSend = isEmptyRoute || isEmptyStack || isSuccess || isStubComponent
if (isPopSend) {
router.pop()
} else {
stackNavigation.pop()
when {
isSendingInProgress -> Unit // Do not anything while transaction sending in progress
isPopSend -> router.pop()
else -> stackNavigation.pop()
}
}

View file

@ -20,13 +20,13 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
@ -168,6 +168,7 @@ internal class SendConfirmModel @Inject constructor(
subscribeOnCheckFeeResultUpdates()
initialState()
subscribeOnBalanceHidden()
subscribeOnTapHelpUpdates()
}
fun updateState(state: SendUM) {
@ -232,10 +233,6 @@ internal class SendConfirmModel @Inject constructor(
override fun showEditDestination() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
@ -249,10 +246,6 @@ internal class SendConfirmModel @Inject constructor(
override fun showEditAmount() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
@ -266,10 +259,6 @@ internal class SendConfirmModel @Inject constructor(
override fun showEditFee() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
@ -348,7 +337,7 @@ internal class SendConfirmModel @Inject constructor(
val confirmUM = uiState.value.confirmUM
modelScope.launch {
val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false }
val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false }
if (confirmUM is ConfirmUM.Empty) {
_uiState.update {
it.copy(
@ -364,6 +353,16 @@ internal class SendConfirmModel @Inject constructor(
}
}
private fun subscribeOnTapHelpUpdates() {
isSendTapHelpEnabledUseCase().getOrNull()
?.onEach { showTapHelp ->
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM)
}
}?.launchIn(modelScope)
}
private fun subscribeOnNotificationsUpdateTrigger() {
notificationsUpdateListener.hasErrorFlow
.onEach { hasError ->

View file

@ -226,14 +226,14 @@ internal class SendModel @Inject constructor(
}
}
override fun onConvertToAnotherToken(lastAmount: String) {
override fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean) {
analyticsEventHandler.send(
SendAnalyticEvents.ConvertTokenButtonClicked(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
params.callback?.onConvertToAnotherToken(lastAmount = lastAmount)
params.callback?.onConvertToAnotherToken(lastAmount = lastAmount, isEnterInFiatSelected = isEnterInFiatSelected)
}
override fun resetSendNavigation() {
@ -454,7 +454,7 @@ internal class SendModel @Inject constructor(
)
// If it is in active state use flow to update value in amount component
modelScope.launch {
amount?.let { sendAmountUpdateTrigger.triggerUpdateAmount(it) }
amount?.let { sendAmountUpdateTrigger.triggerUpdateAmount(it, null) }
}
}

View file

@ -215,11 +215,13 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty
val isEmptyStack = childStack.value.backStack.isEmpty()
val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success
val isSendingInProgress = (model.uiState.value.confirmUM as? ConfirmUM.Content)?.isSending == true
if (isEmptyRoute || isEmptyStack || isSuccess) {
router.pop()
} else {
stackNavigation.pop()
val isPopSend = isEmptyRoute || isEmptyStack || isSuccess
when {
isSendingInProgress -> Unit // Do not anything while transaction sending in progress
isPopSend -> router.pop()
else -> stackNavigation.pop()
}
}

View file

@ -136,6 +136,7 @@ internal class NFTSendConfirmModel @Inject constructor(
configConfirmNavigation()
subscribeOnNotificationsUpdateTrigger()
subscribeOnCheckFeeResultUpdates()
subscribeOnTapHelpUpdates()
initialState()
}
@ -176,10 +177,6 @@ internal class NFTSendConfirmModel @Inject constructor(
override fun showEditDestination() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
@ -193,10 +190,6 @@ internal class NFTSendConfirmModel @Inject constructor(
override fun showEditFee() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
@ -262,7 +255,7 @@ internal class NFTSendConfirmModel @Inject constructor(
}
modelScope.launch {
val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false }
val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false }
if (confirmUM is ConfirmUM.Empty || isEmptyFee) {
_uiState.update {
it.copy(
@ -277,6 +270,16 @@ internal class NFTSendConfirmModel @Inject constructor(
}
}
private fun subscribeOnTapHelpUpdates() {
isSendTapHelpEnabledUseCase().getOrNull()
?.onEach { showTapHelp ->
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM)
}
}?.launchIn(modelScope)
}
private fun subscribeOnNotificationsUpdateTrigger() {
notificationsUpdateListener.hasErrorFlow
.onEach { hasError ->

View file

@ -40,7 +40,7 @@ internal class SendAmountComponent(
interface ModelCallback : NavigationModelCallback {
fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean)
fun onConvertToAnotherToken(lastAmount: String)
fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean)
fun resetSendNavigation()
fun onError(error: GetUserWalletError)
}

View file

@ -30,7 +30,7 @@ interface SendAmountReduceListener {
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SendAmountUpdateTrigger {
suspend fun triggerUpdateAmount(amountValue: String)
suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean?)
}
/**
@ -38,7 +38,7 @@ interface SendAmountUpdateTrigger {
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SendAmountUpdateListener {
val updateAmountTriggerFlow: Flow<String>
val updateAmountTriggerFlow: Flow<Pair<String, Boolean?>>
}
@Singleton
@ -51,7 +51,7 @@ internal class DefaultSendAmountReduceTrigger @Inject constructor() :
override val reduceToTriggerFlow = MutableSharedFlow<BigDecimal>()
override val reduceByTriggerFlow = MutableSharedFlow<ReduceByData>()
override val ignoreReduceTriggerFlow = MutableSharedFlow<Unit>()
override val updateAmountTriggerFlow = MutableSharedFlow<String>()
override val updateAmountTriggerFlow = MutableSharedFlow<Pair<String, Boolean?>>()
override suspend fun triggerReduceBy(reduceBy: ReduceByData) {
reduceByTriggerFlow.emit(reduceBy)
@ -65,7 +65,7 @@ internal class DefaultSendAmountReduceTrigger @Inject constructor() :
ignoreReduceTriggerFlow.emit(Unit)
}
override suspend fun triggerUpdateAmount(amountValue: String) {
updateAmountTriggerFlow.emit(amountValue)
override suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean?) {
updateAmountTriggerFlow.emit(amountValue to isEnterInFiatSelected)
}
}

View file

@ -290,7 +290,15 @@ internal class SendAmountModel @Inject constructor(
isPrimaryButtonEnabled = false,
) ?: it
}
amountParams.callback.onConvertToAnotherToken(amountFieldData?.amountTextField?.value.orEmpty())
val isEnterInFiatSelected = amountFieldData?.amountTextField?.isFiatValue == true
val lastAmount = if (isEnterInFiatSelected) {
amountFieldData.amountTextField.fiatValue
} else {
amountFieldData?.amountTextField?.value
}
amountParams.callback.onConvertToAnotherToken(lastAmount.orEmpty(), isEnterInFiatSelected)
}
private fun subscribeOnAmountReduceToTriggerUpdates() {
@ -346,7 +354,8 @@ internal class SendAmountModel @Inject constructor(
private fun subscribeOnAmountUpdateTriggerUpdates() {
sendAmountUpdateListener.updateAmountTriggerFlow
.onEach { amount ->
.onEach { (amount, isEnterInFiat) ->
isEnterInFiat?.let { onCurrencyChangeClick(isEnterInFiat) }
onAmountValueChange(amount)
saveResult()
}.launchIn(modelScope)

View file

@ -60,6 +60,7 @@ internal class SendDestinationInitialStateTransformer(
networkName = cryptoCurrency.network.name,
isValidating = false,
isRedesignEnabled = isRedesignEnabled,
isRecentHidden = false,
)
}
}

View file

@ -2,9 +2,9 @@ package com.tangem.features.send.v2.subcomponents.destination.model.transformers
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientWalletListConverter
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
import com.tangem.utils.transformer.Transformer

View file

@ -54,6 +54,7 @@ internal class SendDestinationValidationResultTransformer(
wallets = state.wallets.map { wallet ->
wallet.copy(isVisible = !isValidAddress && (wallet.isLoading || wallet.title != TextReference.EMPTY))
}.toPersistentList(),
isRecentHidden = isValidAddress,
)
}

View file

@ -211,6 +211,7 @@ private class DestinationBlockPreviewProvider : PreviewParameterProvider<Destina
isValidating = false,
isInitialized = true,
isRedesignEnabled = false,
isRecentHidden = false,
)
override val values: Sequence<DestinationUM.Content>

View file

@ -25,12 +25,12 @@ import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationClickIntents
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationRecipientListUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationClickIntents
import kotlinx.collections.immutable.ImmutableList
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
@ -74,13 +74,14 @@ internal fun SendDestinationContent(
)
listHeaderItem(
titleRes = R.string.send_recipient_wallets_title,
isVisible = wallets.isNotEmpty() && wallets.first().isVisible,
isVisible = wallets.isNotEmpty() && wallets.first().isVisible && !state.isRecentHidden,
isFirst = true,
)
listItem(
list = wallets,
isLast = recipients.any { !it.isVisible },
isBalanceHidden = isBalanceHidden,
isRecentHidden = state.isRecentHidden,
onClick = { title ->
clickIntents.onRecipientAddressValueChange(
title,
@ -90,13 +91,14 @@ internal fun SendDestinationContent(
)
listHeaderItem(
titleRes = R.string.send_recent_transactions,
isVisible = recipients.isNotEmpty() && recipients.first().isVisible,
isVisible = recipients.isNotEmpty() && recipients.first().isVisible && !state.isRecentHidden,
isFirst = wallets.any { !it.isVisible },
)
listItem(
list = recipients,
isLast = true,
isBalanceHidden = isBalanceHidden,
isRecentHidden = state.isRecentHidden,
onClick = { title ->
clickIntents.onRecipientAddressValueChange(
title,
@ -257,6 +259,7 @@ private fun LazyListScope.listItem(
list: ImmutableList<DestinationRecipientListUM>,
isLast: Boolean,
isBalanceHidden: Boolean,
isRecentHidden: Boolean,
onClick: (String) -> Unit,
) {
items(
@ -267,7 +270,7 @@ private fun LazyListScope.listItem(
val item = list[index]
val title = item.title.resolveReference()
if (item.isVisible) {
if (item.isVisible && !isRecentHidden) {
ListItemWithIcon(
title = title,
subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(),

View file

@ -16,6 +16,6 @@ interface SendWithSwapComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, SendWithSwapComponent>
interface ModelCallback {
fun onCloseSwap(lastAmount: String)
fun onCloseSwap(lastAmount: String, isEnterInFiatSelected: Boolean)
}
}

View file

@ -5,7 +5,7 @@ package com.tangem.features.swap.v2.api.subcomponents
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SwapAmountUpdateTrigger {
suspend fun triggerUpdateAmount(amountValue: String)
suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean)
suspend fun triggerQuoteReload()
}

View file

@ -14,7 +14,7 @@ import javax.inject.Singleton
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SwapAmountUpdateListener {
val updateAmountTriggerFlow: Flow<String>
val updateAmountTriggerFlow: Flow<Pair<String, Boolean>>
val reloadQuotesTriggerFlow: Flow<Unit>
}
@ -44,8 +44,8 @@ internal class DefaultSwapAmountUpdateTrigger @Inject constructor() :
SwapAmountReduceTrigger,
SwapAmountReduceListener {
override val updateAmountTriggerFlow: SharedFlow<String>
field = MutableSharedFlow<String>()
override val updateAmountTriggerFlow: SharedFlow<Pair<String, Boolean>>
field = MutableSharedFlow<Pair<String, Boolean>>()
override val reduceToTriggerFlow: SharedFlow<BigDecimal>
field = MutableSharedFlow<BigDecimal>()
@ -59,8 +59,8 @@ internal class DefaultSwapAmountUpdateTrigger @Inject constructor() :
override val reloadQuotesTriggerFlow: Flow<Unit>
field = MutableSharedFlow<Unit>()
override suspend fun triggerUpdateAmount(amountValue: String) {
updateAmountTriggerFlow.emit(amountValue)
override suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean) {
updateAmountTriggerFlow.emit(amountValue to isEnterInFiatSelected)
}
override suspend fun triggerQuoteReload() {

View file

@ -46,7 +46,7 @@ internal class SwapAmountComponent @AssistedInject constructor(
interface ModelCallback : NavigationModelCallback {
fun onAmountResult(amountUM: SwapAmountUM)
fun onSeparatorClick(lastAmount: String)
fun onSeparatorClick(lastAmount: String, isEnterInFiatSelected: Boolean)
fun resetSendWithSwapNavigation(resetNavigation: Boolean)
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -32,7 +33,7 @@ internal sealed class SwapAmountComponentParams {
override val filterProviderTypes: List<ExpressProviderType> = emptyList(),
val title: TextReference,
val callback: SwapAmountComponent.ModelCallback,
val currentRoute: Flow<SwapAmountRoute>,
val currentRoute: Flow<SendWithSwapRoute>,
) : SwapAmountComponentParams()
data class AmountBlockParams(

View file

@ -53,6 +53,7 @@ import com.tangem.features.swap.v2.impl.amount.model.transformers.*
import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent
import com.tangem.features.swap.v2.impl.common.SwapAlertFactory
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.Debouncer
import com.tangem.utils.coroutines.PeriodicTask
@ -347,7 +348,10 @@ internal class SwapAmountModel @Inject constructor(
private fun confirmSendWithSwapClose() {
val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return
val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data
val amountField = uiState.value.swapDirection.withSwapDirection(
onDirect = { uiState.value.primaryAmount },
onReverse = { uiState.value.secondaryAmount },
).amountField as? AmountState.Data
val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
if (primaryCryptoCurrencyStatus != null) {
@ -366,7 +370,16 @@ internal class SwapAmountModel @Inject constructor(
)
}
amountParams.callback.onSeparatorClick(lastAmount = amountFieldData?.amountTextField?.value.orEmpty())
val isEnterInFiatSelected = amountField?.amountTextField?.isFiatValue == true
val lastAmount = if (isEnterInFiatSelected) {
amountField.amountTextField.fiatValue
} else {
amountField?.amountTextField?.value
}
amountParams.callback.onSeparatorClick(
lastAmount = lastAmount.orEmpty(),
isEnterInFiatSelected = isEnterInFiatSelected,
)
}
private fun subscribeOnCryptoCurrencyStatusFlow() {
@ -406,9 +419,10 @@ internal class SwapAmountModel @Inject constructor(
private fun subscribeOnAmountUpdateTriggerUpdates() {
swapAmountUpdateListener.updateAmountTriggerFlow
.onEach {
.onEach { (amount, isEnterInFiat) ->
if (uiState.value is SwapAmountUM.Content) {
onAmountValueChange(it)
onCurrencyChangeClick(isEnterInFiat)
onAmountValueChange(amount)
saveResult()
}
}
@ -689,7 +703,7 @@ internal class SwapAmountModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, route) ->
).filter { (_, route) -> route is SendWithSwapRoute.Amount }.onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.common_amount),

View file

@ -7,7 +7,7 @@ import com.tangem.utils.transformer.Transformer
internal object SwapAmountPasteTransformer : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
return prevState.updateAmount(
val updatedState = prevState.updateAmount(
onPrimaryAmount = {
copy(
amountField = AmountPastedTriggerDismissTransformer.transform(
@ -23,5 +23,9 @@ internal object SwapAmountPasteTransformer : Transformer<SwapAmountUM> {
)
},
)
return (updatedState as? SwapAmountUM.Content)?.copy(
isPrimaryButtonEnabled = prevState.isPrimaryButtonEnabled,
) ?: updatedState
}
}

View file

@ -31,6 +31,7 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountComponent
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent
import com.tangem.features.swap.v2.impl.sendviaswap.model.SendWithSwapModel
import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent
@ -136,7 +137,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
params = SwapAmountComponentParams.AmountParams(
amountUM = model.uiState.value.amountUM,
title = resourceReference(R.string.common_send),
currentRoute = model.currentRoute.filterIsInstance<SendWithSwapRoute.Amount>(),
currentRoute = model.currentRoute,
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = model.analyticCategoryName,
primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow,
@ -202,11 +203,15 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
private fun onChildBack() {
val isEmptyStack = childStack.value.backStack.isEmpty()
val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success
val isSendingInProgress = (model.uiState.value.confirmUM as? ConfirmUM.Content)?.isTransactionInProcess == true
if (isEmptyStack) {
router.pop()
} else {
stackNavigation.pop()
val isPopSend = isEmptyStack || isSuccess
when {
isSendingInProgress -> Unit // Do not anything while transaction sending in progress
isPopSend -> router.pop()
else -> stackNavigation.pop()
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -51,6 +52,8 @@ import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmInitialStateTransformer
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmSendingStateTransformer
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmSentStateTransformer
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmationNotificationsTransformer
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
import com.tangem.lib.crypto.BlockchainFeeUtils.patchTransactionFeeForSwap
@ -141,6 +144,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
configConfirmNavigation()
initialState()
subscribeOnNotificationUpdates()
subscribeOnTapHelpUpdates()
}
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
@ -239,10 +243,12 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private fun onSendClick() {
val provider = confirmData.quote?.provider ?: return
modelScope.launch {
uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(true))
swapTransactionSender.sendTransaction(
confirmData = confirmData,
isAmountSubtractAvailable = isAmountSubtractAvailable,
onExpressError = { expressError ->
uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false))
swapAlertFactory.getGenericErrorState(
expressError = expressError,
onFailedTxEmailClick = {
@ -259,6 +265,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
)
},
onSendError = { error ->
uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false))
swapAlertFactory.getSendTransactionErrorState(
error = error,
onFailedTxEmailClick = {
@ -280,19 +287,17 @@ internal class SendWithSwapConfirmModel @Inject constructor(
networkId = primaryCurrencyStatus.currency.network.id,
).getOrNull().orEmpty()
sendSuccessAnalytics()
uiState.update {
it.copy(
confirmUM = ConfirmUM.Success(
isPrimaryButtonEnabled = true,
transactionDate = timestamp,
txUrl = txUrl,
provider = provider,
swapDataModel = data,
),
)
}
uiState.transformerUpdate(
SendWithSwapConfirmSentStateTransformer(
timestamp = timestamp,
txUrl = txUrl,
provider = provider,
swapDataModel = data,
),
)
router.replaceAll(SendWithSwapRoute.Success)
},
expressOperationType = ExpressOperationType.SEND_WITH_SWAP,
)
}
}
@ -311,7 +316,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
val confirmUM = uiState.value.confirmUM
modelScope.launch {
val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false }
val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false }
if (confirmUM is ConfirmUM.Empty) {
uiState.update {
it.copy(
@ -325,6 +330,16 @@ internal class SendWithSwapConfirmModel @Inject constructor(
}
}
private fun subscribeOnTapHelpUpdates() {
isSendTapHelpEnabledUseCase().getOrNull()
?.onEach { showTapHelp ->
uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM)
}
}?.launchIn(modelScope)
}
private fun updateConfirmNotifications() {
modelScope.launch {
sendNotificationsUpdateTrigger.triggerUpdate(
@ -415,7 +430,15 @@ internal class SendWithSwapConfirmModel @Inject constructor(
backIconRes = R.drawable.ic_back_24,
backIconClick = router::pop,
primaryButton = NavigationButton(
textReference = resourceReference(R.string.common_send),
textReference = when (confirmUM) {
is ConfirmUM.Success -> resourceReference(R.string.common_close)
is ConfirmUM.Content -> if (confirmUM.isTransactionInProcess) {
resourceReference(R.string.send_sending)
} else {
resourceReference(R.string.common_send)
}
else -> resourceReference(R.string.common_send)
},
iconRes = R.drawable.ic_tangem_24,
isIconVisible = isReadyToSend,
isHapticClick = isReadyToSend,

View file

@ -3,6 +3,7 @@ package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model
import arrow.core.getOrElse
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -40,6 +41,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
onSendSuccess: (String, Long, SwapDataModel) -> Unit,
onExpressError: (ExpressError) -> Unit,
onSendError: (SendTransactionError?) -> Unit,
expressOperationType: ExpressOperationType,
) {
val provider = confirmData.quote?.provider ?: return
@ -50,6 +52,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
onExpressError = onExpressError,
onSendSuccess = onSendSuccess,
onSendError = onSendError,
expressOperationType = expressOperationType,
)
ExpressProviderType.DEX,
ExpressProviderType.DEX_BRIDGE,
@ -67,6 +70,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
onSendSuccess: (String, Long, SwapDataModel) -> Unit,
onExpressError: (ExpressError) -> Unit,
onSendError: (SendTransactionError?) -> Unit,
expressOperationType: ExpressOperationType,
) {
val fromStatus = confirmData.fromCryptoCurrencyStatus ?: return
val toStatus = confirmData.toCryptoCurrencyStatus ?: return
@ -92,6 +96,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
toAddress = destination,
expressProvider = provider,
rateType = rateType,
expressOperationType,
).getOrElse { onExpressError(it); return }
createAndSendCexTransaction(

View file

@ -0,0 +1,19 @@
package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
import com.tangem.utils.transformer.Transformer
internal class SendWithSwapConfirmSendingStateTransformer(
private val isTransactionInProcess: Boolean,
) : Transformer<SendWithSwapUM> {
override fun transform(prevState: SendWithSwapUM): SendWithSwapUM {
val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState
return prevState.copy(
confirmUM = confirmUM.copy(
isPrimaryButtonEnabled = !isTransactionInProcess,
isTransactionInProcess = isTransactionInProcess,
),
)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.swap.models.SwapDataModel
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
import com.tangem.utils.transformer.Transformer
internal class SendWithSwapConfirmSentStateTransformer(
private val txUrl: String,
private val timestamp: Long,
private val provider: ExpressProvider,
private val swapDataModel: SwapDataModel,
) : Transformer<SendWithSwapUM> {
override fun transform(prevState: SendWithSwapUM): SendWithSwapUM {
return prevState.copy(
confirmUM = ConfirmUM.Success(
isPrimaryButtonEnabled = true,
transactionDate = timestamp,
txUrl = txUrl,
provider = provider,
swapDataModel = swapDataModel,
),
)
}
}

View file

@ -110,8 +110,8 @@ internal class SendWithSwapModel @Inject constructor(
uiState.update { it.copy(navigationUM = navigationUM) }
}
override fun onSeparatorClick(lastAmount: String) {
params.callback?.onCloseSwap(lastAmount)
override fun onSeparatorClick(lastAmount: String, isEnterInFiatSelected: Boolean) {
params.callback?.onCloseSwap(lastAmount, isEnterInFiatSelected)
router.popTo(initialRoute)
}

View file

@ -301,6 +301,7 @@ private fun SendWithSwapSuccessContent_Preview() {
isValidating = false,
isInitialized = false,
isRedesignEnabled = false,
isRecentHidden = false,
),
feeSelectorUM = FeeSelectorUM.Content(
fees = TransactionFee.Single(

View file

@ -42,6 +42,7 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.express.models)
/** Data */
implementation(projects.data.common)

View file

@ -27,6 +27,7 @@ import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -283,6 +284,7 @@ internal class DefaultSwapRepository(
providerId: String,
rateType: RateType,
toAddress: String,
expressOperationType: ExpressOperationType,
refundAddress: String?, // for cex only
refundExtraId: String?, // for cex only
): Either<ExpressDataError, SwapDataModel> {
@ -304,6 +306,7 @@ internal class DefaultSwapRepository(
requestId = requestId,
refundAddress = refundAddress,
refundExtraId = refundExtraId,
partnerOperationType = expressOperationType.value,
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,

View file

@ -12,6 +12,7 @@ dependencies {
implementation(projects.features.swap.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.express.models)
implementation(deps.arrow.core)

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.domain.api
import arrow.core.Either
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -64,6 +65,7 @@ interface SwapRepository {
providerId: String,
rateType: RateType,
toAddress: String,
expressOperationType: ExpressOperationType,
refundAddress: String? = null, // for cex only
refundExtraId: String? = null, // for cex only
): Either<ExpressDataError, SwapDataModel>

View file

@ -35,6 +35,7 @@ dependencies {
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.express.models)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
@ -65,6 +66,7 @@ interface SwapInteractor {
amountToSwap: String,
includeFeeInAmount: IncludeFeeInAmount,
fee: TxFee,
expressOperationType: ExpressOperationType,
): SwapTransactionState
// suspend fun updateQuotesStateWithSelectedFee(

View file

@ -18,6 +18,7 @@ import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
@ -287,6 +288,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
selectedFee = selectedFee,
amount = amount,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
expressOperationType = ExpressOperationType.SWAP,
)
}
ExchangeProviderType.CEX -> {
@ -313,6 +315,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
selectedFee: FeeType,
amount: SwapAmount,
isBalanceWithoutFeeEnough: Boolean,
expressOperationType: ExpressOperationType,
): Pair<SwapProvider, SwapState> {
val maybeQuotes = repository.findBestQuote(
userWallet = userWallet,
@ -349,6 +352,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toToken = toToken,
amount = amount,
selectedFee = selectedFee,
expressOperationType = expressOperationType,
)
} else {
provider to getQuotesState(
@ -495,6 +499,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
amountToSwap: String,
includeFeeInAmount: IncludeFeeInAmount,
fee: TxFee,
expressOperationType: ExpressOperationType,
): SwapTransactionState {
Timber.i(
"""
@ -529,6 +534,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
amount = amountToSwapWithFee,
txFee = fee,
swapProvider = swapProvider,
expressOperationType = expressOperationType,
)
}
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
@ -676,6 +682,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
amount: SwapAmount,
txFee: TxFee,
swapProvider: SwapProvider,
expressOperationType: ExpressOperationType,
): SwapTransactionState {
val exchangeData = repository.getExchangeData(
userWallet = userWallet,
@ -689,9 +696,10 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toDecimals = currencyToGet.currency.decimals,
providerId = swapProvider.providerId,
rateType = RateType.FLOAT,
expressOperationType = expressOperationType,
toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(),
refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value,
refundExtraId = null, // currently always null
refundExtraId = null, // currently always null,
).getOrElse { return SwapTransactionState.Error.ExpressError(it) }
val exchangeDataCex =
@ -1161,6 +1169,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toToken: CryptoCurrencyStatus,
amount: SwapAmount,
selectedFee: FeeType,
expressOperationType: ExpressOperationType,
): SwapState {
return repository.getExchangeData(
userWallet = userWallet,
@ -1176,6 +1185,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
rateType = RateType.FLOAT,
toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(),
refundAddress = fromToken.value.networkAddress?.defaultAddress?.value,
expressOperationType = expressOperationType,
).fold(
ifRight = { swapData ->
val transaction = swapData.transaction as ExpressTransactionModel.DEX

View file

@ -43,6 +43,7 @@ dependencies {
implementation(projects.domain.promo.models)
implementation(projects.domain.txhistory)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.express.models)
/** Feature modules */
implementation(projects.features.swap.domain)

View file

@ -23,6 +23,7 @@ import com.tangem.core.ui.utils.InputNumberFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
@ -612,6 +613,7 @@ internal class SwapModel @Inject constructor(
amountToSwap = requireNotNull(dataState.amount),
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
fee = fee,
expressOperationType = ExpressOperationType.SWAP,
)
}.onSuccess {
when (it) {

View file

@ -78,10 +78,11 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor(
appComponentContext = appComponentContext,
params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams(
cryptoCurrency = model.params.config.cryptoCurrency,
address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"),
address = model.state.value.addresses.find { it.value == config.address } ?: error(
"Address has to be there",
),
callback = model,
onDismiss = ::dismiss,
id = config.addressId,
),
)
TokenReceiveRoutes.ReceiveAssets -> TokenReceiveAssetsComponent(

View file

@ -13,7 +13,6 @@ import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel
import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
internal class TokenReceiveAssetsComponent(
appComponentContext: AppComponentContext,
@ -29,13 +28,13 @@ internal class TokenReceiveAssetsComponent(
}
internal interface TokenReceiveAssetsModelCallback {
fun onQrCodeClick(id: Int)
fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource)
fun onQrCodeClick(address: String)
fun onCopyClick(address: ReceiveAddress, source: TokenReceiveCopyActionSource)
}
data class TokenReceiveAssetsParams(
val notificationConfigs: ImmutableList<NotificationUM>,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val addresses: ImmutableList<ReceiveAddress>,
val callback: TokenReceiveAssetsModelCallback,
val onDismiss: () -> Unit,
val showMemoDisclaimer: Boolean,

View file

@ -27,12 +27,11 @@ internal class TokenReceiveQrCodeComponent(
}
internal interface TokenReceiveQrCodeModelCallback {
fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource)
fun onCopyClick(address: ReceiveAddress, source: TokenReceiveCopyActionSource)
fun onShareClick(address: String)
}
data class TokenReceiveQrCodeParams(
val id: Int,
val cryptoCurrency: CryptoCurrency,
val address: ReceiveAddress,
val callback: TokenReceiveQrCodeModelCallback,

View file

@ -0,0 +1,156 @@
package com.tangem.features.tokenreceive.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveNotification
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.ens.EnsAddress
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Ens
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Primary
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
internal class TokenReceiveStateFactory(
private val currentStateProvider: Provider<TokenReceiveUM>,
private val cryptoCurrency: CryptoCurrency,
private val addresses: List<ReceiveAddressModel>,
private val tokenReceiveNotification: List<TokenReceiveNotification>,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
fun getInitialState(tokenName: String): TokenReceiveUM {
return TokenReceiveUM(
addresses = mapAddresses(
addresses = addresses,
cryptoCurrency = cryptoCurrency,
),
iconState = iconStateConverter.convert(cryptoCurrency),
network = cryptoCurrency.network.name,
isEnsResultLoading = false,
notificationConfigs = getNotifications(
tokenName = tokenName,
tokenReceiveNotification = tokenReceiveNotification,
).toImmutableList(),
)
}
fun getLoadingState(): TokenReceiveUM {
return currentStateProvider.invoke().copy(isEnsResultLoading = true)
}
fun getLoadedState(ensAddress: List<EnsAddress>): TokenReceiveUM {
val currentAddressValues = currentStateProvider.invoke().addresses.map { it.value }.toSet()
val newEnsAddresses = ensAddress
.filterIsInstance<EnsAddress.Address>()
.filterNot { it.name in currentAddressValues }
.map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = Ens) }
val combinedAddresses = (currentStateProvider.invoke().addresses + newEnsAddresses)
.sortedWith(
compareBy { address ->
when (address.type) {
is Ens -> 0
is Primary.Default -> 1
is Primary.Legacy -> 2
}
},
)
return currentStateProvider.invoke().copy(
isEnsResultLoading = false,
addresses = combinedAddresses.toPersistentList(),
)
}
private fun mapAddresses(
addresses: List<ReceiveAddressModel>,
cryptoCurrency: CryptoCurrency,
): ImmutableList<ReceiveAddress> {
val needUseToLegacyAndDefaultName = addresses.any { it.nameService == ReceiveAddressModel.NameService.Legacy }
val receiveAddresses = addresses.map { model ->
val type = when (model.nameService) {
ReceiveAddressModel.NameService.Default -> {
val displayName = when (cryptoCurrency) {
is CryptoCurrency.Coin -> cryptoCurrency.name
is CryptoCurrency.Token -> cryptoCurrency.symbol
}
Primary.Default(
displayName = if (needUseToLegacyAndDefaultName) {
TextReference.Res(R.string.domain_receive_assets_default_address)
} else {
TextReference.Combined(
wrappedList(
TextReference.Str(displayName),
TextReference.Str(" "),
TextReference.Res(R.string.common_address),
),
)
},
)
}
ReceiveAddressModel.NameService.Ens -> Ens
ReceiveAddressModel.NameService.Legacy -> Primary.Legacy(
displayName = TextReference.Res(R.string.domain_receive_assets_legacy_address),
)
}
ReceiveAddress(
value = model.value,
type = type,
)
}
val sortedAddresses = receiveAddresses.sortedWith(
compareBy { address ->
when (address.type) {
is Ens -> 0
is Primary.Default -> 1
is Primary.Legacy -> 2
}
},
)
return sortedAddresses.toPersistentList()
}
private fun getNotifications(
tokenName: String,
tokenReceiveNotification: List<TokenReceiveNotification>,
): List<NotificationUM> {
return buildList {
add(
NotificationUM.Info(
title = resourceReference(
R.string.receive_bottom_sheet_warning_title,
wrappedList(
tokenName,
cryptoCurrency.network.name,
),
),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconTint = NotificationConfig.IconTint.Accent,
),
)
tokenReceiveNotification.map { notification ->
add(
NotificationUM.Warning(
title = resourceReference(notification.title),
subtitle = resourceReference(notification.subtitle),
),
)
}
}
}
}

View file

@ -41,7 +41,7 @@ internal class TokenReceiveAssetsModel @Inject constructor(
ReceiveAssetsUM(
onCopyClick = {
params.callback.onCopyClick(
id = it,
address = it,
source = TokenReceiveCopyActionSource.Receive,
)
},
@ -54,7 +54,7 @@ internal class TokenReceiveAssetsModel @Inject constructor(
)
private fun configureEnsStatus(): AnalyticsParam.EnsStatus {
val hasEnsAddress = params.addresses.values.any { it.type == ReceiveAddress.Type.Ens }
val hasEnsAddress = params.addresses.any { it.type == ReceiveAddress.Type.Ens }
return if (hasEnsAddress) {
AnalyticsParam.EnsStatus.FULL
} else {

View file

@ -2,22 +2,13 @@ package com.tangem.features.tokenreceive.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.push
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.ui.R
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.Asset
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ens.EnsAddress
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
@ -28,12 +19,11 @@ import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Ens
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Primary
import com.tangem.features.tokenreceive.entity.TokenReceiveStateFactory
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@ -52,14 +42,21 @@ internal class TokenReceiveModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), TokenReceiveModelCallback {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val tokenReceiveStateFactory: TokenReceiveStateFactory by lazy {
TokenReceiveStateFactory(
cryptoCurrency = params.config.cryptoCurrency,
addresses = params.config.receiveAddress,
tokenReceiveNotification = params.config.tokenReceiveNotification,
currentStateProvider = Provider { state.value },
)
}
val params = paramsContainer.require<TokenReceiveComponent.Params>()
val stackNavigation = StackNavigation<TokenReceiveRoutes>()
internal val state: StateFlow<TokenReceiveUM>
field = MutableStateFlow<TokenReceiveUM>(getInitState())
field = MutableStateFlow<TokenReceiveUM>(tokenReceiveStateFactory.getInitialState(getTokenName()))
init {
modelScope.launch {
@ -67,20 +64,19 @@ internal class TokenReceiveModel @Inject constructor(
}
}
override fun onQrCodeClick(id: Int) {
override fun onQrCodeClick(address: String) {
analyticsEventHandler.send(
TokenReceiveNewAnalyticsEvent.QrScreenOpened(
token = getTokenName(),
blockchainName = params.config.cryptoCurrency.network.name,
),
)
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id))
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(address = address))
}
override fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource) {
val addressToCopy = state.value.addresses[id] ?: return
sendCopyActionAnalytic(addressToCopy, source)
clipboardManager.setText(text = addressToCopy.value, isSensitive = true)
override fun onCopyClick(address: ReceiveAddress, source: TokenReceiveCopyActionSource) {
sendCopyActionAnalytic(address, source)
clipboardManager.setText(text = address.value, isSensitive = true)
}
override fun onShareClick(address: String) {
@ -106,51 +102,8 @@ internal class TokenReceiveModel @Inject constructor(
}
}
private fun mapAddresses(
addresses: List<ReceiveAddressModel>,
networkName: String,
): ImmutableMap<Int, ReceiveAddress> {
val needUseToLegacyAndDefaultName = addresses.any { it.nameService == ReceiveAddressModel.NameService.Legacy }
return buildMap {
addresses.mapIndexed { index, model ->
val type = when (model.nameService) {
ReceiveAddressModel.NameService.Default -> {
Primary.Default(
displayName = if (needUseToLegacyAndDefaultName) {
TextReference.Res(R.string.domain_receive_assets_default_address)
} else {
TextReference.Combined(
wrappedList(
TextReference.Str(networkName),
TextReference.Str(" "),
TextReference.Res(R.string.common_address),
),
)
},
)
}
ReceiveAddressModel.NameService.Ens -> Ens
ReceiveAddressModel.NameService.Legacy -> Primary.Legacy(
displayName = TextReference.Res(R.string.domain_receive_assets_legacy_address),
)
}
put(
key = index,
value = ReceiveAddress(
value = model.value,
type = type,
),
)
}
}.toPersistentMap()
}
private suspend fun load() = withContext(dispatchers.default) {
state.value = state.value.copy(
isEnsResultLoading = true,
)
state.value = tokenReceiveStateFactory.getLoadingState()
val reverseResolveResult =
if (params.config.cryptoCurrency.network.nameResolvingType == Network.NameResolvingType.ENS) {
getReverseResolvedEnsAddressUseCase(
@ -161,74 +114,7 @@ internal class TokenReceiveModel @Inject constructor(
} else {
emptyList()
}
val currentAddressValues = state.value.addresses.values.map { it.value }.toSet()
val newEnsAddresses = reverseResolveResult
.filterIsInstance<EnsAddress.Address>()
.filterNot { it.name in currentAddressValues }
.map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = Ens) }
val combinedAddresses = (state.value.addresses.values + newEnsAddresses)
.sortedWith(
compareBy { address ->
when (address.type) {
is Ens -> 0
is Primary.Default -> 1
is Primary.Legacy -> 2
}
},
)
val updatedAddresses = combinedAddresses
.mapIndexed { index, address -> index to address }
.toMap()
.toPersistentMap()
state.value = state.value.copy(
isEnsResultLoading = false,
addresses = updatedAddresses,
)
}
private fun getNotifications(): List<NotificationUM> {
return buildList {
add(
NotificationUM.Info(
title = resourceReference(
R.string.receive_bottom_sheet_warning_title,
wrappedList(
getTokenName(),
params.config.cryptoCurrency.network.name,
),
),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconTint = NotificationConfig.IconTint.Accent,
),
)
params.config.tokenReceiveNotification.map { notification ->
add(
NotificationUM.Warning(
title = resourceReference(notification.title),
subtitle = resourceReference(notification.subtitle),
),
)
}
}
}
private fun getInitState(): TokenReceiveUM {
return TokenReceiveUM(
addresses = mapAddresses(
addresses = params.config.receiveAddress,
networkName = params.config.cryptoCurrency.network.name,
),
iconState = iconStateConverter.convert(params.config.cryptoCurrency),
network = params.config.cryptoCurrency.network.name,
isEnsResultLoading = false,
notificationConfigs = getNotifications().toImmutableList(),
)
state.value = tokenReceiveStateFactory.getLoadedState(reverseResolveResult)
}
private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress, source: TokenReceiveCopyActionSource) {

View file

@ -30,7 +30,7 @@ internal class TokenReceiveQrCodeModel @Inject constructor(
addressName = TextReference.Str("${params.cryptoCurrency.name} (${params.cryptoCurrency.symbol})"),
onCopyClick = {
params.callback.onCopyClick(
id = params.id,
address = params.address,
source = TokenReceiveCopyActionSource.QR,
)
},

View file

@ -15,5 +15,5 @@ internal sealed interface TokenReceiveRoutes : Route {
data object ReceiveAssets : TokenReceiveRoutes
@Serializable
data class QrCode(val addressId: Int) : TokenReceiveRoutes
data class QrCode(val address: String) : TokenReceiveRoutes
}

View file

@ -29,8 +29,8 @@ import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.buttons.small.TangemIconButton
@ -45,9 +45,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch
@Composable
@ -121,9 +119,9 @@ private fun Info(
@Composable
private fun AddressBlock(
onOpenQrCodeClick: (id: Int) -> Unit,
onCopyClick: (id: Int) -> Unit,
addresses: ImmutableMap<Int, ReceiveAddress>,
onOpenQrCodeClick: (address: String) -> Unit,
onCopyClick: (address: ReceiveAddress) -> Unit,
addresses: ImmutableList<ReceiveAddress>,
snackbarHostState: SnackbarHostState,
) {
val hapticFeedback = LocalHapticFeedback.current
@ -131,13 +129,13 @@ private fun AddressBlock(
val context = LocalContext.current
val resources = context.resources
addresses.entries.toList().fastForEach { entry ->
when (val type = entry.value.type) {
addresses.fastForEach { address ->
when (val type = address.type) {
is ReceiveAddress.Type.Primary -> {
key(entry.key) {
key(address.value) {
AddressItem(
onCopyClick = {
onCopyClick(entry.key)
onCopyClick(address)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
@ -149,19 +147,19 @@ private fun AddressBlock(
},
onOpenQrCodeClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onOpenQrCodeClick(entry.key)
onOpenQrCodeClick(address.value)
},
address = entry.value.value,
address = address.value,
primaryType = type,
)
SpacerH8()
}
}
ReceiveAddress.Type.Ens -> {
key(entry.key) {
key(address.value) {
EnsItem(
onCopyClick = {
onCopyClick(entry.key)
onCopyClick(address)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
@ -171,7 +169,7 @@ private fun AddressBlock(
)
}
},
address = entry.value.value,
address = address.value,
)
SpacerH8()
}
@ -213,9 +211,9 @@ private fun AddressItem(
Column(modifier = Modifier.weight(1f)) {
EllipsisText(
text = primaryType.displayName.resolveReference(),
ellipsis = TextEllipsis.Middle,
ellipsis = TextEllipsis.End,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
style = TangemTheme.typography.subtitle2,
)
EllipsisText(
@ -229,16 +227,16 @@ private fun AddressItem(
SpacerW12()
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
modifier = Modifier.size(TangemTheme.dimens.size32),
innerPadding = 6.dp,
iconRes = R.drawable.ic_qrcode_new_24,
onClick = onOpenQrCodeClick,
)
SpacerW8()
SpacerW(12.dp)
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
modifier = Modifier.size(TangemTheme.dimens.size32),
iconRes = R.drawable.ic_copy_new_24,
innerPadding = 6.dp,
onClick = onCopyClick,
@ -334,9 +332,9 @@ private class TokenReceiveAssetsContentProvider : PreviewParameterProvider<Recei
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
),
),
addresses = persistentMapOf(
0 to address,
1 to address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"),
addresses = persistentListOf(
address,
address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"),
),
showMemoDisclaimer = false,
onCopyClick = {},

View file

@ -3,7 +3,9 @@ package com.tangem.features.tokenreceive.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -73,7 +75,10 @@ private fun QrCodePage(addressFullName: TextReference, addressValue: String, net
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.size36)) {
Column(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.size36),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(
R.string.receive_bottom_sheet_warning_message_compact,
@ -87,12 +92,23 @@ private fun QrCodePage(addressFullName: TextReference, addressValue: String, net
SpacerH(20.dp)
Image(
painter = qrCodePainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.size(248.dp),
)
Box(
modifier = Modifier
.border(
width = 8.dp,
color = TangemTheme.colors.icon.constant,
shape = RoundedCornerShape(8.dp),
)
.padding(8.dp),
) {
Image(
painter = qrCodePainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.sizeIn(minWidth = 148.dp),
)
}
SpacerH24()
}

View file

@ -3,13 +3,12 @@ package com.tangem.features.tokenreceive.ui.state
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
internal data class ReceiveAssetsUM(
val showMemoDisclaimer: Boolean,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val onOpenQrCodeClick: (id: Int) -> Unit,
val onCopyClick: (id: Int) -> Unit,
val addresses: ImmutableList<ReceiveAddress>,
val onOpenQrCodeClick: (address: String) -> Unit,
val onCopyClick: (address: ReceiveAddress) -> Unit,
val isEnsResultLoading: Boolean,
val notificationConfigs: ImmutableList<NotificationUM>,
)

View file

@ -4,12 +4,11 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
internal data class TokenReceiveUM(
val network: String,
val iconState: CurrencyIconState,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val addresses: ImmutableList<ReceiveAddress>,
val isEnsResultLoading: Boolean,
val notificationConfigs: ImmutableList<NotificationUM>,
)

View file

@ -60,6 +60,7 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.DetailsScreenOpened.TokenBalance
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.transaction.error.IncompleteTransactionError
@ -86,6 +87,7 @@ import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.extensions.isZero
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.async
@ -156,6 +158,7 @@ internal class TokenDetailsModel @Inject constructor(
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var isBalanceLoadedEventSent = false
private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
/** Transaction id to check for status */
@ -199,9 +202,6 @@ internal class TokenDetailsModel @Inject constructor(
val uiState: StateFlow<TokenDetailsState> = internalUiState
init {
analyticsEventsHandler.send(
event = TokenScreenAnalyticsEvent.DetailsScreenOpened(token = cryptoCurrency.symbol),
)
updateTopBarMenu()
initButtons()
updateContent()
@ -317,6 +317,7 @@ internal class TokenDetailsModel @Inject constructor(
.onEach { maybeCurrencyStatus ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
maybeCurrencyStatus.onRight { status ->
sendOneTimeBalanceLoadedAnalyticsEvent(status)
cryptoCurrencyStatus = status
updateButtons(currencyStatus = status)
updateWarnings(status)
@ -1078,6 +1079,44 @@ internal class TokenDetailsModel @Inject constructor(
)
}
private fun sendOneTimeBalanceLoadedAnalyticsEvent(cryptoCurrencyStatus: CryptoCurrencyStatus?) {
if (isBalanceLoadedEventSent || cryptoCurrencyStatus == null) return
val tokenBalance = when (val value = cryptoCurrencyStatus.value) {
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.NoQuote,
-> {
if (value.sources.networkSource.isActual()) {
val amount = value.amount
if (amount != null && !amount.isZero()) {
TokenBalance.Full
} else {
TokenBalance.Empty
}
} else {
return
}
}
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.MissedDerivation,
-> TokenBalance.NoAddress
is CryptoCurrencyStatus.NoAmount,
is CryptoCurrencyStatus.Unreachable,
-> TokenBalance.Error
CryptoCurrencyStatus.Loading -> return
}
analyticsEventsHandler.send(
event = TokenScreenAnalyticsEvent.DetailsScreenOpened(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
tokenBalance = tokenBalance,
),
)
isBalanceLoadedEventSent = true
}
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}

View file

@ -132,6 +132,7 @@ internal class WcAddNetworkModel @Inject constructor(
rawRequest = useCase.rawSdkRequest,
network = useCase.network,
emulationStatus = null,
securityStatus = null,
),
)

View file

@ -408,6 +408,7 @@ internal class WcSendTransactionModel @Inject constructor(
rawRequest = useCase.rawSdkRequest,
network = useCase.network,
emulationStatus = emulationStatus,
securityStatus = useCase.session.securityStatus,
),
)

View file

@ -158,6 +158,7 @@ internal class WcSignTransactionModel @Inject constructor(
rawRequest = useCase.rawSdkRequest,
network = useCase.network,
emulationStatus = null,
securityStatus = null,
),
)