Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-27 13:54:20 +03:00
parent d43e4fb40e
commit db1299b6af
17 changed files with 386 additions and 272 deletions

View file

@ -22,7 +22,7 @@ import com.tangem.domain.common.LogConfig
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.common.analytics.filters.TopUpFilter
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
@ -158,6 +158,14 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder.mainStore = store
appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager
initTopUpController()
}
private fun initTopUpController() {
val topUpController = store.state.globalState.topUpController
topUpController.walletStoresManagerProvider = { walletStoresManager }
topUpController.scanResponseProvider = { store.state.globalState.scanResponse }
walletCurrenciesManager.addListener(topUpController)
}
override fun newImageLoader(): ImageLoader {
@ -186,7 +194,7 @@ class TapApplication : Application(), ImageLoaderFactory {
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addFilter(BasicTopUpFilter(preferencesStorage.toppedUpWalletStorage))
factory.addFilter(TopUpFilter(preferencesStorage.toppedUpWalletStorage))
val buildData = AnalyticsHandlerBuilder.Data(
application = application,

View file

@ -1,174 +0,0 @@
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.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.WalletState
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalCryptoAmount
import timber.log.Timber
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class BasicEventsPreChecker {
fun tryToSend(converterData: BasicEventsSourceData) {
if (!isReadyToSend(converterData)) return
BasicTopUpEventConverter().convert(converterData)?.let { Analytics.send(it) }
}
@Suppress("ComplexMethod")
private fun isReadyToSend(data: BasicEventsSourceData): Boolean {
val (scanResponse, walletState, biometricsWalletDataModels) = data
if (walletState.derivationsCheckIsScheduled) {
Timber.d("FAILED: derivationsCheckIsScheduled")
return false
}
if (scanResponse.cardTypesResolver.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
}
val isCorrectStatus = 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
}
if (isCorrectStatus) {
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 userWalletIdStringValue: String? by lazy {
UserWalletIdBuilder.scanResponse(scanResponse).build()?.stringValue
}
val paramCardCurrency: AnalyticsParam.CardCurrency? by lazy {
ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
}
val paramCardBalanceState: AnalyticsParam.CardBalanceState by lazy { calculateAmount().toCardBalanceState() }
private fun calculateAmount(): BigDecimal {
val calculator: IBalanceCalculator = biometricsWalletDataModels
?.let { BiometricsBalanceCalculator(it) }
?: BalanceCalculator(walletState)
return calculator.calculate()
}
private fun BigDecimal.toCardBalanceState(): AnalyticsParam.CardBalanceState = when {
isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
}
private interface IBalanceCalculator {
fun calculate(): BigDecimal
}
private class BiometricsBalanceCalculator(
private val walletDataModel: List<WalletDataModel>,
) : IBalanceCalculator {
override fun calculate(): BigDecimal {
val singleToken = walletDataModel
.filter { it.currency.isToken() }
.firstOrNull { it.isCardSingleToken }
val totalAmount = singleToken?.status?.amount
?: walletDataModel.calculateTotalCryptoAmount()
return totalAmount
}
private fun List<WalletDataModel>.calculateTotalCryptoAmount(): BigDecimal = this
.map { it.status.amount }
.reduce(BigDecimal::plus)
}
private class BalanceCalculator(
private val walletState: WalletState,
) : IBalanceCalculator {
override fun calculate(): BigDecimal {
val singleTokenData = walletState.primaryTokenData
val totalAmount = singleTokenData?.currencyData?.amount
?: walletState.walletsDataFromStores.calculateTotalCryptoAmount()
return totalAmount
}
}
class BasicTopUpEventConverter : Converter<BasicEventsSourceData, Basic.ToppedUp?> {
override fun convert(value: BasicEventsSourceData): Basic.ToppedUp? {
if (value.paramCardCurrency == null || value.userWalletIdStringValue == null) return null
val data = BasicTopUpFilter.Data(
walletId = value.userWalletIdStringValue!!,
cardBalanceState = value.paramCardBalanceState,
)
return Basic.ToppedUp(value.paramCardCurrency!!).apply { filterData = data }
}
}

View file

@ -0,0 +1,67 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.common.Converter
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.CardTypesResolver
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.filters.TopUpFilter
import com.tangem.tap.domain.model.WalletDataModel
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class TopUpEventConverter : Converter<TopUpEventConverter.Data, Basic.ToppedUp?> {
override fun convert(value: Data): Basic.ToppedUp? {
val (cardTypesResolver, walletDataModels, userWalletIdValue, isToppedUpInPast) = value
val paramCardCurrency = ParamCardCurrencyConverter().convert(cardTypesResolver) ?: return null
val cardBalanceState = BalanceCalculator(walletDataModels).calculate().toCardBalanceState()
val filterData = TopUpFilter.Data(
walletId = userWalletIdValue,
cardBalanceState = cardBalanceState,
isToppedUpInPast = isToppedUpInPast,
)
return Basic.ToppedUp(paramCardCurrency).apply {
this.filterData = filterData
}
}
private fun BigDecimal.toCardBalanceState(): AnalyticsParam.CardBalanceState = when {
isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
data class Data(
val cardTypesResolver: CardTypesResolver,
val walletDataModels: List<WalletDataModel>,
val userWalletIdValue: String,
val isToppedUpInPast: Boolean,
)
}
private interface IBalanceCalculator {
fun calculate(): BigDecimal
}
private class BalanceCalculator(
private val walletDataModels: List<WalletDataModel>,
) : IBalanceCalculator {
override fun calculate(): BigDecimal {
val singleToken = walletDataModels
.filter { it.currency.isToken() }
.firstOrNull { it.isCardSingleToken }
val totalAmount = singleToken?.status?.amount
?: walletDataModels.calculateTotalCryptoAmount()
return totalAmount
}
private fun List<WalletDataModel>.calculateTotalCryptoAmount(): BigDecimal = this
.map { it.status.amount }
.reduce(BigDecimal::plus)
}

View file

@ -1,50 +0,0 @@
package com.tangem.tap.common.analytics.filters
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.api.AnalyticsEventFilter
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.persistence.ToppedUpWalletStorage
/**
[REDACTED_AUTHOR]
*/
class BasicTopUpFilter(
private val topupWalletStorage: ToppedUpWalletStorage,
) : AnalyticsEventFilter {
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Basic.ToppedUp
override fun canBeSent(event: AnalyticsEvent): Boolean {
val data = event.filterData as? Data ?: return false
val walletInfo = topupWalletStorage.restore(data.walletId).guard {
val newWalletInfo = Data(
walletId = data.walletId,
cardBalanceState = data.cardBalanceState,
)
topupWalletStorage.save(newWalletInfo)
return false
}
if (walletInfo.isToppedUp) return false
return if (!walletInfo.isToppedUp && data.isToppedUp) {
topupWalletStorage.save(walletInfo.copy(cardBalanceState = AnalyticsParam.CardBalanceState.Full))
true
} else {
false
}
}
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean = true
data class Data(
val walletId: String,
val cardBalanceState: AnalyticsParam.CardBalanceState,
) {
val isToppedUp: Boolean = cardBalanceState == AnalyticsParam.CardBalanceState.Full
}
}

View file

@ -0,0 +1,72 @@
package com.tangem.tap.common.analytics.filters
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.AnalyticsEventFilter
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.topup.log
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.persistence.ToppedUpWalletStorage
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
class TopUpFilter(
private val topupWalletStorage: ToppedUpWalletStorage,
) : AnalyticsEventFilter {
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Basic.ToppedUp
override fun canBeSent(event: AnalyticsEvent): Boolean {
val data = event.filterData as? Data ?: return false
if (data.isToppedUpInPast) {
log("BasicTopUpFilter: [false]: sending is blocked")
val newWalletInfo = ToppedUpWalletStorage.Companion.TopupInfo(
walletId = data.walletId,
cardBalanceState = AnalyticsParam.CardBalanceState.Full,
)
topupWalletStorage.save(newWalletInfo)
return false
}
val topupInfo = topupWalletStorage.restore(data.walletId).guard {
log("BasicTopUpFilter: [false]: sending is blocked")
val topupInfo = ToppedUpWalletStorage.Companion.TopupInfo(
walletId = data.walletId,
cardBalanceState = data.cardBalanceState,
)
topupWalletStorage.save(topupInfo)
return false
}
if (topupInfo.isToppedUp) {
log("BasicTopUpFilter: [false]: sending is blocked")
return false
}
return if (!topupInfo.isToppedUp && data.isToppedUp) {
log("BasicTopUpFilter: [TRUE]: !topupInfo.isToppedUp && data.isToppedUp")
log("BasicTopUpFilter: [TRUE]: SEND")
store.dispatchDebugErrorNotification("Topped UP sent")
topupWalletStorage.save(topupInfo.copy(cardBalanceState = AnalyticsParam.CardBalanceState.Full))
true
} else {
log("BasicTopUpFilter: [false]: sending is blocked")
false
}
}
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean = true
data class Data(
val walletId: String,
val cardBalanceState: AnalyticsParam.CardBalanceState,
val isToppedUpInPast: Boolean,
) {
val isToppedUp: Boolean = cardBalanceState == AnalyticsParam.CardBalanceState.Full
}
}

View file

@ -0,0 +1,156 @@
package com.tangem.tap.common.analytics.topup
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.converters.TopUpEventConverter
import com.tangem.tap.common.extensions.copy
import com.tangem.tap.domain.model.UserWallet
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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import kotlinx.coroutines.launch
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class TopUpController(
var scanResponseProvider: (() -> ScanResponse?)? = null,
var walletStoresManagerProvider: (() -> WalletStoresManager)? = null,
) : WalletCurrenciesManager.Listener {
private var hadMissedDerivations: Boolean = false
private val addedCurrencies = mutableListOf<Currency>()
override fun willUpdate(userWallet: UserWallet, currency: Currency) {
}
override fun didUpdate(userWallet: UserWallet, currency: Currency) {
tryToNotify()
}
override fun willCurrenciesAdd(userWallet: UserWallet, currenciesToAdd: List<Currency>) {
addedCurrencies.addAll(currenciesToAdd.distinct())
log("addCurrencies = [${addedCurrencies.joinToString()}]")
}
override fun willCurrenciesRemove(userWallet: UserWallet, currenciesToRemove: List<Currency>) {
}
override fun willCurrencyRemove(userWallet: UserWallet, currencyToRemove: Currency) {
}
fun walletStoresChanged(walletStores: List<WalletStoreModel>) {
val missedDerivations = walletStores
.flatMap { it.walletsData }
.map { it.status }
.filterIsInstance<WalletDataModel.MissedDerivation>()
hadMissedDerivations = missedDerivations.isNotEmpty()
log("walletStoresChanged: hadMissedDerivations = [${hadMissedDerivations}]")
}
fun scanToGetDerivations() {
hadMissedDerivations = true
log("scanAndUpdateCard: hadMissedDerivations = [${hadMissedDerivations}]")
}
fun addMissingDerivations(blockchains: List<BlockchainNetwork>) {
hadMissedDerivations = blockchains.isNotEmpty()
log("addMissingDerivations: hadMissedDerivations = [${hadMissedDerivations}]")
}
fun totalBalanceStateChanged(state: ProgressState) {
log("totalBalanceStateChanged = [${state.name}]")
if (state == ProgressState.Done) {
tryToNotify()
}
}
fun loadDataSuccess() {
log("loadDataSuccess")
tryToNotify()
}
private fun tryToNotify() {
if (hadMissedDerivations) {
log("tryToNotify: FAILED: derivationsCheckIsScheduled")
return
}
val scanResponse = scanResponseProvider?.invoke() ?: return
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() ?: return
val walletStoresManager = walletStoresManagerProvider?.invoke() ?: return
scope.launch {
val walletDataModels = walletStoresManager.getSync(userWalletId).flatMap { it.walletsData }
if (walletDataModels.isEmpty()) {
log("tryToNotify: FAILED: walletDataModels.size = [0]")
return@launch
}
val isCorrectStatus = walletDataModels.any {
it.status is WalletDataModel.Loading ||
it.status is WalletDataModel.NoAccount ||
it.status is WalletDataModel.Unreachable ||
it.status is WalletDataModel.MissedDerivation ||
it.status.isErrorStatus
}
if (isCorrectStatus) {
log("tryToNotify: FAILED: by status")
return@launch
}
notify(userWalletId, walletDataModels, scanResponse.cardTypesResolver)
}
}
private fun notify(
userWalletId: UserWalletId,
walletDataModels: List<WalletDataModel>,
cardTypesResolver: CardTypesResolver,
) {
val isToppedUpInPast = findToppedUpCurrenciesInPast(walletDataModels).isNotEmpty()
log("notify: currencies from manage tokens had toppedUp in the past = [${isToppedUpInPast}]")
val data = TopUpEventConverter.Data(
cardTypesResolver = cardTypesResolver,
walletDataModels = walletDataModels,
userWalletIdValue = userWalletId.stringValue,
isToppedUpInPast = isToppedUpInPast,
)
val event = TopUpEventConverter().convert(data).guard {
log("notify: TopUpEventConverter can't convert a data to an event")
return
}
log("notify: Analytics.send(event)")
Analytics.send(event)
}
private fun findToppedUpCurrenciesInPast(walletDataModels: List<WalletDataModel>): List<WalletDataModel> {
log("findToppedUpCurrenciesInPast: added new currencies = [${addedCurrencies.size}]")
val currenciesToppedUpInPast = addedCurrencies.copy()
.mapNotNull { currency ->
val foundCurrencyModel = walletDataModels
.find { it.currency == currency }
?: return@mapNotNull null
if (foundCurrencyModel.status.amount.isZero()) null else foundCurrencyModel
}
addedCurrencies.clear()
return currenciesToppedUpInPast
}
}
fun log(log: String) {
Timber.d("TopUp: %s", log)
}

View file

@ -28,4 +28,8 @@ fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boolean {
fun <T> MutableList<T>.replaceByOrAdd(item: T, predicate: (T) -> Boolean) {
if (!replaceBy(item, predicate)) add(item)
}
fun <T> MutableList<T>.copy(): MutableList<T> {
return this.map { it }.toMutableList()
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
@ -28,6 +29,7 @@ data class GlobalState(
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val userCountryCode: String? = null,
val userWalletsListManager: UserWalletsListManager? = null,
val topUpController: TopUpController = TopUpController(),
) : StateType
typealias CryptoCurrencyName = String

View file

@ -22,7 +22,6 @@ import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.middlewares.handleBasicAnalyticsEvent
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -67,7 +66,7 @@ class TapWalletManager {
.doOnSuccess {
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
store.dispatchOnMain(WalletAction.LoadData.Success)
handleBasicAnalyticsEvent()
store.state.globalState.topUpController.loadDataSuccess()
}
.doOnFailure { error ->
val errorAction = when (error) {

View file

@ -69,6 +69,31 @@ interface WalletCurrenciesManager {
currenciesToRemove: List<Currency>,
): CompletionResult<Unit>
/**
* Add a callback [Listener]
*
* @param listener The callback that will add
*/
fun addListener(listener: Listener)
/**
* Remove a callback [Listener]
*
* @param listener The callback that will removed
*/
fun removeListener(listener: Listener)
/**
* Interface definition for a callbacks
*/
interface Listener {
fun willUpdate(userWallet: UserWallet, currency: Currency)
fun didUpdate(userWallet: UserWallet, currency: Currency)
fun willCurrenciesAdd(userWallet: UserWallet, currenciesToAdd: List<Currency>)
fun willCurrenciesRemove(userWallet: UserWallet, currenciesToRemove: List<Currency>)
fun willCurrencyRemove(userWallet: UserWallet, currencyToRemove: Currency)
}
// For provider
companion object
}

View file

@ -30,23 +30,31 @@ internal class DefaultWalletCurrenciesManager(
private val walletManagersRepository: WalletManagersRepository,
private val appCurrencyProvider: () -> FiatCurrency,
) : WalletCurrenciesManager {
private val listeners = mutableListOf<WalletCurrenciesManager.Listener>()
override suspend fun update(
userWallet: UserWallet,
currency: Currency,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
listeners.forEach { it.willUpdate(userWallet, currency) }
val walletStore = walletStoresRepository.getSync(userWallet.walletId)
.find {
it.blockchain == currency.blockchain &&
it.derivationPath?.rawPath == currency.derivationPath
}
if (walletStore != null) {
val updateResult = if (walletStore == null) {
CompletionResult.Success(Unit)
} else {
walletAmountsRepository.updateAmountsForWalletStore(
walletStore = walletStore,
userWallet = userWallet,
fiatCurrency = appCurrencyProvider(),
)
} else CompletionResult.Success(Unit)
}
listeners.forEach { it.didUpdate(userWallet, currency) }
updateResult
}
override suspend fun addCurrencies(
@ -55,6 +63,7 @@ internal class DefaultWalletCurrenciesManager(
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
val card = userWallet.scanResponse.card
val currenciesToAddWithMissingBlockchains = currenciesToAdd.addMissingBlockchainsIfNeeded(card)
listeners.forEach { it.willCurrenciesAdd(userWallet, currenciesToAddWithMissingBlockchains) }
updateWalletStores(
userWallet = userWallet,
@ -77,6 +86,7 @@ internal class DefaultWalletCurrenciesManager(
userWallet: UserWallet,
currenciesToRemove: List<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
listeners.forEach { it.willCurrenciesRemove(userWallet, currenciesToRemove) }
val card = userWallet.scanResponse.card
val remainingCurrencies = getSavedCurrencies(userWallet.walletId)
.filter { it !in currenciesToRemove }
@ -96,9 +106,18 @@ internal class DefaultWalletCurrenciesManager(
userWallet: UserWallet,
currencyToRemove: Currency,
): CompletionResult<Unit> {
listeners.forEach { it.willCurrencyRemove(userWallet, currencyToRemove) }
return removeCurrencies(userWallet, listOf(currencyToRemove))
}
override fun addListener(listener: WalletCurrenciesManager.Listener) {
listeners.add(listener)
}
override fun removeListener(listener: WalletCurrenciesManager.Listener) {
listeners.remove(listener)
}
private suspend fun getSavedCurrencies(userWalletId: UserWalletId): List<Currency> {
return withContext(Dispatchers.Default) {
walletStoresRepository.getSync(userWalletId)

View file

@ -19,9 +19,6 @@ import com.tangem.tap.store
import org.rekotlin.StateType
import kotlin.properties.ReadOnlyProperty
/**
* @property derivationsCheckIsScheduled - used only for analytics
*/
data class WalletState(
val cardId: String = "",
val state: ProgressState = ProgressState.Done,
@ -37,7 +34,6 @@ 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,
val canSaveUserWallets: Boolean = false,

View file

@ -91,13 +91,14 @@ class MultiWalletMiddleware {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is WalletAction.MultiWallet.AddMissingDerivations -> {
scope.launch { handleBasicAnalyticsEvent() }
store.state.globalState.topUpController.addMissingDerivations(action.blockchains)
}
is WalletAction.MultiWallet.ScanToGetDerivations -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to scan to get derivations, no user wallet selected")
return
}
store.state.globalState.topUpController.scanToGetDerivations()
scanAndUpdateCard(selectedUserWallet, walletState)
}
else -> {}
@ -108,7 +109,6 @@ class MultiWalletMiddleware {
selectedUserWallet: UserWallet,
state: WalletState?,
) = scope.launch(Dispatchers.Default) {
dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
tangemSdkManager.scanProduct(
cardId = selectedUserWallet.cardId,
userTokensRepository = userTokensRepository,

View file

@ -6,8 +6,6 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
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.Basic
import com.tangem.tap.common.analytics.events.MainScreen
@ -25,7 +23,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
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.domain.userWalletList.lockIfLockable
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
@ -46,7 +43,6 @@ 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.delay
@ -188,7 +184,7 @@ class WalletMiddleware {
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
store.dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
store.state.globalState.topUpController.walletStoresChanged(action.walletStores)
updateWalletStores(action.walletStores, walletState)
fetchTotalFiatBalance(action.walletStores)
findMissedDerivations(action.walletStores)
@ -351,23 +347,4 @@ class WalletMiddleware {
tokenRate = tokenRate,
)
}
}
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 = scanResponse,
walletState = store.state.walletState,
biometricsWalletDataModels = biometricsWalletDataModels,
)
BasicEventsPreChecker().tryToSend(converterData)
}

View file

@ -15,12 +15,8 @@ class MultiWalletReducer {
state.copy(selectedCurrency = action.currency)
}
is WalletAction.MultiWallet.TryToRemoveWallet -> state
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,6 +17,7 @@ import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import coil.size.Scale
import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener
@ -63,6 +64,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private val viewModel by viewModels<WalletViewModel>()
private val totalBalanceWatcher = modelWatcher {
(WalletState::totalBalance) { totalBalance ->
totalBalance?.state?.let {
store.state.globalState.topUpController.totalBalanceStateChanged(it)
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
lifecycle.addObserver(viewModel)
@ -167,6 +176,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
else -> {} // we keep the same view unless we scan a card that requires a different view
}
totalBalanceWatcher.invoke(state)
walletView.swapInteractor = swapInteractor

View file

@ -3,7 +3,7 @@ package com.tangem.tap.persistence
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import timber.log.Timber
/**
@ -14,23 +14,23 @@ class ToppedUpWalletStorage(
private val jsonConverter: MoshiJsonConverter,
) {
private val walletList: MutableSet<BasicTopUpFilter.Data> = mutableSetOf()
private val walletList: MutableSet<TopupInfo> = mutableSetOf()
init {
walletList.addAll(restore())
}
fun save(userWalletInfo: BasicTopUpFilter.Data): Boolean {
fun save(userWalletInfo: TopupInfo): Boolean {
walletList.removeAll { it.walletId == userWalletInfo.walletId }
walletList.add(userWalletInfo)
return save(walletList)
}
fun restore(walletId: String): BasicTopUpFilter.Data? {
fun restore(walletId: String): TopupInfo? {
return walletList.firstOrNull { it.walletId == walletId }
}
private fun save(userWallets: MutableSet<BasicTopUpFilter.Data>): Boolean {
private fun save(userWallets: MutableSet<TopupInfo>): Boolean {
return try {
val json = jsonConverter.toJson(userWallets)
preferences.edit(true) { putString(KEY, json) }
@ -41,11 +41,11 @@ class ToppedUpWalletStorage(
}
}
private fun restore(): MutableSet<BasicTopUpFilter.Data> {
private fun restore(): MutableSet<TopupInfo> {
val json = preferences.getString(KEY, null) ?: return mutableSetOf()
return try {
val typedList = jsonConverter.typedList(BasicTopUpFilter.Data::class.java)
val listData = jsonConverter.fromJson<List<BasicTopUpFilter.Data>>(json, typedList)!!
val typedList = jsonConverter.typedList(TopupInfo::class.java)
val listData = jsonConverter.fromJson<List<TopupInfo>>(json, typedList)!!
listData.toMutableSet()
} catch (ex: Exception) {
preferences.edit(true) { remove(KEY) }
@ -55,5 +55,12 @@ class ToppedUpWalletStorage(
companion object {
private const val KEY = "userWalletsInfo"
data class TopupInfo(
val walletId: String,
val cardBalanceState: AnalyticsParam.CardBalanceState,
) {
val isToppedUp: Boolean = cardBalanceState == AnalyticsParam.CardBalanceState.Full
}
}
}