Updated on 2026-08-14
This commit is contained in:
parent
35dbee8691
commit
3a57ac60c2
13 changed files with 177 additions and 61 deletions
|
|
@ -2,77 +2,156 @@ package com.tangem.tap.common.analytics.converters
|
|||
|
||||
import com.tangem.common.Converter
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletData
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalCryptoAmount
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class BasicSignInEventConverter(
|
||||
private val scanResponse: ScanResponse,
|
||||
) : Converter<WalletState, Basic.SignedIn?> {
|
||||
class BasicEventsPreChecker {
|
||||
|
||||
override fun convert(value: WalletState): Basic.SignedIn? {
|
||||
if (!statesIsReadyToCreateEvent(scanResponse, value)) return null
|
||||
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
|
||||
fun tryToSend(converterData: BasicEventsSourceData) {
|
||||
if (!isReadyToSend(converterData)) return
|
||||
|
||||
BasicSignInEventConverter().convert(converterData)?.let { Analytics.send(it) }
|
||||
BasicTopUpEventConverter().convert(converterData)?.let { Analytics.send(it) }
|
||||
}
|
||||
|
||||
private fun isReadyToSend(data: BasicEventsSourceData): Boolean {
|
||||
val (scanResponse, walletState, biometricsWalletDataModels) = data
|
||||
if (walletState.derivationsCheckIsScheduled) {
|
||||
Timber.d("FAILED: derivationsCheckIsScheduled")
|
||||
return false
|
||||
}
|
||||
if (scanResponse.card.isMultiwalletAllowed && walletState.missingDerivations.isNotEmpty()) {
|
||||
Timber.d("FAILED: isMultiwalletAllowed || missingDerivations.isNotEmpty")
|
||||
return false
|
||||
}
|
||||
|
||||
if (biometricsWalletDataModels == null) {
|
||||
Timber.d("SWITCH: OLD")
|
||||
val walletsDataFromStores = data.walletState.walletsDataFromStores
|
||||
if (walletsDataFromStores.isEmpty()) {
|
||||
Timber.d("FAILED: walletsDataFromStores.isEmpty")
|
||||
return false
|
||||
}
|
||||
|
||||
val totalBalanceState = data.walletState.totalBalance?.state
|
||||
if (totalBalanceState == null || totalBalanceState == ProgressState.Loading ||
|
||||
totalBalanceState == ProgressState.Refreshing
|
||||
) {
|
||||
Timber.d("FAILED: totalBalanceState: ${totalBalanceState?.name}")
|
||||
return false
|
||||
}
|
||||
|
||||
val balancesCount = walletsDataFromStores
|
||||
.map { if (it.currencyData.amount == null) 0 else 1 }
|
||||
.reduce { acc, i -> acc + i }
|
||||
|
||||
if (balancesCount != walletsDataFromStores.size) {
|
||||
Timber.d("FAILED: balancesCount != walletsDataFromStores.size")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
Timber.d("SWITCH: BIOMETRICS")
|
||||
if (biometricsWalletDataModels.isEmpty()) {
|
||||
Timber.d("FAILED: biometricsWalletDataModels.isEmpty")
|
||||
return false
|
||||
}
|
||||
|
||||
if (biometricsWalletDataModels.any {
|
||||
it.status is WalletDataModel.Loading ||
|
||||
it.status is WalletDataModel.NoAccount ||
|
||||
it.status is WalletDataModel.Unreachable ||
|
||||
it.status is WalletDataModel.MissedDerivation ||
|
||||
it.status.isErrorStatus
|
||||
}) {
|
||||
Timber.d("FAILED: by status")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("SUCCESS")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* With biometrics enabled, we should check its storage instead of "WalletState.walletsDataFromStores"
|
||||
* because the latter is updated after some time.
|
||||
* @property biometricsWalletDataModels - wallet data models from the 'WalletStoresManager'. If null, then
|
||||
* the "WalletState.walletsDataFromStores" will be used to determine appropriate state
|
||||
*/
|
||||
data class BasicEventsSourceData(
|
||||
val scanResponse: ScanResponse,
|
||||
val walletState: WalletState,
|
||||
val biometricsWalletDataModels: List<WalletDataModel>?,
|
||||
) {
|
||||
val batchId: String by lazy { scanResponse.card.batchId }
|
||||
|
||||
val userWalletIdStringValue: String? by lazy {
|
||||
UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
?.stringValue
|
||||
}
|
||||
|
||||
val paramCardCurrency: AnalyticsParam.CardCurrency? by lazy { ParamCardCurrencyConverter().convert(scanResponse) }
|
||||
|
||||
val paramCardBalanceState: AnalyticsParam.CardBalanceState by lazy { calculateAmount().toCardBalanceState() }
|
||||
|
||||
private fun calculateAmount(): BigDecimal {
|
||||
return biometricsWalletDataModels?.calculateTotalCryptoAmount()
|
||||
?: walletState.walletsDataFromStores.calculateTotalCryptoAmount()
|
||||
}
|
||||
|
||||
private fun BigDecimal.toCardBalanceState(): AnalyticsParam.CardBalanceState = when {
|
||||
isZero() -> AnalyticsParam.CardBalanceState.Empty
|
||||
else -> AnalyticsParam.CardBalanceState.Full
|
||||
}
|
||||
|
||||
private fun List<WalletDataModel>.calculateTotalCryptoAmount(): BigDecimal {
|
||||
return this
|
||||
.map { it.status.amount }
|
||||
.reduce(BigDecimal::plus)
|
||||
}
|
||||
}
|
||||
|
||||
class BasicSignInEventConverter : Converter<BasicEventsSourceData, Basic.SignedIn?> {
|
||||
|
||||
override fun convert(value: BasicEventsSourceData): Basic.SignedIn? {
|
||||
if (value.paramCardCurrency == null || value.userWalletIdStringValue == null) return null
|
||||
|
||||
return Basic.SignedIn(
|
||||
state = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
|
||||
currency = cardCurrency,
|
||||
batch = scanResponse.card.batchId,
|
||||
state = value.paramCardBalanceState,
|
||||
currency = value.paramCardCurrency!!,
|
||||
batch = value.batchId,
|
||||
).apply {
|
||||
filterData = UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
?.stringValue
|
||||
filterData = value.userWalletIdStringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BasicTopUpEventConverter(
|
||||
private val scanResponse: ScanResponse,
|
||||
) : Converter<WalletState, Basic.ToppedUp?> {
|
||||
class BasicTopUpEventConverter : Converter<BasicEventsSourceData, Basic.ToppedUp?> {
|
||||
|
||||
override fun convert(value: WalletState): Basic.ToppedUp? {
|
||||
if (!statesIsReadyToCreateEvent(scanResponse, value)) return null
|
||||
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
|
||||
override fun convert(value: BasicEventsSourceData): Basic.ToppedUp? {
|
||||
if (value.paramCardCurrency == null || value.userWalletIdStringValue == null) return null
|
||||
|
||||
val data = BasicTopUpFilter.Data(
|
||||
walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()?.stringValue ?: "",
|
||||
cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
|
||||
walletId = value.userWalletIdStringValue!!,
|
||||
cardBalanceState = value.paramCardBalanceState,
|
||||
)
|
||||
|
||||
return Basic.ToppedUp(cardCurrency).apply { filterData = data }
|
||||
return Basic.ToppedUp(value.paramCardCurrency!!).apply { filterData = data }
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnalyticsParam.CardBalanceState.Companion.from(walletsData: List<WalletData>): AnalyticsParam.CardBalanceState {
|
||||
val totalCryptoAmount = walletsData.calculateTotalCryptoAmount()
|
||||
return when {
|
||||
totalCryptoAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty
|
||||
else -> AnalyticsParam.CardBalanceState.Full
|
||||
}
|
||||
}
|
||||
|
||||
private fun statesIsReadyToCreateEvent(scanResponse: ScanResponse, state: WalletState): Boolean {
|
||||
if (scanResponse.card.isMultiwalletAllowed && state.missingDerivations.isNotEmpty()) return false
|
||||
if (state.walletsDataFromStores.isEmpty()) return false
|
||||
|
||||
val totalBalanceState = state.totalBalance?.state ?: return false
|
||||
if (totalBalanceState == ProgressState.Loading || totalBalanceState == ProgressState.Refreshing) return false
|
||||
|
||||
val balancesCount = state.walletsDataFromStores.map {
|
||||
if (it.currencyData.amount == null) 0 else 1
|
||||
}.reduce { acc, i -> acc + i }
|
||||
|
||||
if (balancesCount != state.walletsDataFromStores.size) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ class ParamCardCurrencyConverter : Converter<ScanResponse, AnalyticsParam.CardCu
|
|||
}
|
||||
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
|
||||
value.isSaltPay() -> AnalyticsParam.CurrencyType.Token(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay))
|
||||
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
|
||||
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
|
||||
else -> null
|
||||
} ?: return null
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
|||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.middlewares.handleBasicAnalyticsEvent
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
|
|
@ -117,6 +118,7 @@ class TapWalletManager {
|
|||
.doOnSuccess {
|
||||
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
|
||||
store.dispatchOnMain(WalletAction.LoadData.Success)
|
||||
handleBasicAnalyticsEvent()
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
val errorAction = when (error) {
|
||||
|
|
@ -190,6 +192,7 @@ class TapWalletManager {
|
|||
}
|
||||
|
||||
if (data.card.isMultiwalletAllowed) {
|
||||
dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
|
||||
loadMultiWalletData(data)
|
||||
} else {
|
||||
loadSingleWalletData(data)
|
||||
|
|
@ -212,9 +215,7 @@ class TapWalletManager {
|
|||
.filter {
|
||||
it.derivationPath != null && !scanResponse.hasDerivation(it.blockchain, it.derivationPath)
|
||||
}
|
||||
if (missingDerivations.isNotEmpty()) {
|
||||
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
|
||||
}
|
||||
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
|
||||
}
|
||||
|
||||
private suspend fun loadSingleWalletData(data: ScanResponse) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
interface WalletStoresManager {
|
||||
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>>
|
||||
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
|
||||
suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel>
|
||||
|
||||
suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ internal class DefaultWalletStoresManager(
|
|||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
override suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
|
||||
return walletStoresRepository.getSync(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
return walletStoresRepository.delete(userWalletsIds)
|
||||
.flatMap { walletManagersRepository.delete(userWalletsIds) }
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ internal class DummyWalletStoresManager : WalletStoresManager {
|
|||
return emptyFlow()
|
||||
}
|
||||
|
||||
override suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ sealed class WalletAction : Action {
|
|||
data class SetPrimaryToken(val token: Token) : MultiWallet()
|
||||
data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet()
|
||||
object BackupWallet : MultiWallet()
|
||||
object ScheduleCheckForMissingDerivation : MultiWallet()
|
||||
data class AddMissingDerivations(val blockchains: List<BlockchainNetwork>) : MultiWallet()
|
||||
object ScanToGetDerivations : MultiWallet()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ import org.rekotlin.StateType
|
|||
import java.math.BigDecimal
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
|
||||
/**
|
||||
* @property derivationsCheckIsScheduled - used only for analytics
|
||||
*/
|
||||
data class WalletState(
|
||||
val cardId: String = "",
|
||||
val state: ProgressState = ProgressState.Done,
|
||||
|
|
@ -46,6 +49,7 @@ data class WalletState(
|
|||
val totalBalance: TotalBalance? = null,
|
||||
val showBackupWarning: Boolean = false,
|
||||
val missingDerivations: List<BlockchainNetwork> = emptyList(),
|
||||
val derivationsCheckIsScheduled: Boolean = false,
|
||||
val loadingUserTokens: Boolean = false,
|
||||
val walletCardsCount: Int? = null,
|
||||
) : StateType {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,9 @@ class MultiWalletMiddleware {
|
|||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.AddMissingDerivations -> {
|
||||
scope.launch { handleBasicAnalyticsEvent() }
|
||||
}
|
||||
is WalletAction.MultiWallet.ScanToGetDerivations -> {
|
||||
val selectedWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedWallet != null) {
|
||||
|
|
@ -182,6 +185,7 @@ class MultiWalletMiddleware {
|
|||
selectedUserWallet: UserWallet,
|
||||
state: WalletState?,
|
||||
) = scope.launch(Dispatchers.Default) {
|
||||
dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
|
||||
ScanCardProcessor.scan(
|
||||
analyticsEvent = null,
|
||||
cardId = selectedUserWallet.cardId,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import com.tangem.core.analytics.Analytics
|
|||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.operations.attestation.Attestation
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker
|
||||
import com.tangem.tap.common.analytics.converters.BasicEventsSourceData
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
|
|
@ -34,6 +36,7 @@ import com.tangem.tap.domain.failedRates
|
|||
import com.tangem.tap.domain.loadedRates
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
|
|
@ -55,6 +58,7 @@ import com.tangem.tap.tangemSdkManager
|
|||
import com.tangem.tap.totalFiatBalanceCalculator
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.tap.userWalletsListManagerSafe
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -108,6 +112,7 @@ class WalletMiddleware {
|
|||
walletState.walletManagers.map { walletManager ->
|
||||
async { globalState.tapWalletManager.loadWalletData(walletManager) }
|
||||
}.awaitAll()
|
||||
handleBasicAnalyticsEvent()
|
||||
} else {
|
||||
val walletManager = walletState.getWalletManager(action.blockchain)
|
||||
?: action.walletManager
|
||||
|
|
@ -295,6 +300,7 @@ class WalletMiddleware {
|
|||
}
|
||||
is WalletAction.UserWalletChanged -> Unit
|
||||
is WalletAction.WalletStoresChanged -> {
|
||||
store.dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
|
||||
updateWalletStores(action.walletStores, walletState)
|
||||
fetchTotalFiatBalance(action.walletStores)
|
||||
findMissedDerivations(action.walletStores)
|
||||
|
|
@ -489,4 +495,19 @@ class WalletMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleBasicAnalyticsEvent() {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
|
||||
val biometricsWalletDataModels = if (preferencesStorage.shouldSaveUserWallets) {
|
||||
UserWalletIdBuilder.scanResponse(scanResponse).build()?.let { userWalletId ->
|
||||
walletStoresManager.getSync(userWalletId).map { it.walletsData }.flatten()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val converterData = BasicEventsSourceData(scanResponse, store.state.walletState, biometricsWalletDataModels)
|
||||
BasicEventsPreChecker().tryToSend(converterData)
|
||||
}
|
||||
|
|
@ -185,7 +185,13 @@ class MultiWalletReducer {
|
|||
is WalletAction.MultiWallet.SetPrimaryToken -> state.copy(primaryToken = action.token)
|
||||
is WalletAction.MultiWallet.SaveCurrencies -> state
|
||||
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(showBackupWarning = action.show)
|
||||
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(missingDerivations = action.blockchains)
|
||||
is WalletAction.MultiWallet.ScheduleCheckForMissingDerivation -> state.copy(
|
||||
derivationsCheckIsScheduled = true,
|
||||
)
|
||||
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(
|
||||
missingDerivations = action.blockchains,
|
||||
derivationsCheckIsScheduled = false
|
||||
)
|
||||
is WalletAction.MultiWallet.BackupWallet -> state
|
||||
is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(state = ProgressState.Loading)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ import androidx.transition.TransitionInflater
|
|||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import coil.load
|
||||
import coil.size.Scale
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.fragments.setStatusBarColor
|
||||
import com.tangem.core.ui.utils.OneTouchClickListener
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.converters.BasicSignInEventConverter
|
||||
import com.tangem.tap.common.analytics.converters.BasicTopUpEventConverter
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.analytics.events.Portfolio
|
||||
import com.tangem.tap.common.extensions.show
|
||||
|
|
@ -148,7 +146,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
override fun newState(state: WalletState) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
handleBasicAnalyticsEvent(state)
|
||||
val isSaltPay = store.state.globalState.scanResponse?.card?.isSaltPay == true
|
||||
|
||||
when {
|
||||
|
|
@ -258,11 +255,4 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.menu_wallet, menu)
|
||||
}
|
||||
|
||||
private fun handleBasicAnalyticsEvent(state: WalletState) {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
|
||||
BasicSignInEventConverter(scanResponse).convert(state)?.let { Analytics.send(it) }
|
||||
BasicTopUpEventConverter(scanResponse).convert(state)?.let { Analytics.send(it) }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue