Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-16 15:14:23 +01:00
commit 48f20925f6
53 changed files with 541 additions and 76 deletions

View file

@ -7,6 +7,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import kotlinx.coroutines.*
import timber.log.Timber
import kotlin.time.Duration
@ -103,12 +104,14 @@ internal class LockUserWalletsTimer(
|- Millis passed: ${currentTime - startTime}
""".trimIndent(),
)
userWalletsListManager.lock()
if (preferencesStorage.wasApplicationStopped) {
preferencesStorage.shouldOpenWelcomeScreenOnResume = true
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Welcome))
}
userWalletsListManager.lock()
}
}
}

View file

@ -19,12 +19,14 @@ import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -35,10 +37,13 @@ import com.tangem.data.card.sdk.CardSdkLifecycleObserver
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tester.api.TesterRouter
@ -95,6 +100,7 @@ val userWalletsListManagerSafe: UserWalletsListManager?
val userWalletsListManager: UserWalletsListManager
get() = userWalletsListManagerSafe!!
@Suppress("LargeClass")
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
@ -141,6 +147,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@Inject
lateinit var getPolkadotCheckHasImmortalUseCase: GetPolkadotCheckHasImmortalUseCase
@Inject
lateinit var analyticsEventsHandler: AnalyticsEventHandler
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -168,6 +183,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
checkForNotificationPermission()
observeStateUpdates()
observePolkadotAccountHealthCheck()
if (intent != null) {
deepLinksRegistry.launch(intent)
@ -457,4 +473,25 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0)
}
}
private fun observePolkadotAccountHealthCheck() {
lifecycleScope.launch {
getPolkadotCheckHasResetUseCase()
.flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED)
.distinctUntilChanged()
.collect {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Token.PolkadotAccountReset(it.second))
}
}
lifecycleScope.launch {
getPolkadotCheckHasImmortalUseCase()
.flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED)
.distinctUntilChanged()
.collect {
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.Token.PolkadotImmortalTransactions(it.second),
)
}
}
}
}

View file

@ -46,7 +46,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context, state.dialog.source)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
@ -51,7 +52,11 @@ sealed class GlobalAction : Action {
}
object ScanFailsCounter {
data class ChooseBehavior(val result: CompletionResult<ScanResponse>) : GlobalAction()
data class ChooseBehavior(
val result: CompletionResult<ScanResponse>,
val analyticsSource: AnalyticsParam.ScreensSources,
) : GlobalAction()
object Reset : GlobalAction()
object Increment : GlobalAction()
}

View file

@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.models.Config
@ -54,14 +55,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
when (action.result) {
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
is CompletionResult.Failure -> {
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatchDialogShow(StateDialog.ScanFailsDialog)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
}
handleFailureChooseBehaviour(action.result, action.analyticsSource)
}
}
}
@ -184,6 +178,26 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
private fun handleFailureChooseBehaviour(
result: CompletionResult.Failure<ScanResponse>,
analyticsSource: AnalyticsParam.ScreensSources,
) {
if (result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
val scanFailsSource = when (analyticsSource) {
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
else -> StateDialog.ScanFailsSource.MAIN
}
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
}
}
private fun restoreAppCurrency() {
scope.launch {
val currency = store.inject(DaggerGraphState::appCurrencyRepository)

View file

@ -4,7 +4,9 @@ import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.feedback.ScanFailsEmail
import com.tangem.tap.common.redux.global.GlobalAction
@ -15,12 +17,18 @@ import com.tangem.wallet.R
[REDACTED_AUTHOR]
*/
object ScanFailsDialog {
fun create(context: Context): AlertDialog {
fun create(context: Context, source: StateDialog.ScanFailsSource): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
Analytics.send(Basic.ButtonSupport())
val sourceAnalytics = when (source) {
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
}
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
}
setNeutralButton(R.string.common_cancel) { _, _ -> }

View file

@ -4,6 +4,9 @@ import android.content.Context
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
@ -57,4 +60,20 @@ internal object ActivityModule {
fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
@Provides
@Singleton
fun provideGetPolkadotCheckHasResetUseCase(
polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
): GetPolkadotCheckHasResetUseCase {
return GetPolkadotCheckHasResetUseCase(polkadotAccountHealthCheckRepository)
}
@Provides
@Singleton
fun provideGetPolkadotCheckHasImmortalUseCase(
polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
): GetPolkadotCheckHasImmortalUseCase {
return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository)
}
}

View file

@ -343,4 +343,12 @@ internal object TokensDomainModule {
): IsAmountSubtractAvailableUseCase {
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideRunPolkadotAccountHealthCheckUseCase(
repository: PolkadotAccountHealthCheckRepository,
): RunPolkadotAccountHealthCheckUseCase {
return RunPolkadotAccountHealthCheckUseCase(repository)
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.domain.scanCard
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
@ -27,7 +27,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
@Suppress("LongParameterList")
override suspend fun scan(
analyticsEvent: AnalyticsEvent?,
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
@ -37,7 +37,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
) {
if (isNewCardScanningEnabled) {
UseCaseScanProcessor.scan(
analyticsEvent,
analyticsSource,
cardId,
onProgressStateChange,
onWalletNotCreated,
@ -47,7 +47,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
)
} else {
LegacyScanProcessor.scan(
analyticsEvent,
analyticsSource,
cardId,
onProgressStateChange,
onWalletNotCreated,

View file

@ -7,6 +7,8 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
@ -45,7 +47,7 @@ internal object LegacyScanProcessor {
@Suppress("LongParameterList")
suspend fun scan(
analyticsEvent: AnalyticsEvent?,
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
@ -59,7 +61,8 @@ internal object LegacyScanProcessor {
val result = tangemSdkManager.scanProduct(cardId)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
val analyticsEvent = Basic.CardWasScanned(analyticsSource)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource))
result
.doOnFailure { error ->

View file

@ -4,7 +4,7 @@ import arrow.fx.coroutines.resourceScope
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -44,7 +44,7 @@ internal object UseCaseScanProcessor {
@Suppress("LongParameterList")
suspend fun scan(
analyticsEvent: AnalyticsEvent?,
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
@ -55,9 +55,7 @@ internal object UseCaseScanProcessor {
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
val chains = buildList {
add(FailedScansCounterChain(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog))
if (analyticsEvent != null) {
add(AnalyticsChain(analyticsEvent))
}
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
add(DisclaimerChain(store, disclaimerWillShow))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}

View file

@ -81,6 +81,10 @@ internal class BiometricUserWalletsListManager(
state.update { State() }
}
override fun isLockable(): Boolean {
return true
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
if (state.value.selectedUserWalletId == userWalletId) {
return@catching findSelectedUserWallet()!!

View file

@ -118,6 +118,10 @@ internal class GeneralUserWalletsListManager(
}
}
override fun isLockable(): Boolean {
return implementation.value.isLockable()
}
private fun subscribeOnCurrentManager() {
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
.distinctUntilChanged()

View file

@ -85,6 +85,10 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
state.value.userWallet ?: walletNotFound()
}
override fun isLockable(): Boolean {
return false
}
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(

View file

@ -7,7 +7,6 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.extensions.TextReference
@ -580,7 +579,7 @@ class DetailsMiddleware {
)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsEvent = Basic.CardWasScanned(CoreAnalyticsParam.ScannedFrom.MyWallets),
analyticsSource = CoreAnalyticsParam.ScreensSources.Settings,
onWalletNotCreated = {
// No need to rollback policy, continue with the policy set before the card scan
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -146,7 +147,7 @@ internal class DetailsViewModel(
}
private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport())
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
mainScope.launch {
val email = getSupportFeedbackEmailUseCase()

View file

@ -1,8 +1,5 @@
package com.tangem.tap.features.home.redux
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@ -16,11 +13,9 @@ sealed class HomeAction : Action {
/**
* Action for scanning card
*
* @property analyticsEvent analytics event
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
*/
data class ReadCard(
val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction),
val scope: CoroutineScope,
) : HomeAction()

View file

@ -5,7 +5,7 @@ import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -60,7 +60,7 @@ private fun handleHomeAction(action: Action) {
}
is HomeAction.ReadCard -> {
action.scope.launch {
readCard(action.analyticsEvent)
readCard()
}
}
is HomeAction.GoToShop -> {
@ -76,13 +76,13 @@ private fun handleHomeAction(action: Action) {
}
}
private suspend fun readCard(analyticsEvent: AnalyticsEvent?) {
private suspend fun readCard() {
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes,
)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsEvent = analyticsEvent,
analyticsSource = AnalyticsParam.ScreensSources.Intro,
onProgressStateChange = { showProgress ->
if (showProgress) {
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))

View file

@ -17,6 +17,7 @@ import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.*
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.delay
@ -42,7 +43,8 @@ object OnboardingHelper {
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = onboardingManager?.isActivationInProgress(cardId)
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
!DemoHelper.isDemoCard(response)
emptyWallets || activationInProgress == true || isNoBackup
}

View file

@ -5,6 +5,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
@ -21,7 +22,7 @@ class OnboardingMenuProvider : MenuProvider {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.menu_item_chat_support -> {
Analytics.send(Basic.ButtonSupport())
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
true

View file

@ -22,6 +22,7 @@ import com.tangem.common.CardIdFormatter
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.domain.common.util.cardTypesResolver
@ -502,7 +503,7 @@ class OnboardingWalletFragment :
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
onBack = ::legacyOnBackHandler,
onOpenChat = {
Analytics.send(Basic.ButtonSupport())
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
},

View file

@ -4,6 +4,7 @@ import android.app.Dialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.feedback.SupportInfo
@ -21,7 +22,7 @@ object WalletActivationErrorDialog {
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY]
Analytics.send(Basic.ButtonSupport())
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
}
setOnDismissListener { store.dispatchDialogHide() }

View file

@ -8,7 +8,6 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.isLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -209,7 +208,7 @@ internal class SaveWalletMiddleware {
}
private suspend fun provideLockableUserWalletsListManagerIfNot() {
if (store.state.globalState.userWalletsListManager?.isLockable == true) return
if (store.state.globalState.userWalletsListManager?.isLockable() == true) return
val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard {
val error = IllegalStateException("No activities in foreground")

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction
@ -21,7 +22,7 @@ object RequestFeeErrorDialog {
setTitle(R.string.common_fee_error)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport())
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.sdk.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString
@ -33,7 +34,7 @@ object SendTransactionFailsDialog {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport())
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_cancel) { _, _ -> }

View file

@ -169,7 +169,7 @@ internal class WelcomeMiddleware {
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes,
)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn),
analyticsSource = AnalyticsParam.ScreensSources.SignIn,
onSuccess = { scanResponse ->
scope.launch { onCardScanned(scanResponse) }
},

View file

@ -57,11 +57,13 @@ sealed class AnalyticsParam {
object BlockchainSdk : Error("Blockchain Sdk Error")
}
sealed class ScannedFrom(val value: String) {
object Introduction : ScannedFrom("Introduction")
object Main : ScannedFrom("Main")
object SignIn : ScannedFrom("Sign In")
object MyWallets : ScannedFrom("My Wallets")
sealed class ScreensSources(val value: String) {
data object Settings : ScreensSources("Settings")
data object Main : ScreensSources("Main")
data object SignIn : ScreensSources("Sign In")
data object Send : ScreensSources("Send")
data object Intro : ScreensSources("Introduction")
data object MyWallets : ScreensSources("My Wallets")
}
sealed class TxSentFrom(val value: String) {

View file

@ -7,7 +7,7 @@ sealed class Basic(
) : AnalyticsEvent("Basic", event, params, error) {
class CardWasScanned(
source: AnalyticsParam.ScannedFrom,
source: AnalyticsParam.ScreensSources,
) : Basic(
event = "Card Was Scanned",
params = mapOf(
@ -76,5 +76,10 @@ sealed class Basic(
class WalletOpened : Basic(event = "Wallet Opened")
class ButtonSupport : Basic("Request Support")
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
event = "Request Support",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
),
)
}

View file

@ -72,6 +72,12 @@ class AppPreferencesStore(
return this[key]?.let(adapter::fromJson).orEmpty()
}
/** Get set of data [T] by string [key] */
inline fun <reified T> MutablePreferences.getObjectSet(key: Preferences.Key<String>): Set<T>? {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return this[key]?.let(adapter::fromJson)
}
/**
* Set data [T] by string [key] to [MutablePreferences]
*
@ -97,4 +103,10 @@ class AppPreferencesStore(
this[key] = adapter.toJson(value)
}
/** Sets set of data [T] by string [key] to [MutablePreferences] */
inline fun <reified T> MutablePreferences.setObjectSet(key: Preferences.Key<String>, value: Set<T>) {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
this[key] = adapter.toJson(value)
}
}

View file

@ -63,6 +63,16 @@ object PreferencesKeys {
val APP_LOGS_KEY by lazy { stringPreferencesKey(name = "app_logs") }
val POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY by lazy {
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX")
}
val POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY by lazy {
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS")
}
val POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY by lazy {
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS")
}
val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") }
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")

View file

@ -133,3 +133,12 @@ suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences
?.let(adapter::fromJson)
.orEmpty()
}
/** Get set of data [T] by string [key], or empty if data is not found */
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}

View file

@ -2,5 +2,9 @@ package com.tangem.core.navigation
interface StateDialog {
object ScanFailsDialog : StateDialog
data class ScanFailsDialog(val source: ScanFailsSource) : StateDialog
enum class ScanFailsSource {
MAIN, SIGN_IN, SETTINGS, INTRO;
}
}

View file

@ -119,4 +119,18 @@ internal object TokensDataModule {
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {
return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade)
}
@Provides
@Singleton
fun providePolkadotAccountHealthCheckRepository(
walletManagersFacade: WalletManagersFacade,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): PolkadotAccountHealthCheckRepository {
return DefaultPolkadotAccountHealthCheckRepository(
walletManagersFacade = walletManagersFacade,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
}

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import timber.log.Timber
@Suppress("LongParameterList")
internal class DefaultNetworksRepository(
private val networksStatusesStore: NetworksStatusesStore,
private val walletManagersFacade: WalletManagersFacade,

View file

@ -0,0 +1,179 @@
package com.tangem.data.tokens.repository
import androidx.datastore.preferences.core.Preferences
import com.tangem.blockchain.blockchains.polkadot.AccountCheckProvider
import com.tangem.blockchain.blockchains.polkadot.network.accounthealthcheck.ExtrinsicListItemResponse
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.preferences.utils.getObjectSetSync
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.set
internal class DefaultPolkadotAccountHealthCheckRepository(
private val walletManagersFacade: WalletManagersFacade,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : PolkadotAccountHealthCheckRepository {
private val hasImmortalTransaction = MutableSharedFlow<Pair<String, Boolean>>()
private val hasResetTransaction = MutableSharedFlow<Pair<String, Boolean>>()
private val mutex = Mutex()
private val mutexes = ConcurrentHashMap<String, Mutex>()
override suspend fun runCheck(userWalletId: UserWalletId, network: Network) {
// Run Polkadot account health check
if (Blockchain.fromId(network.id.value) != Blockchain.Polkadot) return
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
val address = requireNotNull(walletManager?.wallet?.address) {
Timber.e("Address is null")
return
}
val accountCheckProvider = requireNotNull(walletManager as? AccountCheckProvider) {
Timber.e("Unable to cast wallet manager to AccountCheckProvider")
return
}
// use a separate mutexForKey for each key to avoid multiple calls block() to the same key
// also used mutex to safe create mutexForKey, otherwise it can lead to multiple calls for the same key
val mutexForKey = mutex.withLock { mutexes.getOrPut(address) { Mutex() } }
mutexForKey.withLock {
withContext(dispatchers.io) {
checkHasReset(accountCheckProvider, address)
checkHasImmortal(accountCheckProvider, address)
}
}
}
override fun subscribeToHasImmortalResults() = hasImmortalTransaction.asSharedFlow()
override fun subscribeToHasResetResults() = hasResetTransaction.asSharedFlow()
private suspend fun checkHasReset(polkadotManager: AccountCheckProvider, address: String) {
val checkedAddresses = appPreferencesStore.getObjectSetSync<String>(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
if (checkedAddresses.contains(address)) return
runCatching {
val accountInfo = requireNotNull(polkadotManager.getAccountInfo().account) {
Timber.e("Account info is null")
}
val nonce = accountInfo.nonce
val extrinsicCount = accountInfo.countExtrinsic
// Account was reset
if (nonce != null && extrinsicCount != null) {
val hasReset = nonce < extrinsicCount
updateChecked(address, POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
hasResetTransaction.emit(address to hasReset)
}
}.onFailure {
if ((it as? BlockchainSdkError.CustomError)?.customMessage == ACCOUNT_NOT_FOUND) {
updateChecked(address, POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
}
}
}
private suspend fun checkHasImmortal(accountCheckerProvider: AccountCheckProvider, address: String) {
val checkedAddresses =
appPreferencesStore.getObjectSetSync<String>(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY)
if (checkedAddresses.contains(address)) return
runCatching {
do {
// Getting batch of extrinsics to check
val lastChecked = appPreferencesStore.getObjectMap<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY)
val lastExtrinsic = lastChecked[address]
val extrinsicListResult = accountCheckerProvider.getExtrinsicList(afterExtrinsicId = lastExtrinsic)
extrinsicListResult.extrinsic
?.forEach { tx ->
// Checking extrinsic one by one
if (checkTx(tx, address, accountCheckerProvider)) return
}
} while (!extrinsicListResult.extrinsic.isNullOrEmpty())
// We checked all transactions up to current moment and did not found an `immortal` transaction
hasImmortalTransaction.emit(address to false)
updateChecked(address, POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY)
clearLastCheckedTransaction(address)
}
}
private suspend fun checkTx(
tx: ExtrinsicListItemResponse,
address: String,
accountCheckerProvider: AccountCheckProvider,
): Boolean {
val hash = tx.hash
val id = tx.id
if (hash != null && id != null) {
val details = accountCheckerProvider.getExtrinsicDetail(hash)
// We found an `immortal` transaction
if (details.lifetime == null) {
hasImmortalTransaction.emit(address to true)
updateChecked(address, POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY)
clearLastCheckedTransaction(address)
return true
}
// Saving last checked transaction
updateLastCheckedTransaction(address, id)
}
return false
}
private suspend fun updateLastCheckedTransaction(address: String, txId: Long) {
appPreferencesStore.editData {
val savedList = it.getObjectMap<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY)
val updatedList = savedList.toMutableMap()
updatedList[address] = txId
it.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY, updatedList)
}
}
private suspend fun clearLastCheckedTransaction(address: String) {
appPreferencesStore.editData { mutablePreferences ->
val savedList = mutablePreferences.getObjectMap<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY)
val updatedList = savedList.toMutableMap()
updatedList.remove(address)
mutablePreferences.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY, updatedList)
}
}
private suspend fun updateChecked(address: String, key: Preferences.Key<String>) {
appPreferencesStore.editData { mutablePreferences ->
val savedList = mutablePreferences.getObjectSet<String>(key)
val updatedList = savedList?.toMutableSet() ?: mutableSetOf()
updatedList.add(address)
if (updatedList.isNotEmpty()) {
mutablePreferences.setObjectSet(
key,
updatedList.toSet(),
)
} else {
mutablePreferences.remove(key)
}
}
}
private companion object {
const val ACCOUNT_NOT_FOUND = "Record Not Found"
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.domain.card
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.models.scan.ScanResponse
interface ScanCardProcessor {
@ -13,7 +13,7 @@ interface ScanCardProcessor {
): CompletionResult<ScanResponse>
suspend fun scan(
analyticsEvent: AnalyticsEvent? = null,
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
onWalletNotCreated: suspend () -> Unit = {},

View file

@ -399,12 +399,27 @@ class DemoConfig {
"AB02000000048063",
"AB02000000023736",
"AB02000000058187",
"AB02000000000007",
"AC03000000076229",
"AF04000000000118",
// Wallet 2
"AF04000000012006",
"AF04000000012014",
"AF04000000012022",
"AF04000000012030",
"AF15000000257889",
"AF15000000257897",
"AF15000000637809",
"AF15000000640282",
"AF15000001187424",
"AF15000001187408",
"AF15000001187416",
"AF15000001195781",
"AF15000001195773",
"AF15000001195799",
"AF19000000000038",
"AC19000000000064",
)
@Suppress("ClassOrdering")

View file

@ -0,0 +1,11 @@
package com.tangem.domain.tokens
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import kotlinx.coroutines.flow.Flow
class GetPolkadotCheckHasImmortalUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
operator fun invoke(): Flow<Pair<String, Boolean>> =
polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults()
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.tokens
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import kotlinx.coroutines.flow.Flow
class GetPolkadotCheckHasResetUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
operator fun invoke(): Flow<Pair<String, Boolean>> =
polkadotAccountHealthCheckRepository.subscribeToHasResetResults()
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.wallets.models.UserWalletId
class RunPolkadotAccountHealthCheckUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Unit> = Either.catch {
polkadotAccountHealthCheckRepository.runCheck(userWalletId, network)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface PolkadotAccountHealthCheckRepository {
suspend fun runCheck(userWalletId: UserWalletId, network: Network)
fun subscribeToHasImmortalResults(): Flow<Pair<String, Boolean>>
fun subscribeToHasResetResults(): Flow<Pair<String, Boolean>>
}

View file

@ -84,6 +84,11 @@ interface UserWalletsListManager {
*/
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* Indicates that the [UserWalletsListManager] supports [UserWalletsListManager.Lockable]
* */
fun isLockable(): Boolean
interface Lockable : UserWalletsListManager {
/**

View file

@ -6,12 +6,6 @@ import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
/**
* Indicates that the [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
* */
val UserWalletsListManager.isLockable: Boolean
get() = this is UserWalletsListManager.Lockable
/**
* Indicates that the [UserWalletsListManager] is locked
*
@ -48,16 +42,6 @@ suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockTyp
return asLockable()?.unlock(type) ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets())
}
/**
* Call [UserWalletsListManager.Lockable.lock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
* or do nothing otherwise
*
* @see UserWalletsListManager.Lockable.lock
* */
fun UserWalletsListManager.lockIfLockable() {
asLockable()?.lock()
}
/**
* Safe cast [UserWalletsListManager] to [UserWalletsListManager.Lockable]
*
@ -65,5 +49,8 @@ fun UserWalletsListManager.lockIfLockable() {
* [UserWalletsListManager.Lockable] otherwise
* */
fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? {
return this as? UserWalletsListManager.Lockable
if (this.isLockable()) {
return this as? UserWalletsListManager.Lockable
}
return null
}

View file

@ -163,7 +163,11 @@ internal class DefaultWalletRouter(
}
override fun openScanFailedDialog() {
reduxNavController.navigate(action = NavigationAction.OpenDialog(StateDialog.ScanFailsDialog))
reduxNavController.navigate(
action = NavigationAction.OpenDialog(
StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN),
),
)
}
private companion object {

View file

@ -25,7 +25,7 @@ sealed class WalletScreenAnalyticsEvent {
data object WalletOpened : Basic(event = "Wallet Opened")
class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic(
class CardWasScanned(source: AnalyticsParam.ScreensSources) : Basic(
event = "Card Was Scanned",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
@ -48,6 +48,26 @@ sealed class WalletScreenAnalyticsEvent {
)
}
sealed class Token(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Token", event = event, params = params) {
class PolkadotAccountReset(hasReset: Boolean) : Token(
event = "Polkadot Account Reset",
params = mapOf(
AnalyticsParam.STATE to if (hasReset) "Yes" else "No",
),
)
class PolkadotImmortalTransactions(hasImmortalTransaction: Boolean) : Token(
event = "Polkadot Immortal Transactions",
params = mapOf(
AnalyticsParam.STATE to if (hasImmortalTransaction) "Yes" else "No",
),
)
}
sealed class MainScreen(
event: String,
params: Map<String, String> = mapOf(),

View file

@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -29,6 +30,7 @@ internal class MultiWalletContentLoader(
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val reduxStateHolder: ReduxStateHolder,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> {
@ -42,6 +44,7 @@ internal class MultiWalletContentLoader(
getTokenListUseCase = getTokenListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
),
MultiWalletWarningsSubscriber(
userWallet = userWallet,

View file

@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -26,6 +27,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader {
@ -41,6 +43,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
reduxStateHolder = reduxStateHolder,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -24,6 +25,7 @@ internal class SingleWalletWithTokenContentLoader(
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val getCardTokensListUseCase: GetCardTokensListUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> {
@ -36,6 +38,7 @@ internal class SingleWalletWithTokenContentLoader(
walletWithFundsChecker = walletWithFundsChecker,
getCardTokensListUseCase = getCardTokensListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
),
MultiWalletWarningsSubscriber(
userWallet = userWallet,

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -21,6 +22,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val getCardTokensListUseCase: GetCardTokensListUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader {
@ -34,6 +36,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
getCardTokensListUseCase = getCardTokensListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)
}
}

View file

@ -4,7 +4,9 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
@ -33,6 +35,7 @@ internal abstract class BasicTokenListSubscriber(
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletSubscriber() {
private val sendAnalyticsJobHolder = JobHolder()
@ -53,6 +56,8 @@ internal abstract class BasicTokenListSubscriber(
coroutineScope.launch {
onTokenListReceived(maybeTokenList)
}.saveIn(onTokenListReceivedJobHolder)
coroutineScope.launch { startCheck(maybeTokenList) }
},
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
transform = { maybeTokenList, maybeAppCurrency ->
@ -72,6 +77,21 @@ internal abstract class BasicTokenListSubscriber(
)
}
private suspend fun startCheck(maybeTokenList: Either<TokenListError, TokenList>) {
// Run Polkadot account health check
maybeTokenList.getOrNull()?.let { tokenList ->
val cryptoCurrencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
cryptoCurrencies.forEach {
runPolkadotAccountHealthCheckUseCase(userWallet.walletId, it.currency.network)
}
}
}
protected open suspend fun onTokenListReceived(maybeTokenList: Either<TokenListError, TokenList>) {
/* no-op */
}

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.TokenList
@ -24,6 +25,7 @@ internal class MultiWalletTokenListSubscriber(
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
@ -31,6 +33,7 @@ internal class MultiWalletTokenListSubscriber(
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
@ -17,6 +18,7 @@ internal class SingleWalletWithTokenListSubscriber(
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
@ -24,6 +26,7 @@ internal class SingleWalletWithTokenListSubscriber(
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): MaybeTokenListFlow = getCardTokensListUseCase(userWallet.walletId)

View file

@ -107,7 +107,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>) {
analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main))
analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main))
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped)
viewModelScope.launch(dispatchers.main) {