Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-20 15:27:00 +03:00
parent 2a1369fce6
commit 5a99de87fb
29 changed files with 194 additions and 47 deletions

View file

@ -14,9 +14,15 @@ interface SendComponent : ComposableContentComponent {
val amount: String? = null,
val tag: String? = null,
val destinationAddress: String? = null,
val entryType: EntryType = EntryType.Manual,
val callback: ModelCallback? = null,
)
enum class EntryType {
Manual,
QR,
}
interface Factory : ComponentFactory<Params, SendComponent>
interface ModelCallback {

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
/**
@ -32,11 +33,13 @@ sealed class CommonSendAnalyticEvents(
data class AmountScreenOpened(
val categoryName: String,
val source: CommonSendSource,
val type: SendEntryType = SendEntryType.Manual,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Amount Screen Opened",
params = mapOf(
SOURCE to source.analyticsName,
TYPE to type.analyticsName,
),
), AppsFlyerIncludedEvent
@ -95,6 +98,7 @@ sealed class CommonSendAnalyticEvents(
val toDerivationIndex: Int?,
val sendBlockchain: String,
val sendToken: String,
val type: SendEntryType = SendEntryType.Manual,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Confirm Screen Opened",
@ -108,6 +112,7 @@ sealed class CommonSendAnalyticEvents(
"$fromDerivationIndex, $toDerivationIndex",
)
}
put(TYPE, type.analyticsName)
},
), AppsFlyerIncludedEvent
@ -221,6 +226,11 @@ sealed class CommonSendAnalyticEvents(
Confirm,
}
enum class SendEntryType(val analyticsName: String) {
QR("QR"),
Manual("Manually"),
}
enum class CommonSendSource(val analyticsName: String) {
Send("Send"),
Swap("Swap"),

View file

@ -3,6 +3,8 @@ package com.tangem.features.send.v2.networkselection.model
import androidx.compose.runtime.Stable
import com.tangem.common.getTotalCryptoAmount
import com.tangem.common.getTotalFiatAmount
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -44,6 +46,7 @@ import javax.inject.Inject
internal class NetworkSelectionModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@ -51,6 +54,10 @@ internal class NetworkSelectionModel @Inject constructor(
private val params: NetworkSelectionComponent.Params = paramsContainer.require()
init {
analyticsEventHandler.send(SendAnalyticEvents.ChooseTokenScreenOpened())
}
private val searchQuery = MutableStateFlow("")
private val expandedWallets = MutableStateFlow(
params.walletGroups.firstOrNull()
@ -227,7 +234,15 @@ internal class NetworkSelectionModel @Inject constructor(
text = cryptoAmount.format { crypto(currency) },
isFlickering = isFlickering,
),
onItemClick = { params.onTokenSelected(context.userWalletId, currency) },
onItemClick = {
analyticsEventHandler.send(
SendAnalyticEvents.TokenSelected(
token = currency.symbol,
blockchain = currency.network.name,
),
)
params.onTokenSelected(context.userWalletId, currency)
},
onItemLongClick = null,
)
}

View file

@ -105,6 +105,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
sendToken = fromCurrency.symbol,
fromDerivationIndex = fromDerivationIndex,
toDerivationIndex = null,
type = model.consumeEntryType(),
),
)
if (model.currentRoute.value.isEditMode) {
@ -116,6 +117,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
CommonSendAnalyticEvents.AmountScreenOpened(
categoryName = model.analyticCategoryName,
source = model.analyticsSendSource,
type = model.consumeEntryType(),
),
)
activeComponent.updateState(model.uiState.value.amountUM)

View file

@ -50,6 +50,21 @@ internal sealed class SendAnalyticEvents(
},
), AppsFlyerIncludedEvent
class ChooseTokenScreenOpened : SendAnalyticEvents(
event = "Choose Token Screen Opened",
)
class TokenSelected(
val token: String,
val blockchain: String,
) : SendAnalyticEvents(
event = "Token Selected",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
)
data class ConvertTokenButtonClicked(
val token: String,
val blockchain: String,

View file

@ -105,6 +105,8 @@ internal class SendModel @Inject constructor(
private val params: SendComponent.Params = paramsContainer.require()
private val cryptoCurrency = params.currency
private var isEntryTypeConsumed = false
val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY
val analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Send
@ -264,6 +266,15 @@ internal class SendModel @Inject constructor(
showAlertError()
}
fun consumeEntryType(): CommonSendAnalyticEvents.SendEntryType {
if (isEntryTypeConsumed) return CommonSendAnalyticEvents.SendEntryType.Manual
isEntryTypeConsumed = true
return when (params.entryType) {
SendComponent.EntryType.QR -> CommonSendAnalyticEvents.SendEntryType.QR
SendComponent.EntryType.Manual -> CommonSendAnalyticEvents.SendEntryType.Manual
}
}
private suspend fun prepareTransferTransaction(): Either<Throwable, TransactionData> {
val predefinedValues = predefinedValues
val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value

View file

@ -202,6 +202,7 @@ internal class WalletComponent @AssistedInject constructor(
address = dialogConfig.address,
amount = dialogConfig.amount?.parseBigDecimal(currency.decimals),
tag = dialogConfig.memo,
entryType = AppRoute.Send.EntryType.Manual,
)
},
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.analytics.models.AnalyticsParam
@ -767,6 +768,7 @@ internal class WalletModel @Inject constructor(
userWalletId = stateHolder.getSelectedWalletId(),
uri = target.uri,
source = source,
screen = WcPairRequest.Screen.MAIN,
),
)
}
@ -777,6 +779,7 @@ internal class WalletModel @Inject constructor(
address = target.address,
amount = target.amount?.parseBigDecimal(target.currency.decimals),
tag = target.memo,
entryType = AppRoute.Send.EntryType.QR,
)
}
is QrSendTarget.Multiple -> {
@ -789,9 +792,17 @@ internal class WalletModel @Inject constructor(
private fun handleQrError(error: ClassifiedQrContent.Error) {
when (error) {
is ClassifiedQrContent.Error.Unrecognized -> {
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.NoticeUnrecognizedQr(),
)
uiMessageSender.send(WalletAlertUM.qrCodeUnrecognized())
}
is ClassifiedQrContent.Error.UnsupportedNetwork -> {
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.NoticeNoAvailableTokens(
blockchain = error.blockchain,
),
)
uiMessageSender.send(WalletAlertUM.qrCodeUnsupportedNetwork())
}
}

View file

@ -382,6 +382,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
override fun onScanQrClick() {
analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonQrScan())
router.openQrScanner()
}
}

View file

@ -186,6 +186,7 @@ internal class DefaultWalletRouter @Inject constructor(
address: String,
amount: String?,
tag: String?,
entryType: AppRoute.Send.EntryType,
) {
router.push(
AppRoute.Send(
@ -194,6 +195,7 @@ internal class DefaultWalletRouter @Inject constructor(
destinationAddress = address,
amount = amount,
tag = tag,
entryType = entryType,
),
)
}

View file

@ -95,7 +95,14 @@ internal interface InnerWalletRouter {
fun openQrScanner()
/** Open send screen with prefilled destination */
fun openSend(userWalletId: UserWalletId, currency: CryptoCurrency, address: String, amount: String?, tag: String?)
fun openSend(
userWalletId: UserWalletId,
currency: CryptoCurrency,
address: String,
amount: String?,
tag: String?,
entryType: AppRoute.Send.EntryType = AppRoute.Send.EntryType.Manual,
)
/** Open network selection bottom sheet for multiple QR matches */
fun openNetworkSelectionBottomSheet(target: QrSendTarget.Multiple)

View file

@ -167,6 +167,17 @@ sealed class WalletScreenAnalyticsEvent {
class NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined")
class NoticeUnrecognizedQr : MainScreen(
event = "Notice - Unrecognized QR",
)
class NoticeNoAvailableTokens(blockchain: String?) : MainScreen(
event = "Notice - No Available Tokens",
params = buildMap {
if (blockchain != null) put("Blockchain", blockchain)
},
)
// region Referral Promo
class ReferralPromo : MainScreen(event = "Referral Banner")
class ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate")

View file

@ -15,7 +15,6 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.walletconnect.connections.model.WcPairModel
@ -127,5 +126,7 @@ internal class WcPairComponent(
}
}
data class Params(val userWalletId: UserWalletId, val wcUrl: String, val source: WcPairRequest.Source)
data class Params(
val request: WcPairRequest,
)
}

View file

@ -86,6 +86,7 @@ internal class WcConnectionsModel @Inject constructor(
userWalletId = params.userWalletId,
uri = result.qrCode,
source = source,
screen = WcPairRequest.Screen.WALLET_CONNECT,
),
)
}

View file

@ -29,7 +29,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcPairError.Unknown
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
@ -75,13 +74,7 @@ internal class WcPairModel @Inject constructor(
) : Model(), WcPairComponentCallback {
private val params: WcPairComponent.Params = paramsContainer.require()
private val wcPairUseCase = wcPairUseCaseFactory.create(
WcPairRequest(
userWalletId = params.userWalletId,
uri = params.wcUrl,
source = params.source,
),
)
private val wcPairUseCase = wcPairUseCaseFactory.create(params.request)
val stackNavigation = StackNavigation<WcAppInfoRoutes>()
val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
@ -109,7 +102,7 @@ internal class WcPairModel @Inject constructor(
modelScope.launch {
val portfolioBalance = portfolioFetcher.data.first().balances
.firstNotNullOfOrNull { (walletId, balance) ->
if (params.userWalletId == walletId) balance else null
if (params.request.userWalletId == walletId) balance else null
}
if (portfolioBalance == null) {
router.pop()

View file

@ -89,9 +89,7 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor(
appComponentContext = childContext,
portfolioSelectorComponentFactory = portfolioSelectorComponentFactory,
params = WcPairComponent.Params(
userWalletId = config.request.userWalletId,
wcUrl = config.request.uri,
source = config.request.source,
request = config.request,
),
)
is WcInnerRoute.UnsupportedMethodAlert -> AlertsComponent(