Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-11 16:24:00 +07:00
parent 9e38389156
commit 7493e56a52
22 changed files with 216 additions and 74 deletions

View file

@ -18,6 +18,7 @@ import com.google.mlkit.vision.common.InputImage
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
import com.tangem.feature.qrscanning.model.QrScanningModel
import com.tangem.feature.qrscanning.presentation.QrScanningContent
@ -41,10 +42,10 @@ class DefaultQrScanningComponent @AssistedInject constructor(
// Camera requires its own analyzer instance due to flow of frames needed to be analyzed.
// Each new frame can cancel previous analysis e.i. image from the gallery can be skipped.
private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
MLKitBarcodeAnalyzer(model::onQrScanned)
MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.CAMERA) }
}
private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
MLKitBarcodeAnalyzer(model::onQrScanned)
MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.GALLERY) }
}
init {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.qrscanning.model
import com.tangem.domain.qrscanning.models.QrResultSource
import kotlinx.coroutines.flow.SharedFlow
internal interface QrScanningClickIntents {
@ -8,7 +9,7 @@ internal interface QrScanningClickIntents {
fun onBackClick()
fun onQrScanned(qrCode: String)
fun onQrScanned(qrCode: String, source: QrResultSource)
fun onGalleryClicked()

View file

@ -10,6 +10,8 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.RawQrResult
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.qrscanning.presentation.QrScanningState
@ -71,10 +73,11 @@ internal class QrScanningModel @Inject constructor(
override fun onBackClick() = appRouter.pop()
override fun onQrScanned(qrCode: String) {
override fun onQrScanned(qrCode: String, source: QrResultSource) {
if (qrCode.isNotBlank()) {
modelScope.launch(dispatchers.mainImmediate) {
emitQrScannedEventUseCase.invoke(params.source, qrCode)
val qrCode = RawQrResult(qrCode, source, params.source)
emitQrScannedEventUseCase.invoke(qrCode)
}
if (!isScanned) {
appRouter.pop()

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
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.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.impl.R
import com.tangem.feature.qrscanning.model.QrScanningClickIntents
@ -28,7 +29,7 @@ internal class InitializeQrScanningStateTransformer(
topBarConfig = constructTopBarConfig(),
message = message,
onBackClick = clickIntents::onBackClick,
onQrScanned = clickIntents::onQrScanned,
onQrScanned = { qrCode -> clickIntents.onQrScanned(qrCode, QrResultSource.CAMERA) },
onGalleryClick = clickIntents::onGalleryClicked,
pasteAction = constructPasteAction(),
)
@ -50,7 +51,7 @@ internal class InitializeQrScanningStateTransformer(
private fun constructPasteAction(): PasteAction {
val uri = clipboardManager.getText()
return if (uri != null) {
PasteAction.Perform { clickIntents.onQrScanned(uri) }
PasteAction.Perform { clickIntents.onQrScanned(uri, QrResultSource.CLIPBOARD) }
} else {
PasteAction.None
}

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.common.routing)
implementation(projects.common.ui)
implementation(projects.core.analytics)
/** Domain models */
implementation(projects.domain.appCurrency.models)

View file

@ -4,6 +4,7 @@ import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute
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
@ -19,6 +20,7 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.WcSession
@ -47,6 +49,7 @@ internal class WcConnectionsModel @Inject constructor(
private val wcDisconnectUseCase: WcDisconnectUseCase,
private val wcPairService: WcPairService,
override val dispatchers: CoroutineDispatcherProvider,
private val analytics: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model() {
@ -56,6 +59,7 @@ internal class WcConnectionsModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<WcConnectionsBottomSheetConfig> = SlotNavigation()
init {
analytics.send(WcAnalyticEvents.ScreenOpened)
listenQrUpdates()
listenWcSessions()
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.walletconnect.connections.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.stack.*
import com.domain.blockaid.models.dapp.CheckDAppResult
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
@ -11,6 +12,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.WcSessionApprove
@ -42,10 +44,12 @@ private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class WcPairModel @Inject constructor(
private val router: Router,
private val messageSender: UiMessageSender,
override val dispatchers: CoroutineDispatcherProvider,
private val analytics: AnalyticsEventHandler,
wcPairUseCaseFactory: WcPairUseCase.Factory,
getWalletsUseCase: GetWalletsUseCase,
paramsContainer: ParamsContainer,
@ -165,10 +169,22 @@ internal class WcPairModel @Inject constructor(
}
private fun showUnknownDomainAlert() {
val event = WcAnalyticEvents.NoticeSecurityAlert(
dAppMetaData = sessionProposal.dAppMetaData,
securityStatus = sessionProposal.securityStatus,
source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain,
)
analytics.send(event)
stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnknownDomain))
}
private fun showSecurityRiskAlert() {
val event = WcAnalyticEvents.NoticeSecurityAlert(
dAppMetaData = sessionProposal.dAppMetaData,
securityStatus = sessionProposal.securityStatus,
source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain,
)
analytics.send(event)
stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnsafeDomain))
}

View file

@ -8,6 +8,7 @@ import com.arkivanov.decompose.router.stack.pushNew
import com.domain.blockaid.models.transaction.ValidationResult
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
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
@ -21,6 +22,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcRequestError.Companion.message
@ -58,6 +60,7 @@ internal class WcSendTransactionModel @Inject constructor(
private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter,
private val getFeeUseCase: GetFeeUseCase,
private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase,
private val analytics: AnalyticsEventHandler,
) : Model(), WcCommonTransactionModel, FeeSelectorModelCallback {
private val params = paramsContainer.require<WcTransactionModelParams>()
@ -219,6 +222,19 @@ internal class WcSendTransactionModel @Inject constructor(
} else {
sign()
}
securityCheck?.result?.validation?.let { securityStatus ->
val event = WcAnalyticEvents.NoticeSecurityAlert(
dAppMetaData = useCase.session.sdkModel.appMetaData,
securityStatus = useCase.session.securityStatus,
source = WcAnalyticEvents.NoticeSecurityAlert.Source.SmartContract,
)
when (securityStatus) {
ValidationResult.SAFE -> Unit
ValidationResult.UNSAFE,
ValidationResult.FAILED_TO_VALIDATE,
-> analytics.send(event)
}
}
}
fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) {