Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-05 09:12:03 +00:00
commit 3f386274ec
286 changed files with 4852 additions and 2134 deletions

View file

@ -36,6 +36,8 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
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.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.navigation.SendRouter
@ -140,6 +142,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var deepLinksRegistry: DeepLinksRegistry
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
@Inject
lateinit var generalUserWalletsListManager: UserWalletsListManager
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -438,7 +446,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val canSaveWallets = userWalletsListManager is BiometricUserWalletsListManager
val canSaveWallets = if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) {
runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
} else {
userWalletsListManager is BiometricUserWalletsListManager
}
val hasSavedWallets = userWalletsListManager.hasUserWallets
if (canSaveWallets && hasSavedWallets) {

View file

@ -30,11 +30,16 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
@ -155,6 +160,24 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var accountCreator: AccountCreator
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
@Inject
lateinit var generalUserWalletsListManager: UserWalletsListManager
@Inject
lateinit var wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase
@Inject
lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase
@Inject
lateinit var cardRepository: CardRepository
@Inject
lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
// endregion Injected
override fun onCreate() {
@ -183,8 +206,13 @@ internal class TapApplication : Application(), ImageLoaderFactory {
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
runBlocking {
initUserWalletsListManager()
featureTogglesManager.init()
if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) {
store.dispatch(GlobalAction.UpdateUserWalletsListManager(generalUserWalletsListManager))
} else {
initUserWalletsListManager()
}
}
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
@ -232,6 +260,12 @@ internal class TapApplication : Application(), ImageLoaderFactory {
sendFeatureToggles = sendFeatureToggles,
blockchainDataStorage = blockchainDataStorage,
accountCreator = accountCreator,
userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles,
generalUserWalletsListManager = generalUserWalletsListManager,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
cardRepository = cardRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
),
),
)

View file

@ -128,18 +128,13 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::walletRouter)
.getEntryFragment()
store.inject(getDependency = DaggerGraphState::walletRouter).getEntryFragment()
}
AppScreen.Send -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::sendFeatureToggles,
)
val featureToggles = store.inject(getDependency = DaggerGraphState::sendFeatureToggles)
if (featureToggles.isRedesignedSendEnabled) {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::sendRouter)
.getEntryFragment()
store.inject(getDependency = DaggerGraphState::sendRouter).getEntryFragment()
} else {
SendFragment()
}
@ -154,15 +149,11 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.ManageTokens -> TokensListFragment()
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.WalletDetails -> {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::tokenDetailsRouter)
.getEntryFragment()
store.inject(getDependency = DaggerGraphState::tokenDetailsRouter).getEntryFragment()
}
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScanning -> {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::qrScanningRouter)
.getEntryFragment()
store.inject(getDependency = DaggerGraphState::qrScanningRouter).getEntryFragment()
}
AppScreen.ReferralProgram -> ReferralFragment()
AppScreen.Swap -> SwapFragment()

View file

@ -7,6 +7,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -87,4 +88,10 @@ fun Store<*>.dispatchOpenUrl(url: String) {
fun Store<*>.dispatchShare(url: String) {
dispatch(NavigationAction.Share(url))
}
inline fun <reified T> Store<AppState>.inject(getDependency: DaggerGraphState.() -> T?): T {
return requireNotNull(state.daggerGraphState.getDependency()) {
"${T::class.simpleName} isn't initialized "
}
}

View file

@ -37,7 +37,7 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try
} catch (exception: Exception) {
Timber.e(exception)
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {
Result.Failure(TapError.NoInternetConnection)
} else {

View file

@ -0,0 +1,12 @@
package com.tangem.tap.common.feedback
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
internal class DefaultFeedbackManagerFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : FeedbackManagerFeatureToggles {
override val isLocalLogsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "LOCAL_USER_LOGS_ENABLED")
}

View file

@ -0,0 +1,42 @@
package com.tangem.tap.common.log
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* CardSDK logger implementation
*
* @property levels logging levels
* @property messageFormatter message formatter
* @property settingsRepository settings repository
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class TangemCardSDKLogger(
private val levels: List<Log.Level>,
private val messageFormatter: LogFormat,
private val settingsRepository: SettingsRepository,
private val dispatchers: CoroutineDispatcherProvider,
) : TangemSdkLogger {
private val scope = CoroutineScope(dispatchers.main)
private val mutex = Mutex()
override fun log(message: () -> String, level: Log.Level) {
if (!levels.contains(level)) return
scope.launch(dispatchers.main) {
mutex.withLock {
settingsRepository.updateAppLogs(message = messageFormatter.format(message, level))
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -20,7 +21,7 @@ class AccessCodeRequestPolicyMiddleware {
}
private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) {
store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet,
)
}

View file

@ -123,7 +123,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
primaryRules = CardExchangeRules(cardProvider),
)
// TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance)
store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager
store.inject(DaggerGraphState::appStateHolder).exchangeService = exchangeManager
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
}
@ -155,7 +155,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
is GlobalAction.UpdateUserWalletsListManager -> {
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade)
/*
* If implementation of the UserWalletsListManager is changed,
@ -186,7 +186,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
private fun restoreAppCurrency() {
scope.launch {
val currency = store.state.daggerGraphState.get(DaggerGraphState::appCurrencyRepository)
val currency = store.inject(DaggerGraphState::appCurrencyRepository)
.getSelectedAppCurrency()
.firstOrNull()
?: AppCurrency.Default

View file

@ -3,11 +3,12 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.extensions.replaceBy
import org.rekotlin.Action
@ -20,8 +21,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
return when (action) {
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
val onboardingManager = OnboardingManager(action.scanResponse)
globalState.copy(onboardingState = OnboardingState(true, onboardingManager))
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
@ -95,10 +95,17 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
)
}
is GlobalAction.UpdateUserWalletsListManager -> {
appStateHolder.userWalletsListManager = action.manager
globalState.copy(
userWalletsListManager = action.manager,
)
val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
if (featureToggles.isGeneralManagerEnabled) {
val generalUserWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
appStateHolder.userWalletsListManager = generalUserWalletsListManager
globalState.copy(userWalletsListManager = generalUserWalletsListManager)
} else {
appStateHolder.userWalletsListManager = action.manager
globalState.copy(userWalletsListManager = action.manager)
}
}
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
appThemeMode = action.appThemeMode,

View file

@ -0,0 +1,34 @@
package com.tangem.tap.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* BlockchainSDK logger implementation
*
* @property settingsRepository settings repository
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class TangemBlockchainSDKLogger(
private val settingsRepository: SettingsRepository,
private val dispatchers: CoroutineDispatcherProvider,
) : BlockchainSDKLogger {
private val scope = CoroutineScope(dispatchers.main)
private val mutex = Mutex()
override fun log(level: BlockchainSDKLogger.Level, message: String) {
scope.launch(dispatchers.main) {
mutex.withLock {
settingsRepository.updateAppLogs(message)
}
}
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.tap.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.tap.common.feedback.DefaultFeedbackManagerFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeedbackManagerFeatureTogglesModule {
@Provides
@Singleton
fun provideFeedbackManagerFeatureToggles(
featureTogglesManager: FeatureTogglesManager,
): FeedbackManagerFeatureToggles {
return DefaultFeedbackManagerFeatureToggles(featureTogglesManager)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.di.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.tap.data.TangemBlockchainSDKLogger
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object BlockchainSDKLoggerModule {
@Provides
@Singleton
fun provideBlockchainSDKLogger(
settingsRepository: SettingsRepository,
dispatchers: CoroutineDispatcherProvider,
): BlockchainSDKLogger {
return TangemBlockchainSDKLogger(settingsRepository, dispatchers)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.tap.di.data
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.tap.common.log.TangemCardSDKLogger
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CardSDKLoggerModule {
@Provides
@Singleton
fun provideCardSDKLogger(
settingsRepository: SettingsRepository,
dispatchers: CoroutineDispatcherProvider,
): TangemSdkLogger {
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
)
return TangemCardSDKLogger(
levels = logLevels,
messageFormatter = LogFormat.StairsFormatter(),
settingsRepository = settingsRepository,
dispatchers = dispatchers,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
@ -8,6 +9,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
@ -31,4 +33,10 @@ internal object AppCurrencyDomainModule {
): GetAvailableCurrenciesUseCase {
return GetAvailableCurrenciesUseCase(appCurrencyRepository)
}
@Provides
@ViewModelScoped
fun provideFetchAppCurrenciesUseCase(appCurrencyRepository: AppCurrencyRepository): FetchAppCurrenciesUseCase {
return FetchAppCurrenciesUseCase(appCurrencyRepository)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object OnboardingDomainModule {
@Provides
@Singleton
fun provideWasTwinsOnboardingShownUseCase(
onboardingRepository: OnboardingRepository,
): WasTwinsOnboardingShownUseCase {
return WasTwinsOnboardingShownUseCase(onboardingRepository)
}
@Provides
@Singleton
fun provideSaveTwinsOnboardingShownUseCase(
onboardingRepository: OnboardingRepository,
): SaveTwinsOnboardingShownUseCase {
return SaveTwinsOnboardingShownUseCase(onboardingRepository)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.tokens.*
import com.tangem.domain.txhistory.repository.TxHistoryRepository
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import dagger.Module
@ -34,4 +35,10 @@ internal object TxHistoryDomainModule {
): GetExplorerTransactionUrlUseCase {
return GetExplorerTransactionUrlUseCase(repository = txHistoryRepository)
}
@Provides
@ViewModelScoped
fun providesGetFixedTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetFixedTxHistoryItemsUseCase {
return GetFixedTxHistoryItemsUseCase(repository = txHistoryRepository)
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
@ -44,8 +45,8 @@ class TapWalletManager(
val walletManagerFactory: WalletManagerFactory by lazy {
WalletManagerFactory(
config = blockchainSdkConfig,
accountCreator = store.state.daggerGraphState.get(DaggerGraphState::accountCreator),
blockchainDataStorage = store.state.daggerGraphState.get(DaggerGraphState::blockchainDataStorage),
accountCreator = store.inject(DaggerGraphState::accountCreator),
blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage),
)
}

View file

@ -5,15 +5,14 @@ import com.tangem.common.core.TangemError
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
// TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead
internal class DefaultScanCardProcessor : ScanCardProcessor {
private val isNewCardScanningEnabled: Boolean
get() = store.state.daggerGraphState
.get(DaggerGraphState::customTokenFeatureToggles)
.isNewCardScanningEnabled
get() = store.inject(DaggerGraphState::customTokenFeatureToggles).isNewCardScanningEnabled
override suspend fun scan(
cardId: String?,

View file

@ -13,12 +13,8 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
@ -26,6 +22,10 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -165,7 +165,10 @@ internal object LegacyScanProcessor {
navigateTo(appScreen) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }

View file

@ -9,9 +9,9 @@ import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.scanCard.chains.*
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
@ -24,7 +24,7 @@ internal object UseCaseScanProcessor {
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse> {
val scanCardUseCase = store.state.daggerGraphState.get(DaggerGraphState::scanCardUseCase)
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository)
.fold(
ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) },
@ -45,14 +45,14 @@ internal object UseCaseScanProcessor {
) = progressScope(onProgressStateChange) {
onScanStateChange(true)
val scanCardUseCase = store.state.daggerGraphState.get(DaggerGraphState::scanCardUseCase)
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
val chains = buildList {
add(ScanningFinishedChain { onScanStateChange(false) })
if (analyticsEvent != null) {
add(AnalyticsChain(analyticsEvent))
}
add(DisclaimerChain(store, disclaimerWillShow))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager, preferencesStorage))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}
scanCardUseCase(cardId, afterScanChains = chains)

View file

@ -5,7 +5,6 @@ import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
@ -13,6 +12,7 @@ import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -20,6 +20,7 @@ import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import org.rekotlin.Store
@ -32,14 +33,12 @@ import org.rekotlin.Store
*
* @param store the [Store] that holds the state of the app.
* @param tapWalletManager manager responsible for handling operations related to the Wallet.
* @param preferencesStore data source for user preferences.
*
* @see Chain for more information about the Chain interface.
*/
class CheckForOnboardingChain(
private val store: Store<AppState>,
private val tapWalletManager: TapWalletManager,
private val preferencesStore: PreferencesDataSource,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(
@ -63,8 +62,12 @@ class CheckForOnboardingChain(
}
else -> {
Analytics.setContext(previousChainResult)
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
// If twins was twinned previously but twins welcome not shown
if (previousChainResult.twinsIsTwinned() && !preferencesStore.wasTwinsOnboardingShown()) {
if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)

View file

@ -32,9 +32,12 @@ import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.files.ReadFilesTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.preferencesStorage
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import kotlin.collections.set
@ -186,23 +189,28 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
mainScope.launch {
val activationInProgress = store.inject(DaggerGraphState::cardRepository)
.isActivationInProgress(card.cardId)
@Suppress("ComplexCondition")
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() && activationInProgress) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
primaryCard = linkingResult.data
deriveKeysIfNeeded(card, session, callback)
}
is CompletionResult.Failure -> {
deriveKeysIfNeeded(card, session, callback)
@Suppress("ComplexCondition")
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() &&
activationInProgress
) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
primaryCard = linkingResult.data
deriveKeysIfNeeded(card, session, callback)
}
is CompletionResult.Failure -> {
deriveKeysIfNeeded(card, session, callback)
}
}
}
} else {
deriveKeysIfNeeded(card, session, callback)
}
} else {
deriveKeysIfNeeded(card, session, callback)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.domain.userWalletList
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
internal class DefaultUserWalletsListManagerFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : UserWalletsListManagerFeatureToggles {
override val isGeneralManagerEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED")
}

View file

@ -0,0 +1,23 @@
package com.tangem.tap.domain.userWalletList.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.tap.domain.userWalletList.DefaultUserWalletsListManagerFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UserWalletsListManagerFeatureTogglesModule {
@Provides
@Singleton
fun provideUserWalletsListManagerFeatureToggles(
featureTogglesManager: FeatureTogglesManager,
): UserWalletsListManagerFeatureToggles {
return DefaultUserWalletsListManagerFeatureToggles(featureTogglesManager)
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.tap.domain.userWalletList.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.authentication.AuthenticatedStorage
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.*
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UserWalletsListManagerModule {
@Provides
@Singleton
fun provideGeneralUserWalletsListManager(
@ApplicationContext applicationContext: Context,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsListManager {
return GeneralUserWalletsListManager(
runtimeUserWalletsListManager = RuntimeUserWalletsListManager(),
biometricUserWalletsListManager = createBiometricUserWalletsListManager(applicationContext),
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
private fun createBiometricUserWalletsListManager(applicationContext: Context): UserWalletsListManager {
val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(KotlinJsonAdapterFactory())
.build()
val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = USER_WALLETS_STORAGE_NAME,
),
)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
featureStorage = secureStorage,
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
),
keystoreManager = DelegatedKeystoreManager(
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
),
)
val keysRepository = BiometricUserWalletsKeysRepository(
moshi = moshi,
secureStorage = secureStorage,
authenticatedStorage = authenticatedStorage,
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(secureStorage = secureStorage)
return BiometricUserWalletsListManager(
keysRepository = keysRepository,
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
)
}
}

View file

@ -21,7 +21,7 @@ import com.tangem.tap.domain.userWalletList.utils.json.*
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider
private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
internal const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
fun UserWalletsListManager.Companion.provideBiometricImplementation(
applicationContext: Context,

View file

@ -0,0 +1,146 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.CompletionResult
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
/**
* General implementation of [UserWalletsListManager] that helps to switch between Runtime and Biometric
* implementations.
*
* @property runtimeUserWalletsListManager runtime user wallets list manager
* @property biometricUserWalletsListManager biometric user wallets list manager
* @property appPreferencesStore app preferences store
* @property dispatchers coroutine dispatcher provider
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalCoroutinesApi::class)
internal class GeneralUserWalletsListManager(
private val runtimeUserWalletsListManager: UserWalletsListManager,
private val biometricUserWalletsListManager: UserWalletsListManager,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : UserWalletsListManager.Lockable {
private val applicationScope = CoroutineScope(dispatchers.io)
private val implementation = MutableStateFlow(runtimeUserWalletsListManager)
init {
subscribeOnCurrentManager()
}
override val userWallets: Flow<List<UserWallet>>
get() = implementation.flatMapLatest { it.userWallets }
override val selectedUserWallet: Flow<UserWallet>
get() = implementation.flatMapLatest { it.selectedUserWallet }
override val selectedUserWalletSync: UserWallet?
get() = implementation.value.selectedUserWalletSync
override val hasUserWallets: Boolean
get() = implementation.value.hasUserWallets
override val walletsCount: Int
get() = implementation.value.walletsCount
override val isLocked: Flow<Boolean>
get() = implementation.flatMapLatest {
if (it is UserWalletsListManager.Lockable) {
it.isLocked
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override val isLockedSync: Boolean
get() {
val implementation = implementation.value
return if (implementation is UserWalletsListManager.Lockable) {
implementation.isLockedSync
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return implementation.value.select(userWalletId)
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return implementation.value.save(userWallet, canOverride)
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return implementation.value.update(userWalletId, update)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
return implementation.value.delete(userWalletIds)
}
override suspend fun clear(): CompletionResult<Unit> {
return implementation.value.clear()
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return implementation.value.get(userWalletId)
}
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> {
val implementation = implementation.value
return if (implementation is UserWalletsListManager.Lockable) {
implementation.unlock()
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override fun lock() {
val implementation = implementation.value
return if (implementation is UserWalletsListManager.Lockable) {
implementation.lock()
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
private fun subscribeOnCurrentManager() {
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
.distinctUntilChanged()
.onEach { shouldSaveUserWallets ->
val manager = if (shouldSaveUserWallets) {
biometricUserWalletsListManager.copyFrom(runtimeUserWalletsListManager)
} else {
runtimeUserWalletsListManager.copyFrom(biometricUserWalletsListManager)
}
Timber.d("Switch to ${manager::class.java.simpleName}")
implementation.value = manager
}
.flowOn(dispatchers.io)
.launchIn(applicationScope)
}
/** Copy data from [old] manager and clean it */
private suspend fun UserWalletsListManager.copyFrom(old: UserWalletsListManager): UserWalletsListManager {
old.selectedUserWalletSync?.let { this.save(it) }
old.clear()
return this
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder
@ -59,7 +60,7 @@ class WalletConnectSdkHelper {
val decimals = wallet.blockchain.decimals()
val value = (transaction.value ?: "0").hexToBigDecimal()
?.movePointLeft(decimals) ?: return null
.movePointLeft(decimals) ?: return null
val gasLimit = getGasLimitFromTx(value, walletManager, transaction)
@ -118,8 +119,7 @@ class WalletConnectSdkHelper {
private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
@ -172,7 +172,7 @@ class WalletConnectSdkHelper {
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
signer = CommonSigner(
tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk,
tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk,
cardId = cardId,
),
)

View file

@ -10,6 +10,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -31,8 +32,8 @@ class DefaultCustomTokenInteractor(
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository)
val networksRepository = store.inject(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}

View file

@ -87,7 +87,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
currentCryptoCurrencies = getSelectedWalletSyncUseCase().fold(
ifLeft = { emptyList() },
ifRight = { selectedWallet ->
getCurrenciesUseCase(selectedWallet.walletId).fold(
getCurrenciesUseCase.getSync(selectedWallet.walletId).fold(
ifLeft = { emptyList() },
ifRight = { it },
)

View file

@ -24,10 +24,7 @@ import com.tangem.domain.wallets.legacy.isLockedSync
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -122,28 +119,24 @@ class DetailsMiddleware {
doBeforeErase()
tangemSdkManager.resetToFactorySettings(card.cardId, true)
.flatMap { userWalletsListManager.delete(listOfNotNull(userWalletId)) }
.flatMap { tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) }
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
} else {
if (userWalletsListManager.isLockedSync) {
userWalletsListManager.delete(listOfNotNull(userWalletId))
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
delay(timeMillis = 500)
userWalletsListManager.delete(listOfNotNull(userWalletId))
store.onUserWalletSelected(selectedUserWallet)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
} else {
userWalletsListManager.delete(listOfNotNull(userWalletId))
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
.doOnFailure { error ->
userWalletsListManager.delete(listOfNotNull(userWalletId))
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.FactoryResetFinished(error))
}
@ -268,7 +261,7 @@ class DetailsMiddleware {
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository)
val repository = store.inject(DaggerGraphState::appThemeModeRepository)
scope.launch {
repository.changeAppThemeMode(appThemeMode)
@ -278,7 +271,7 @@ class DetailsMiddleware {
}
private fun changeBalanceHiding(hideBalance: Boolean) {
val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository)
val repository = store.inject(DaggerGraphState::balanceHidingRepository)
scope.launch {
val newState = repository.getBalanceHidingSettings().copy(
@ -292,7 +285,8 @@ class DetailsMiddleware {
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
// Nothing to change
val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
if (walletsRepository.shouldSaveUserWalletsSync() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
@ -360,6 +354,19 @@ class DetailsMiddleware {
private suspend fun saveCurrentWallet(
scanResponse: ScanResponse?,
enableAccessCodesSaving: Boolean,
): CompletionResult<Unit> {
val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
return if (featureToggles.isGeneralManagerEnabled) {
saveCurrentWalletByNewWay(scanResponse, enableAccessCodesSaving)
} else {
saveCurrentWalletByOldWay(scanResponse, enableAccessCodesSaving)
}
}
private suspend fun saveCurrentWalletByOldWay(
scanResponse: ScanResponse?,
enableAccessCodesSaving: Boolean,
): CompletionResult<Unit> {
val userWallet = userWalletsListManager.selectedUserWalletSync
?: scanResponse?.let { UserWalletBuilder(it).build() }
@ -381,8 +388,26 @@ class DetailsMiddleware {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
preferencesStorage.shouldShowSaveUserWalletScreen = false
store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
.saveShouldSaveUserWallets(item = true)
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
}
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
}
private suspend fun saveCurrentWalletByNewWay(
scanResponse: ScanResponse?,
enableAccessCodesSaving: Boolean,
): CompletionResult<Unit> {
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
return if (enableAccessCodesSaving) {
saveAccessCodes(scanResponse)
} else {
CompletionResult.Success(Unit)
}
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
}
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
@ -390,13 +415,22 @@ class DetailsMiddleware {
}
private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult<Unit> {
val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
return if (featureToggles.isGeneralManagerEnabled) {
deleteSavedWalletsAndAccessCodesByNewWay()
} else {
deleteSavedWalletsAndAccessCodesByOldWay()
}
}
private suspend fun deleteSavedWalletsAndAccessCodesByOldWay(): CompletionResult<Unit> {
return userWalletsListManager.clear()
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
deleteSavedAccessCodes()
updateUserWalletsListManager(enableUserWalletsSaving = false)
store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
.saveShouldSaveUserWallets(item = false)
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
}
@ -405,12 +439,22 @@ class DetailsMiddleware {
}
}
private suspend fun deleteSavedWalletsAndAccessCodesByNewWay(): CompletionResult<Unit> {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
return CompletionResult.Success(Unit)
}
private fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult<Unit> {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On))
preferencesStorage.shouldSaveAccessCodes = true
store.state.daggerGraphState
.get(DaggerGraphState::cardSdkConfigRepository)
store.inject(DaggerGraphState::cardSdkConfigRepository)
.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true)
return CompletionResult.Success(Unit)
@ -422,8 +466,7 @@ class DetailsMiddleware {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off))
preferencesStorage.shouldSaveAccessCodes = false
store.state.daggerGraphState
.get(DaggerGraphState::cardSdkConfigRepository)
store.inject(DaggerGraphState::cardSdkConfigRepository)
.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false)
}
.doOnFailure { error ->
@ -482,7 +525,7 @@ class DetailsMiddleware {
}
private fun scanCard(state: DetailsState) = scope.launch {
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor)
store.inject(DaggerGraphState::scanCardProcessor)
.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
@ -512,7 +555,7 @@ class DetailsMiddleware {
}
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository)
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy()
@ -521,7 +564,7 @@ class DetailsMiddleware {
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes,
)
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan(
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsEvent = Basic.CardWasScanned(CoreAnalyticsParam.ScannedFrom.MyWallets),
onWalletNotCreated = {
// No need to rollback policy, continue with the policy set before the card scan

View file

@ -4,9 +4,11 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.flow.firstOrNull
@ -76,12 +78,12 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
selectedAppCurrency = store.state.globalState.appCurrency,
selectedThemeMode = runBlocking {
store.state.daggerGraphState
.get { appThemeModeRepository }.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT
store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
?: AppThemeMode.DEFAULT
},
isHidingEnabled = runBlocking {
store.state.daggerGraphState
.get { balanceHidingRepository }.getBalanceHidingSettings().isHidingEnabledInSettings
store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings
},
),
)

View file

@ -20,6 +20,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect.BnbHelper
@ -47,9 +48,9 @@ import timber.log.Timber
class WalletConnectMiddleware {
private var walletConnectManager = WalletConnectManager()
private val walletConnectInteractor: WalletConnectInteractor
get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectInteractor)
get() = store.inject(DaggerGraphState::walletConnectInteractor)
private val walletConnectRepository: WalletConnectRepository
get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectRepository)
get() = store.inject(DaggerGraphState::walletConnectRepository)
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
@ -387,8 +388,7 @@ class WalletConnectMiddleware {
}
private suspend fun getWalletManagers(): List<WalletManager> {
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList()
return walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
@ -407,7 +407,7 @@ class WalletConnectMiddleware {
}
private suspend fun getAvailableEvmBlockchains(userWalletId: UserWalletId): List<Blockchain> {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository)
return currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
.asSequence()
@ -496,8 +496,7 @@ class WalletConnectMiddleware {
style = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
)?.rawPath
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
@ -511,8 +510,7 @@ class WalletConnectMiddleware {
}
private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor, userWallet: UserWallet): List<Account> {
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getStoredWalletManagers(userWallet.walletId).mapNotNull {
val wallet = it.wallet
val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull(

View file

@ -262,7 +262,8 @@ private class DetailsScreenStateProvider : CollectionPreviewParameterProvider<De
SettingsItem.LinkMoreCards({}).let(::add)
SettingsItem.CardSettings({}).let(::add)
SettingsItem.AppSettings({}).let(::add)
SettingsItem.Chat({}).let(::add)
// removed chat in task [REDACTED_TASK_KEY]
// SettingsItem.Chat({}).let(::add)
SettingsItem.SendFeedback({}).let(::add)
SettingsItem.ReferralProgram({}).let(::add)
SettingsItem.TermsOfService({}).let(::add)
@ -279,7 +280,8 @@ private class DetailsScreenStateProvider : CollectionPreviewParameterProvider<De
SettingsItem.AddWallet(showProgress = true, {}).let(::add)
SettingsItem.CardSettings({}).let(::add)
SettingsItem.AppSettings({}).let(::add)
SettingsItem.Chat({}).let(::add)
// removed chat in task [REDACTED_TASK_KEY]
// SettingsItem.Chat({}).let(::add)
SettingsItem.SendFeedback({}).let(::add)
SettingsItem.TermsOfService({}).let(::add)
}.toImmutableList(),

View file

@ -89,7 +89,7 @@ internal sealed class SettingsItem(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_comment,
title = resourceReference(R.string.details_row_title_send_feedback),
title = resourceReference(R.string.details_row_title_contact_to_support),
)
data class ReferralProgram(

View file

@ -17,7 +17,6 @@ import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.DetailsAction
@ -89,8 +88,9 @@ internal class DetailsViewModel(
SettingsItem.AppSettings(::navigateToAppSettings)
.let(::add)
SettingsItem.Chat(::navigateToChat)
.let(::add)
// removed chat in task [REDACTED_TASK_KEY]
// SettingsItem.Chat(::navigateToChat)
// .let(::add)
SettingsItem.SendFeedback(::sendFeedback)
.let(::add)
@ -141,11 +141,6 @@ internal class DetailsViewModel(
store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
}
private fun navigateToChat() {
Analytics.send(Settings.ButtonChat())
store.dispatchOnMain(GlobalAction.OpenChat(SupportInfo()))
}
private fun navigateToAppSettings() {
Analytics.send(Settings.ButtonAppSettings())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppSettings))

View file

@ -8,8 +8,8 @@ import android.net.Uri
interface Disclaimer {
fun type(): DisclaimerType
fun getUri(): Uri
fun accept()
fun isAccepted(): Boolean
suspend fun accept()
suspend fun isAccepted(): Boolean
}
abstract class BaseDisclaimer(
@ -18,32 +18,28 @@ abstract class BaseDisclaimer(
val baseUrl = "https://tangem.com"
override fun accept() {
dataProvider.storage().accept(getPreferenceKey())
override suspend fun accept() {
dataProvider.accept()
}
override fun isAccepted(): Boolean = dataProvider.storage().isAccepted(getPreferenceKey())
protected open fun getPreferenceKey(): String = type().name
override suspend fun isAccepted(): Boolean = dataProvider.isAccepted()
}
class DummyDisclaimer : Disclaimer {
override fun type(): DisclaimerType = DisclaimerType.Tangem
override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html")
override fun accept() {}
override fun isAccepted(): Boolean = false
override suspend fun accept() {}
override suspend fun isAccepted(): Boolean = false
}
class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun type(): DisclaimerType = DisclaimerType.Tangem
override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html")
override fun getPreferenceKey(): String = "tangem_tos_accepted"
}
class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun type(): DisclaimerType = DisclaimerType.Start2Coin
override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion()))
override fun getPreferenceKey(): String = "start2Coin_tos_accepted_${getRegion()}"
@Suppress("ComplexMethod")
private fun filename(languageCode: String, regionCode: String?): String {

View file

@ -1,12 +1,11 @@
package com.tangem.tap.features.disclaimer
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
/**
[REDACTED_AUTHOR]
*/
interface DisclaimerDataProvider {
fun getLanguage(): String
fun getCardId(): String
fun storage(): DisclaimerPrefStorage
suspend fun accept()
suspend fun isAccepted(): Boolean
}

View file

@ -2,8 +2,9 @@ package com.tangem.tap.features.disclaimer
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.models.scan.CardDTO
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
import com.tangem.tap.preferencesStorage
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import java.util.Locale
/**
@ -25,7 +26,7 @@ enum class DisclaimerType {
}
fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer {
val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId)
val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId, this)
return when (this) {
DisclaimerType.Tangem -> TangemDisclaimer(dataProvider)
DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider)
@ -34,10 +35,24 @@ fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer {
fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this)
private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider {
private fun provideDisclaimerDataProvider(cardId: String, disclaimerType: DisclaimerType): DisclaimerDataProvider {
val cardRepository = store.inject(DaggerGraphState::cardRepository)
return object : DisclaimerDataProvider {
override fun getLanguage(): String = Locale.getDefault().language
override fun getCardId(): String = cardId
override fun storage(): DisclaimerPrefStorage = preferencesStorage.disclaimerPrefStorage
override suspend fun accept() {
when (disclaimerType) {
DisclaimerType.Tangem -> cardRepository.acceptTangemTOS()
DisclaimerType.Start2Coin -> cardRepository.acceptStart2CoinTOS(cardId)
}
}
override suspend fun isAccepted(): Boolean {
return when (disclaimerType) {
DisclaimerType.Tangem -> cardRepository.isTangemTOSAccepted()
DisclaimerType.Start2Coin -> cardRepository.isStart2CoinTOSAccepted(cardId)
}
}
}
}

View file

@ -3,7 +3,9 @@ package com.tangem.tap.features.disclaimer.redux
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.mainScope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -26,9 +28,11 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
is DisclaimerAction.AcceptDisclaimer -> {
state.disclaimer.accept()
store.dispatch(NavigationAction.PopBackTo())
state.callback?.onAccept?.invoke()
mainScope.launch {
state.disclaimer.accept()
store.dispatch(NavigationAction.PopBackTo())
state.callback?.onAccept?.invoke()
}
}
is DisclaimerAction.OnBackPressed -> {
store.dispatch(NavigationAction.PopBackTo())

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.View
import android.view.View.OVER_SCROLL_NEVER
import android.webkit.WebView
import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.navigation.AppScreen
@ -21,6 +22,7 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDisclaimerBinding
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubscriber<DisclaimerState> {
@ -92,10 +94,12 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
store.dispatch(DisclaimerAction.OnBackPressed)
}
override fun newState(state: DisclaimerState) = with(binding) {
if (activity == null || view == null) return
override fun newState(state: DisclaimerState) {
return with(binding) {
if (activity == null || view == null) return
updateUiVisibility(state.disclaimer, state.progressState)
updateUiVisibility(state.disclaimer, state.progressState)
}
}
private fun updateUiVisibility(disclaimer: Disclaimer, progressState: ProgressState?) = with(binding) {
@ -112,7 +116,9 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
groupError.hide()
groupLoading.hide()
webView.show()
groupAccept.show(!disclaimer.isAccepted())
lifecycleScope.launch {
groupAccept.show(!disclaimer.isAccepted())
}
}
ProgressState.Error -> {
root.beginDelayedTransition()

View file

@ -6,18 +6,15 @@ 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.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
@ -79,11 +76,11 @@ private fun handleHomeAction(action: Action) {
}
private suspend fun readCard(analyticsEvent: AnalyticsEvent?) {
store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes,
)
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan(
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsEvent = analyticsEvent,
onProgressStateChange = { showProgress ->
if (showProgress) {

View file

@ -5,11 +5,13 @@ import androidx.lifecycle.viewModelScope
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -20,6 +22,8 @@ internal class MainViewModel @Inject constructor(
private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase,
private val listenToFlipsUseCase: ListenToFlipsUseCase,
private val reduxNavController: ReduxNavController,
private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase,
private val dispatchers: CoroutineDispatcherProvider,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
@ -33,11 +37,18 @@ internal class MainViewModel @Inject constructor(
val state: StateFlow<MainScreenState> = stateHolder.stateFlow
init {
updateAppCurrencies()
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
}
private fun updateAppCurrencies() {
viewModelScope.launch(dispatchers.main) {
fetchAppCurrenciesUseCase.invoke()
}
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
}

View file

@ -15,10 +15,7 @@ import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.*
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.delay
@ -29,26 +26,26 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
object OnboardingHelper {
fun isOnboardingCase(response: ScanResponse): Boolean {
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
suspend fun isOnboardingCase(response: ScanResponse): Boolean {
val onboardingManager = store.state.globalState.onboardingState.onboardingManager
val cardId = response.card.cardId
return when {
response.cardTypesResolver.isTangemTwins() -> {
if (!response.twinsIsTwinned()) {
true
} else {
cardInfoStorage.isActivationInProgress(cardId)
onboardingManager?.isActivationInProgress(cardId) ?: false
}
}
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardInfoStorage.isActivationInProgress(cardId)
val activationInProgress = onboardingManager?.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true
emptyWallets || activationInProgress || backupNotActive
emptyWallets || activationInProgress == true || backupNotActive
}
response.card.wallets.isNotEmpty() -> cardInfoStorage.isActivationInProgress(cardId)
response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false
else -> true
}
}
@ -80,7 +77,7 @@ object OnboardingHelper {
scope.launch {
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.state.daggerGraphState.get(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
proceedWithScanResponse(scanResponse, backupCardsIds)
store.dispatchOnMain(

View file

@ -5,11 +5,12 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.TapError
@ -17,17 +18,17 @@ import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.model.hasPendingTransactions
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import timber.log.Timber
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class OnboardingManager(
var scanResponse: ScanResponse,
private val usedCardsPrefStorage: UsedCardsPrefStorage,
) {
class OnboardingManager(var scanResponse: ScanResponse) {
private val cardRepository: CardRepository = store.inject(DaggerGraphState::cardRepository)
private var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
suspend fun loadArtworkUrl(): String {
@ -76,17 +77,23 @@ class OnboardingManager(
)
}
fun activationStarted(cardId: String) {
usedCardsPrefStorage.activationStarted(cardId)
suspend fun startActivation(cardId: String) {
cardRepository.startCardActivation(cardId)
}
fun activationFinished(cardId: String) {
usedCardsPrefStorage.activationFinished(cardId)
suspend fun finishActivation(cardId: String) {
cardRepository.finishCardActivation(cardId)
}
fun isActivationStarted(cardId: String): Boolean {
return usedCardsPrefStorage.isActivationStarted(cardId)
suspend fun finishActivation(cardIds: List<String>) {
cardRepository.finishCardsActivation(cardIds)
}
suspend fun isActivationStarted(cardId: String): Boolean = cardRepository.isActivationStarted(cardId)
suspend fun isActivationFinished(cardId: String): Boolean = cardRepository.isActivationFinished(cardId)
suspend fun isActivationInProgress(cardId: String): Boolean = cardRepository.isActivationInProgress(cardId)
}
data class OnboardingWalletBalance(

View file

@ -22,7 +22,8 @@ class OnboardingMenuProvider : MenuProvider {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.menu_item_chat_support -> {
Analytics.send(Onboarding.ButtonChat())
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
true
}
else -> false

View file

@ -20,6 +20,7 @@ import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -57,8 +58,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
when (action) {
is OnboardingNoteAction.Init -> {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingNoteAction.LoadCardArtwork -> {
@ -86,8 +89,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
}
OnboardingNoteStep.Done -> {
Analytics.send(Onboarding.Finished())
onboardingManager.activationFinished(card.cardId)
postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
}
else -> Unit
}
@ -101,7 +106,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = scanResponse.copy(card = result.data.card)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
}
is CompletionResult.Failure -> Unit

View file

@ -11,6 +11,7 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -43,8 +44,10 @@ private fun handleOtherCardsAction(action: Action) {
when (action) {
is OnboardingOtherCardsAction.Init -> {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingOtherCardsAction.LoadCardArtwork -> {
@ -67,8 +70,10 @@ private fun handleOtherCardsAction(action: Action) {
}
OnboardingOtherCardsStep.Done -> {
Analytics.send(Onboarding.Finished())
onboardingManager.activationFinished(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
}
}
else -> Unit
}
@ -88,7 +93,7 @@ private fun handleOtherCardsAction(action: Action) {
)
val updatedCard = updatedResponse.card
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedCard.cardId)
onboardingManager.startActivation(updatedCard.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))

View file

@ -26,7 +26,7 @@ import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.preferencesStorage
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -75,44 +75,61 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
fun startCardActivation(cardId: String) {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
onboardingManager?.activationStarted(cardId)
scope.launch {
onboardingManager?.startActivation(cardId)
}
}
}
fun finishCardActivation() {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
Analytics.send(Onboarding.Finished())
onboardingManager?.activationFinished(getScanResponse().card.cardId)
twinCardsState.pairCardId?.let { onboardingManager?.activationFinished(it) }
val cardIds = listOfNotNull(getScanResponse().card.cardId, twinCardsState.pairCardId)
if (cardIds.isNotEmpty()) {
mainScope.launch {
onboardingManager?.finishActivation(cardIds)
}
}
}
}
when (action) {
is TwinCardsAction.Init -> {
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return
mainScope.launch {
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
if (preferencesStorage.wasTwinsOnboardingShown()) {
val step = when {
!scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
store.dispatch(TwinCardsAction.SetStepOfScreen(step))
} else {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome))
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
mainScope.launch {
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
val dispatchAction = if (wasTwinsOnboardingShown) {
val step = when {
!scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
TwinCardsAction.SetStepOfScreen(step)
} else {
TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome)
}
store.dispatch(dispatchAction)
}
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
}
}
}
}
@ -120,7 +137,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
when (action.step) {
is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
Analytics.send(Onboarding.Twins.ScreenOpened())
preferencesStorage.saveTwinsOnboardingShown()
scope.launch {
store.inject(DaggerGraphState::saveTwinsOnboardingShownUseCase).invoke()
}
}
is TwinCardsStep.CreateFirstWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
@ -295,7 +315,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
CreateTwinWalletMode.RecreateWallet -> {
scope.launch {
val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
if (walletsRepository.shouldSaveUserWalletsSync()) {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)

View file

@ -27,6 +27,8 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@ -72,8 +74,10 @@ private fun handleWalletAction(action: Action) {
when (action) {
OnboardingWalletAction.Init -> {
ifNotNull(onboardingManager, card) { manager, notNullCard ->
if (!manager.isActivationStarted(notNullCard.cardId)) {
Analytics.send(Onboarding.Started())
mainScope.launch {
if (!manager.isActivationStarted(notNullCard.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
when {
@ -144,8 +148,10 @@ private fun handleWalletAction(action: Action) {
onboardingManager.scanResponse = updatedResponse
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
mainScope.launch {
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatchWithMain(OnboardingWalletAction.ResumeBackup)
}
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
@ -398,8 +404,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(true))
backupService.addBackupCard { result ->
backupService.skipCompatibilityChecks = false
store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk
.config.filter.cardIdFilter = null
store.inject(DaggerGraphState::cardSdkConfigRepository).sdk.config.filter.cardIdFilter = null
store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false))
when (result) {
is CompletionResult.Success -> {
@ -493,11 +498,13 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
backupService.primaryCardId?.let {
Analytics.send(Onboarding.Finished())
finishCardsActivationForDiscardedUnfinishedBackup(it)
mainScope.launch {
backupService.primaryCardId?.let {
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(it)
}
backupService.discardSavedBackup()
}
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
if (backupService.hasIncompletedBackup) {
@ -514,17 +521,21 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
is BackupAction.SkipBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
Analytics.send(Onboarding.Finished())
finishCardActivation(gatherCardIds(backupState, card))
scope.launch {
store.state.globalState.onboardingState.onboardingManager?.finishActivation(
cardIds = gatherCardIds(backupState, card),
)
}
}
is BackupAction.FinishBackup -> {
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
scope.launch {
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
if (scanResponse != null) {
scope.launch {
if (scanResponse != null) {
val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(backupState.backupCardIds.toSet())
.build()
@ -546,16 +557,22 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
)
store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(userWalletsListManager))
}
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) {
null
} else {
it
}
}
// All cardIds may already be activated if the backup was skipped before.
if (notActivatedCardIds.isEmpty()) return@launch
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
}
val notActivatedCardIds = gatherCardIds(backupState, card)
.mapNotNull { if (cardActivationIsFinished(it)) null else it }
// All cardIds may already be activated if the backup was skipped before.
if (notActivatedCardIds.isEmpty()) return
Analytics.send(Onboarding.Finished())
finishCardActivation(notActivatedCardIds)
}
is BackupAction.ResetBackupCard -> {
@ -566,36 +583,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
private fun cardActivationIsFinished(cardId: String): Boolean {
return preferencesStorage.usedCardsPrefStorage.isActivationFinished(cardId)
}
/**
* Standard Wallet cards start activation at OnboardingWalletAction.CreateWallet
*/
private fun startCardActivation(scanResponse: ScanResponse) {
preferencesStorage.usedCardsPrefStorage.activationStarted(scanResponse.card.cardId)
}
/**
* Standard Wallet cards finish activation at BackupAction.SkipBackup and BackupAction.FinishBackup
*/
internal fun finishCardActivation(cardIds: List<String>) {
cardIds.forEach { cardId ->
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
}
internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List<String> {
return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.filterNotNull()
.distinct()
}
private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) {
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
private fun handleOnBackPressed(state: OnboardingWalletState) {
when (state.backupState.backupStep) {
BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,

View file

@ -501,7 +501,8 @@ class OnboardingWalletFragment :
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
onBack = ::legacyOnBackHandler,
onOpenChat = {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
},
onOpenUriClick = { uri ->
store.dispatchOpenUrl(uri.toString())

View file

@ -17,8 +17,9 @@ object WalletActivationErrorDialog {
setTitle(context.getString(R.string.onboarding_activation_error_title))
setMessage(context.getString(R.string.onboarding_activation_error_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
setNegativeButton(R.string.chat_button_title) { _, _ ->
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
}
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)

View file

@ -16,6 +16,8 @@ internal sealed interface SaveWalletAction : Action {
data class Error(val error: TangemError) : SaveWalletAction
}
object AllowToUseBiometrics : SaveWalletAction
object Dismiss : SaveWalletAction
object CloseError : SaveWalletAction

View file

@ -18,6 +18,7 @@ import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
@ -42,6 +43,7 @@ internal class SaveWalletMiddleware {
private fun handleAction(action: SaveWalletAction, state: SaveWalletState) {
when (action) {
is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state)
is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state)
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown()
is SaveWalletAction.Dismiss -> dismiss(state)
@ -96,11 +98,16 @@ internal class SaveWalletMiddleware {
.build()
?: return@launch
provideLockableUserWalletsListManagerIfNot()
val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
if (featureToggles.isGeneralManagerEnabled) {
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
} else {
provideLockableUserWalletsListManagerIfNot()
}
val isFirstSavedWallet = !userWalletsListManager.hasUserWallets
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
saveAccessCodeIfNeeded(accessCode = state.backupInfo?.accessCode, cardsInWallet = userWallet.cardsInWallet)
.flatMap {
// Save wallet only at first time (SaveWalletBottomSheet).
// Otherwise (Example, add new wallet in Details) userWalletsListManager.wallets subscribers will
@ -116,16 +123,14 @@ internal class SaveWalletMiddleware {
store.dispatchWithMain(SaveWalletAction.Save.Error(error))
}
.doOnSuccess {
store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
.saveShouldSaveUserWallets(item = true)
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
// Enable saving access codes only if this is the first time user save the wallet
if (isFirstSavedWallet) {
preferencesStorage.shouldSaveAccessCodes = true
store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository)
.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
}
store.dispatchOnMain(SaveWalletAction.Save.Success)
@ -141,6 +146,46 @@ internal class SaveWalletMiddleware {
}
}
private fun allowToUseBiometrics(state: SaveWalletState) {
val scanResponse = state.backupInfo?.scanResponse
?: store.state.globalState.scanResponse
?: return
if (state.backupInfo != null) {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On))
} else {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
}
scope.launch {
val userWallet = userWalletsListManager.selectedUserWalletSync
?: UserWalletBuilder(scanResponse)
.backupCardsIds(state.backupInfo?.backupCardsIds)
.build()
?: return@launch
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
saveAccessCodeIfNeeded(accessCode = state.backupInfo?.accessCode, cardsInWallet = userWallet.cardsInWallet)
.flatMap {
userWalletsListManager.save(userWallet, canOverride = true)
}
.doOnFailure { error ->
store.dispatchWithMain(SaveWalletAction.Save.Error(error))
}
.doOnSuccess {
preferencesStorage.shouldSaveAccessCodes = true
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
store.dispatchOnMain(SaveWalletAction.Save.Success)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
}
}
}
private suspend fun provideLockableUserWalletsListManagerIfNot() {
if (store.state.globalState.userWalletsListManager?.isLockable == true) return

View file

@ -21,9 +21,9 @@ internal object SaveWalletReducer {
backupCardsIds = action.backupCardsIds,
),
)
is SaveWalletAction.Save -> state.copy(
isSaveInProgress = true,
)
is SaveWalletAction.Save,
is SaveWalletAction.AllowToUseBiometrics,
-> state.copy(isSaveInProgress = true)
is SaveWalletAction.Save.Error -> state.copy(
error = action.error,
isSaveInProgress = false,

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.saveWallet.ui
import androidx.lifecycle.ViewModel
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.details.ui.cardsettings.TextReference
@ -19,6 +20,7 @@ import javax.inject.Inject
@HiltViewModel
internal class SaveWalletViewModel @Inject constructor(
private val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), StoreSubscriber<SaveWalletState> {
private val stateInternal = MutableStateFlow(SaveWalletScreenState())
@ -31,7 +33,12 @@ internal class SaveWalletViewModel @Inject constructor(
fun saveWallet() {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
store.dispatch(SaveWalletAction.Save)
if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) {
store.dispatch(SaveWalletAction.AllowToUseBiometrics)
} else {
store.dispatch(SaveWalletAction.Save)
}
}
fun cancelOrClose() {

View file

@ -24,10 +24,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.analytics.events.Token.Send.SelectedCurrency.CurrencyType
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -211,7 +208,7 @@ private fun sendTransaction(
// return@launch
// }
val tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk
val tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk
val linkedTerminalState = tangemSdk.config.linkedTerminal
if (card.isStart2Coin) {
tangemSdk.config.linkedTerminal = false

View file

@ -18,7 +18,7 @@ object RequestFeeErrorDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_fee_error)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.alert_button_send_feedback) { _, _ ->
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }

View file

@ -30,7 +30,7 @@ object SendTransactionFailsDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.alert_button_send_feedback) { _, _ ->
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_cancel) { _, _ -> }

View file

@ -16,6 +16,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import timber.log.Timber
@ -49,7 +50,11 @@ internal class TokensListMigration(
is Either.Right -> {
currentUserWallet = selectedWalletEither.value
when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) {
when (
val currenciesEither = getCurrenciesUseCase.getSync(
userWalletId = selectedWalletEither.value.walletId,
)
) {
is Either.Left -> {
Timber.e(currenciesEither.value.toString())
TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList())
@ -136,8 +141,8 @@ internal class TokensListMigration(
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)

View file

@ -47,9 +47,7 @@ object TradeCryptoMiddleware {
}
private val isSendRedesignedEnabled: Boolean
get() = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::sendFeatureToggles,
).isRedesignedSendEnabled
get() = store.inject(getDependency = DaggerGraphState::sendFeatureToggles).isRedesignedSendEnabled
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
@ -106,8 +104,7 @@ object TradeCryptoMiddleware {
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
@ -175,8 +172,7 @@ object TradeCryptoMiddleware {
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
@ -234,8 +230,7 @@ object TradeCryptoMiddleware {
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,

View file

@ -8,6 +8,7 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
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
import com.tangem.domain.common.util.cardTypesResolver
@ -16,9 +17,9 @@ import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
@ -169,10 +170,10 @@ internal class WelcomeMiddleware {
}
private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) {
store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes,
)
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan(
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn),
onSuccess = { scanResponse ->
scope.launch { onCardScanned(scanResponse) }

View file

@ -8,6 +8,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
@ -105,7 +106,7 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa
val signer = TangemSigner(
card = card,
tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk,
tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk,
initialMessage = Message(),
) { signResponse ->
store.dispatch(

View file

@ -19,10 +19,10 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.blockchain.network.ResultChecker
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -40,7 +40,6 @@ import java.math.RoundingMode
@Suppress("LargeClass")
class TransactionManagerImpl(
private val appStateHolder: AppStateHolder,
private val analytics: AnalyticsEventHandler,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
) : TransactionManager {
@ -119,7 +118,10 @@ class TransactionManagerImpl(
override fun getExplorerTransactionLink(networkId: String, txAddress: String): String {
val blockchain = Blockchain.fromNetworkId(networkId) ?: error("blockchain not found")
return blockchain.getExploreTxUrl(txAddress)
return when (val txUrlState = blockchain.getExploreTxUrl(txAddress)) {
TxExploreState.Unsupported -> ""
is TxExploreState.Url -> txUrlState.url
}
}
override fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
@ -144,10 +146,6 @@ class TransactionManagerImpl(
}
}
override fun getNativeTokenDecimals(networkId: String): Int {
return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found")
}
override suspend fun updateWalletManager(networkId: String, derivationPath: String?) {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
getActualWalletManager(blockchain, derivationPath).update()
@ -205,7 +203,6 @@ class TransactionManagerImpl(
return ProxyNetworkInfo(
name = blockchain.fullName,
blockchainId = blockchain.id,
blockchainCurrency = blockchain.currency,
)
}

View file

@ -164,11 +164,6 @@ class UserWalletManagerImpl(
}
}
override fun getNetworkCurrency(networkId: String): String {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return blockchain.currency
}
@Throws(IllegalArgumentException::class)
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val selectedUserWallet = requireNotNull(

View file

@ -1,6 +1,5 @@
package com.tangem.tap.proxy.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -43,13 +42,11 @@ internal object ProxyModule {
@Singleton
fun provideTransactionManager(
appStateHolder: AppStateHolder,
analytics: AnalyticsEventHandler,
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
): TransactionManager {
return TransactionManagerImpl(
appStateHolder = appStateHolder,
analytics = analytics,
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
)

View file

@ -8,10 +8,16 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
@ -55,11 +61,10 @@ data class DaggerGraphState(
val currenciesRepository: CurrenciesRepository? = null,
val blockchainDataStorage: BlockchainDataStorage? = null,
val accountCreator: AccountCreator? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {
return requireNotNull(getDependency()) {
"${T::class.simpleName} isn't initialized "
}
}
}
val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles? = null,
val generalUserWalletsListManager: UserWalletsListManager? = null,
val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null,
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
val cardRepository: CardRepository? = null,
val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null,
) : StateType