Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-25 01:33:28 +03:00
parent 35dbee8691
commit 3a57ac60c2
13 changed files with 177 additions and 61 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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) {

View file

@ -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>

View file

@ -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) }

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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 {

View file

@ -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,

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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) }
}
}