Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-29 13:50:16 +00:00
commit 6d0b295ca3
219 changed files with 3720 additions and 1687 deletions

View file

@ -9,6 +9,10 @@ plugins {
id("configuration")
}
android {
namespace = "com.tangem.wallet"
}
configurations.all {
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
exclude(group = "com.github.komputing.kethereum")
@ -56,6 +60,7 @@ dependencies {
implementation(projects.domain.transaction)
implementation(projects.domain.analytics)
implementation(projects.domain.visa)
implementation(projects.domain.onboarding)
implementation(projects.common)
implementation(projects.core.analytics)
@ -84,6 +89,7 @@ dependencies {
implementation(projects.data.transaction)
implementation(projects.data.visa)
implementation(projects.data.promo)
implementation(projects.data.onboarding)
/** Features */
implementation(projects.features.onboarding)
@ -112,6 +118,7 @@ dependencies {
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.core.splashScreen)
implementation(deps.androidx.appCompat)
implementation(deps.androidx.datastore)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.constraintLayout)
implementation(deps.androidx.activity.compose)

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.tangem.wallet">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.NFC" />

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,15 @@ 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.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 +159,21 @@ 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
// endregion Injected
override fun onCreate() {
@ -183,8 +202,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 +256,11 @@ internal class TapApplication : Application(), ImageLoaderFactory {
sendFeatureToggles = sendFeatureToggles,
blockchainDataStorage = blockchainDataStorage,
accountCreator = accountCreator,
userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles,
generalUserWalletsListManager = generalUserWalletsListManager,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
cardRepository = cardRepository,
),
),
)

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

@ -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,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
@ -268,7 +265,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 +275,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 +289,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 +358,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 +392,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 +419,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 +443,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 +470,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 +529,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 +559,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 +568,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

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

@ -17,6 +17,7 @@ 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
@ -117,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? {

View file

@ -8,10 +8,15 @@ 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.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 +60,9 @@ 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,
) : StateType

View file

@ -3,6 +3,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_item_chat_support"
android:title="@string/chat_button_title"
android:title="@string/common_support"
app:showAsAction="always" />
</menu>

View file

@ -6,6 +6,10 @@ plugins {
id("configuration")
}
android {
namespace = "com.tangem.datasource"
}
dependencies {
/** Project */

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.datasource"
xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

View file

@ -21,7 +21,7 @@ internal object AppPreferencesStoreModule {
fun provideAppPreferencesStore(
@ApplicationContext appContext: Context,
dispatchers: CoroutineDispatcherProvider,
@NetworkMoshi moshi: Moshi,
@SdkMoshi moshi: Moshi,
): AppPreferencesStore {
return AppPreferencesStore(
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),

View file

@ -45,7 +45,7 @@ class AppPreferencesStore(
* @see getObjectList
* */
inline fun <reified T> MutablePreferences.getObject(key: Preferences.Key<String>): T? {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
val adapter = moshi.adapter(T::class.java)
return this[key]?.let(adapter::fromJson)
}
@ -55,6 +55,15 @@ class AppPreferencesStore(
return this[key]?.let(adapter::fromJson)
}
/** Get list of data [T] by string [key] or default */
inline fun <reified T> MutablePreferences.getObjectListOrDefault(
key: Preferences.Key<String>,
default: List<T>,
): List<T> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return this[key]?.let(adapter::fromJson) ?: default
}
/** Get map with [String] key and value [V] by string [key] from [MutablePreferences] */
inline fun <reified V> MutablePreferences.getObjectMap(key: Preferences.Key<String>): Map<String, V>? {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)

View file

@ -3,10 +3,12 @@ package com.tangem.datasource.local.preferences
import androidx.datastore.preferences.core.*
import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN
/**
* All preferences keys that DataStore<Preferences> is stored.
@ -54,6 +56,12 @@ object PreferencesKeys {
}
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") }
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
@ -65,6 +73,8 @@ internal fun getTapPrefKeysToMigrate(): Set<String> {
FUNDS_FOUND_DATE_KEY,
USER_WAS_INTERACT_WITH_RATING_KEY,
USED_CARDS_INFO_KEY,
WAS_TWINS_ONBOARDING_SHOWN,
IS_TANGEM_TOS_ACCEPTED_KEY,
)
.map(Preferences.Key<*>::name)
.toSet()

View file

@ -6,6 +6,10 @@ plugins {
id("configuration")
}
android {
namespace = "com.tangem.core.featuretoggles"
}
dependencies {
/** DI */

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.core.featuretoggles" />

View file

@ -14,5 +14,9 @@
{
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
"version": "undefined"
},
{
"name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED",
"version": "5.7.0"
}
]

View file

@ -2,4 +2,8 @@ plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.core.res"
}

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.core.res" />

View file

@ -91,8 +91,7 @@
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_label">Комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузка на сеть, объема транзакции и приоритета исполнения. %s</string>
<string name="common_fee_selector_link_description">Подробнее</string>
<string name="common_fee_selector_footer">Сетевые комиссии это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s</string>
<string name="common_fee_selector_option_custom">Свое</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_market">По рынку</string>
@ -111,6 +110,7 @@
<string name="common_ok">OK</string>
<string name="common_origin_card">Основная карта</string>
<string name="common_paste">Вставить</string>
<string name="common_read_more">Подробнее</string>
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
@ -130,6 +130,7 @@
<string name="common_start">Начать</string>
<string name="common_submit">Отправить</string>
<string name="common_success">Успешно</string>
<string name="common_support">Поддержка</string>
<string name="common_swap">Обмен</string>
<string name="common_terms_and_conditions">условия участия</string>
<string name="common_transaction_failed">Ошибка транзакции</string>
@ -181,11 +182,11 @@
<string name="details_row_privacy_policy">Privacy policy</string>
<string name="details_row_subtitle_signed_hashes_format">%s хэшей</string>
<string name="details_row_title_cid">Номер карты</string>
<string name="details_row_title_contact_to_support">Обратиться в поддержку</string>
<string name="details_row_title_create_backup">Добавить еще карты</string>
<string name="details_row_title_currency">Валюта приложения</string>
<string name="details_row_title_flip_to_hide">Скрывать балансы жестом переворота</string>
<string name="details_row_title_issuer">Эмитент</string>
<string name="details_row_title_send_feedback">Отправить отзыв</string>
<string name="details_row_title_signed_hashes">Подписано</string>
<string name="details_title">Подробности</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
@ -452,11 +453,10 @@
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %s</string>
<string name="send_alert_button_request_support">Поддержка</string>
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
<string name="send_alert_fee_coverage_title">Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму?</string>
<string name="send_alert_fee_coverage_subract_text">Вычесть</string>
<string name="send_date_format">%1$s в %2$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
@ -478,14 +478,12 @@
<string name="send_max_fee">Комиссия не превысит </string>
<string name="send_max_fee_footer">Максимальная cумма комиссии</string>
<string name="common_network_fee_title">Сетевая комиссия</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s.</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
<string name="send_notification_fee_too_high_accept">Оставить %s XTZ</string>
<string name="send_notification_fee_too_high_ignore">Отправить все</string>
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
@ -495,6 +493,10 @@
<string name="send_notification_invalid_reserve_amount_title">Сумма отправки не может быть менее %1$s</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_notifiaction_transaction_limit_title">Лимит транзакции</string>
<string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
<string name="send_notification_reduce_by">Уменьшить до %s</string>
<string name="send_notification_reduce_to">Уменьшить на %s</string>
<string name="send_optional_field">Необязательное</string>
<string name="send_recent_transactions">Последние</string>
<string name="send_recipient">Получатель</string>
@ -540,6 +542,7 @@
<string name="swapping_alert_title">Комиссии</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
<string name="swapping_network_fee_warning_content">Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии</string>
<string name="swapping_not_enough_funds_for_fee">Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек.</string>
<string name="swapping_pending_transaction_subtitle">Транзакция в процессе…</string>
<string name="swapping_pending_transaction_title">Подождите</string>

View file

@ -130,7 +130,6 @@
<string name="details_row_title_create_backup">連結更多錢包</string>
<string name="details_row_title_currency">App Currency</string>
<string name="details_row_title_issuer">發行人</string>
<string name="details_row_title_send_feedback">發送反饋</string>
<string name="details_row_title_signed_hashes">簽署</string>
<string name="details_title">更多</string>
<string name="disclaimer_error_loading">檢查您的網路連接或切換到其他網絡</string>

View file

@ -90,7 +90,6 @@
<string name="common_explorer">Explorer</string>
<string name="common_fee_label">Fee</string>
<string name="common_fee_selector_footer">Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s</string>
<string name="common_fee_selector_link_description">Read more</string>
<string name="common_fee_selector_option_custom">Custom</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_market">Market</string>
@ -110,6 +109,7 @@
<string name="common_ok">OK</string>
<string name="common_origin_card">Primary Card</string>
<string name="common_paste">Paste</string>
<string name="common_read_more">Read more</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
@ -129,6 +129,7 @@
<string name="common_start">Start</string>
<string name="common_submit">Submit</string>
<string name="common_success">Success</string>
<string name="common_support">Support</string>
<string name="common_swap">Swap</string>
<string name="common_terms_and_conditions">terms and conditions</string>
<string name="common_transaction_failed">Transaction failed</string>
@ -180,11 +181,11 @@
<string name="details_row_privacy_policy">Privacy policy</string>
<string name="details_row_subtitle_signed_hashes_format">%s hashes</string>
<string name="details_row_title_cid">Card ID</string>
<string name="details_row_title_contact_to_support">Contact to Support</string>
<string name="details_row_title_create_backup">Link More Cards</string>
<string name="details_row_title_currency">App Currency</string>
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_title_issuer">Issuer</string>
<string name="details_row_title_send_feedback">Send Feedback</string>
<string name="details_row_title_signed_hashes">Signed</string>
<string name="details_title">Details</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
@ -393,6 +394,10 @@
<string name="organize_tokens_sort_by_balance">By balance</string>
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="qr_scanner_camera_denied_gallery_button">Select from the gallery</string>
<string name="qr_scanner_camera_denied_settings_button">Settings</string>
<string name="qr_scanner_camera_denied_title">Camera access denied</string>
<string name="qr_scanner_camera_denied_text">You have not given access to your camera</string>
<string name="receive_bottom_sheet_title">%1$s %2$s address on %3$s network</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) on %3$s network</string>
<string name="receive_bottom_sheet_warning_message_full">Send only %s to this address. Sending any other currency will result in its irreversible loss.</string>
@ -450,11 +455,11 @@
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %s</string>
<string name="send_alert_button_request_support">Support</string>
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
<string name="send_alert_fee_coverage_title">Not enough funds to cover the network commission. Subtract the commission from the amount sent?</string>
<string name="send_alert_fee_coverage_subract_text">Subtract</string>
<string name="send_alert_fee_too_low_text">Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?</string>
<string name="send_confirm_label">Confirm</string>
<string name="send_date_format">%1$s at %2$s</string>
<string name="send_destination_hint_address">Address</string>
@ -482,14 +487,12 @@
<string name="send_max_fee_footer">Maximum fee amount</string>
<string name="send_memo_destination_tag_error">Numbers only for Destination Tag</string>
<string name="common_network_fee_title">Network fee</string>
<string name="send_network_fee_warning_content">Sending amount will be reduced to cover the selected fee level</string>
<string name="send_network_fee_warning_content">Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s.</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance</string>
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_high_fee_title">Fee is increased</string>
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
<string name="send_notification_fee_too_high_accept">Reduce by %s XTZ</string>
<string name="send_notification_fee_too_high_ignore">No, send all</string>
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
@ -500,9 +503,11 @@
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_notification_transaction_delay_title">Transaction delays are possible</string>
<string name="send_notifiaction_transaction_limit_title">Transaction limitation</string>
<string name="send_notifiaction_transaction_limit_text">Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
<string name="send_notification_transaction_limit_text">Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
<string name="send_notification_existential_deposit_title">Existential deposit</string>
<string name="send_notification_existential_deposit_text">The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s.</string>
<string name="send_notification_reduce_by">Reduce by %s</string>
<string name="send_notification_reduce_to">Reduce to %s</string>
<string name="send_optional_field">Optional</string>
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
<string name="send_recent_transactions">Recent</string>
@ -553,6 +558,7 @@
<string name="swapping_alert_title">Fees</string>
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
<string name="swapping_network_fee_warning_content">Sending amount will be reduced to cover the selected fee level</string>
<string name="swapping_not_enough_funds_for_fee">Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first.</string>
<string name="swapping_pending_transaction_subtitle">Transaction in progress...</string>
<string name="swapping_pending_transaction_title">Waiting</string>

View file

@ -4,6 +4,10 @@ plugins {
id("configuration")
}
android {
namespace = "com.tangem.core.ui"
}
dependencies {
/** Project - Common */
implementation(projects.common)

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.core.ui" />

View file

@ -1,6 +1,7 @@
package com.tangem.core.ui.components
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.*
@ -84,9 +85,11 @@ fun ResizableText(
var readyToDraw by remember { mutableStateOf(value = false) }
Text(
modifier = modifier.drawWithContent {
if (readyToDraw) drawContent()
},
modifier = modifier
.drawWithContent {
if (readyToDraw) drawContent()
}
.wrapContentHeight(),
text = text,
color = color,
fontSize = fontSize,

View file

@ -163,11 +163,13 @@ private fun PriceChangeInPercent(config: PriceChangeState.Content) {
id = when (type) {
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8
},
),
tint = when (type) {
PriceChangeType.UP -> TangemTheme.colors.icon.accent
PriceChangeType.DOWN -> TangemTheme.colors.icon.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
@ -177,6 +179,7 @@ private fun PriceChangeInPercent(config: PriceChangeState.Content) {
color = when (type) {
PriceChangeType.UP -> TangemTheme.colors.text.accent
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled
},
style = TangemTheme.typography.body2,
)
@ -251,6 +254,14 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr
type = PriceChangeType.UP,
),
),
MarketPriceBlockState.Content(
currencySymbol = "BTC",
price = "98900 $",
priceChangeConfig = PriceChangeState.Content(
valueInPercent = "0.00%",
type = PriceChangeType.NEUTRAL,
),
),
MarketPriceBlockState.Loading(currencySymbol = "BTC"),
MarketPriceBlockState.Error(currencySymbol = "BTC"),
),

View file

@ -5,9 +5,4 @@ sealed class PriceChangeState {
data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState()
object Unknown : PriceChangeState()
}
/** Price changing type */
enum class PriceChangeType {
UP, DOWN
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.components.marketprice
/** Price changing type */
enum class PriceChangeType {
UP, DOWN, NEUTRAL,
}

View file

@ -0,0 +1,21 @@
package com.tangem.core.ui.components.marketprice.utils
import com.tangem.core.ui.components.marketprice.PriceChangeType
import java.math.BigDecimal
import java.math.RoundingMode
object PriceChangeConverter {
fun fromBigDecimal(value: BigDecimal?, scale: Int = 2): PriceChangeType {
val formattedValue = value
?.movePointRight(2)
?.setScale(scale, RoundingMode.HALF_UP)
?: return PriceChangeType.NEUTRAL
return when (formattedValue.signum()) {
1 -> PriceChangeType.UP
-1 -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="8dp"
android:height="8dp"
android:viewportWidth="8"
android:viewportHeight="8">
<path
android:fillColor="#000"
android:pathData="M3,4m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,8C13.061,8 14.078,8.421 14.828,9.172C15.578,9.922 16,10.939 16,12C16,13.061 15.578,14.078 14.828,14.828C14.078,15.579 13.061,16 12,16C10.939,16 9.922,15.579 9.172,14.828C8.421,14.078 8,13.061 8,12C8,10.939 8.421,9.922 9.172,9.172C9.922,8.421 10.939,8 12,8ZM12,10C11.469,10 10.961,10.211 10.586,10.586C10.211,10.961 10,11.47 10,12C10,12.53 10.211,13.039 10.586,13.414C10.961,13.789 11.469,14 12,14C12.53,14 13.039,13.789 13.414,13.414C13.789,13.039 14,12.53 14,12C14,11.47 13.789,10.961 13.414,10.586C13.039,10.211 12.53,10 12,10ZM10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10ZM11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"
android:fillColor="#1E1E1E"/>
</vector>

View file

@ -16,6 +16,11 @@ class JobHolder {
this.job?.cancel()
this.job = job
}
/** Cancel current [job] */
fun cancel() {
job?.cancel()
}
}
fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this)

View file

@ -1,9 +1,13 @@
package com.tangem.data.card
import androidx.datastore.preferences.core.MutablePreferences
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.card.repository.CardRepository
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
@ -21,23 +25,106 @@ internal class DefaultCardRepository(
}
override suspend fun setCardWasScanned(cardId: String) {
appPreferencesStore.editData { mutablePreferences ->
val usedCards: List<UsedCardInfo>? = mutablePreferences.getObjectList(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
)
appPreferencesStore.editUsedCards(cardId) { it.copy(isScanned = true) }
}
val updatedUsedCards = usedCards?.updateCard(cardId)
?: listOf(UsedCardInfo(cardId = cardId, isScanned = true))
override suspend fun startCardActivation(cardId: String) {
appPreferencesStore.editUsedCards(cardId) { it.copy(isActivationStarted = true) }
}
mutablePreferences.setObjectList(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
value = updatedUsedCards,
)
override suspend fun finishCardActivation(cardId: String) {
appPreferencesStore.editUsedCards(cardId) {
it.copy(isActivationStarted = true, isActivationFinished = true)
}
}
private fun List<UsedCardInfo>.updateCard(cardId: String): List<UsedCardInfo> {
val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId, isScanned = true)
return addOrReplace(item = card.copy(isScanned = true), predicate = { it.cardId == cardId })
override suspend fun finishCardsActivation(cardIds: List<String>) {
appPreferencesStore.editData { mutablePreferences ->
val usedCards = mutablePreferences.getUsedCards()
val updatedUsedCards = cardIds.map { cardId ->
usedCards.updateCard(cardId) {
it.copy(isActivationStarted = true, isActivationFinished = true)
}
}
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
}
}
override suspend fun isActivationStarted(cardId: String): Boolean {
return getUsedCardSync(cardId)?.isActivationStarted ?: false
}
override suspend fun isActivationFinished(cardId: String): Boolean {
return getUsedCardSync(cardId)?.isActivationFinished ?: false
}
override suspend fun isActivationInProgress(cardId: String): Boolean {
val card = getUsedCardSync(cardId) ?: return false
return card.isActivationStarted && !card.isActivationFinished
}
override suspend fun isTangemTOSAccepted(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, default = false)
}
override suspend fun isStart2CoinTOSAccepted(cardId: String): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)),
default = false,
)
}
override suspend fun acceptTangemTOS() {
return appPreferencesStore.store(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, true)
}
override suspend fun acceptStart2CoinTOS(cardId: String) {
appPreferencesStore.store(
key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)),
value = true,
)
}
private suspend fun AppPreferencesStore.editUsedCards(cardId: String, update: (UsedCardInfo) -> UsedCardInfo) {
editData { mutablePreferences ->
val usedCards = mutablePreferences.getUsedCards()
val updatedUsedCards = usedCards.updateCard(cardId = cardId, update = update)
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
}
}
private fun MutablePreferences.getUsedCards(): List<UsedCardInfo> {
return with(appPreferencesStore) {
getObjectListOrDefault(key = PreferencesKeys.USED_CARDS_INFO_KEY, default = mutableListOf())
}
}
private fun List<UsedCardInfo>.updateCard(
cardId: String,
update: (UsedCardInfo) -> UsedCardInfo,
): List<UsedCardInfo> {
val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId)
return addOrReplace(item = update(card), predicate = { it.cardId == cardId })
}
private suspend fun getUsedCardSync(cardId: String): UsedCardInfo? {
return appPreferencesStore.getObjectListSync<UsedCardInfo>(PreferencesKeys.USED_CARDS_INFO_KEY)
.firstOrNull { it.cardId == cardId }
}
private fun getRegion(cardId: String): String? {
if (cardId.isEmpty()) return null
return when (cardId[1]) {
'0' -> "fr"
'1' -> "ch"
'2' -> "at"
else -> null
}
}
}

1
data/onboarding/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,30 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.onboarding"
}
dependencies {
// region AndroidX libraries
implementation(deps.androidx.datastore)
// endregion
// region DI
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
// endregion
// region Core modules
implementation(projects.core.datasource)
// endregion
// region Domain modules
implementation(projects.domain.onboarding)
// endregion
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.onboarding
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.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.onboarding.repository.OnboardingRepository
import kotlinx.coroutines.flow.Flow
/**
* Default implementation of [OnboardingRepository]
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultOnboardingRepository(
private val appPreferencesStore: AppPreferencesStore,
) : OnboardingRepository {
override fun wasTwinsOnboardingShown(): Flow<Boolean> {
return appPreferencesStore.get(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false)
}
override suspend fun wasTwinsOnboardingShownSync(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false)
}
override suspend fun saveTwinsOnboardingShown() {
appPreferencesStore.store(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, value = true)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.onboarding.di
import com.tangem.data.onboarding.DefaultOnboardingRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore
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 OnboardingRepositoryModule {
@Provides
@Singleton
fun provideOnboardingRepository(appPreferencesStore: AppPreferencesStore): OnboardingRepository {
return DefaultOnboardingRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -5,6 +5,10 @@ plugins {
id("configuration")
}
android {
namespace = "com.tangem.data.source.preferences"
}
dependencies {
implementation(deps.androidx.core.ktx)
implementation(deps.moshi)

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.data.source.preferences" />

View file

@ -3,10 +3,6 @@ package com.tangem.data.source.preferences
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.data.source.preferences.adapters.BigDecimalAdapter
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import javax.inject.Inject
// 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories
@ -14,22 +10,11 @@ import javax.inject.Inject
@Deprecated("Create repository instead")
class PreferencesDataSource @Inject internal constructor(applicationContext: Context) {
val usedCardsPrefStorage: UsedCardsPrefStorage
val disclaimerPrefStorage: DisclaimerPrefStorage
private val preferences: SharedPreferences =
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
private val moshiConverter = MoshiJsonConverter(
adapters = listOf(BigDecimalAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
)
init {
incrementLaunchCounter()
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter)
usedCardsPrefStorage.migrate()
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
}
var shouldShowSaveUserWalletScreen: Boolean
@ -56,14 +41,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
putBoolean(OPEN_WELCOME_ON_RESUME_KEY, value)
}
fun saveTwinsOnboardingShown() {
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
}
fun wasTwinsOnboardingShown(): Boolean {
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
}
private fun incrementLaunchCounter() {
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
@ -71,7 +48,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown"
private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes"

View file

@ -1,8 +0,0 @@
package com.tangem.data.source.preferences.model
internal data class DataSourceUsedCardInfo(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
val isActivationFinished: Boolean = false,
)

View file

@ -1,7 +0,0 @@
package com.tangem.data.source.preferences.model
internal data class DataSourceUsedCardInfoOld(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
)

View file

@ -1,21 +0,0 @@
package com.tangem.data.source.preferences.storage
import android.content.SharedPreferences
import androidx.core.content.edit
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class DisclaimerPrefStorage internal constructor(
private val preferences: SharedPreferences,
) {
fun accept(disclaimerKey: String) {
preferences.edit { putBoolean(disclaimerKey, true) }
}
fun isAccepted(disclaimerKey: String): Boolean {
return preferences.getBoolean(disclaimerKey, false)
}
}

View file

@ -1,150 +0,0 @@
package com.tangem.data.source.preferences.storage
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfo
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfoOld
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class UsedCardsPrefStorage internal constructor(
private val preferences: SharedPreferences,
private val jsonConverter: MoshiJsonConverter,
) {
private val migrationList = mutableListOf(
UserCardInfoToV2(this),
)
internal fun migrate() {
migrationList.forEach { it.migrate() }
migrationList.clear()
}
fun scanned(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
?: DataSourceUsedCardInfo(cardId, true)
save(foundItem, restoredList)
}
fun wasScanned(cardId: String): Boolean {
return findCardInfo(cardId)?.isScanned ?: false
}
fun activationStarted(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
?: DataSourceUsedCardInfo(cardId, isActivationStarted = true)
save(foundItem, restoredList)
}
fun activationFinished(cardId: String) {
val restoredList = restore()
var foundItem = findCardInfo(cardId, restoredList) ?: DataSourceUsedCardInfo(cardId)
foundItem = foundItem.copy(
isActivationStarted = true,
isActivationFinished = true,
)
save(foundItem, restoredList)
}
fun isActivationStarted(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationStarted ?: false
}
fun isActivationFinished(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationFinished ?: false
}
fun isActivationInProgress(cardId: String): Boolean {
val cardInfo = findCardInfo(cardId) ?: return false
return cardInfo.isActivationStarted && !cardInfo.isActivationFinished
}
fun hadFinishedActivation(): Boolean {
return restore().any { it.isActivationFinished }
}
private fun findCardInfo(
cardId: String,
list: MutableList<DataSourceUsedCardInfo>? = null,
): DataSourceUsedCardInfo? {
val findInList = list ?: restore()
return findInList.firstOrNull { it.cardId == cardId }
}
private fun save(usedCardInfo: DataSourceUsedCardInfo?, usedCardsInfo: MutableList<DataSourceUsedCardInfo>) {
val info = usedCardInfo ?: return
with(usedCardsInfo) {
val index = indexOfFirst { it.cardId == info.cardId }
if (index == -1) {
add(info)
} else {
set(index, info)
}
}
save(usedCardsInfo)
}
private fun save(list: MutableList<DataSourceUsedCardInfo>) {
val json = jsonConverter.toJson(list)
preferences.edit { putString(USED_CARDS_INFO_V2, json) }
}
private fun restore(): MutableList<DataSourceUsedCardInfo> {
val json = preferences.getString(USED_CARDS_INFO_V2, null) ?: return mutableListOf()
return try {
jsonConverter.fromJson(json, jsonConverter.typedList(DataSourceUsedCardInfo::class.java))!!
} catch (ex: Exception) {
preferences.edit(true) { remove(USED_CARDS_INFO_V2) }
mutableListOf()
}
}
companion object {
private const val USED_CARDS_INFO_V2 = "usedCardsInfo_v2"
private const val USED_CARDS_INFO = "usedCardsInfo"
}
private class UserCardInfoToV2(
private val storage: UsedCardsPrefStorage,
) : Migration {
override fun migrate() {
val restoredCardsInfo = restore()
if (restoredCardsInfo.isEmpty()) return
val newCardsInfo = restoredCardsInfo.map { cardInfo ->
DataSourceUsedCardInfo(
cardId = cardInfo.cardId,
isScanned = cardInfo.isScanned,
isActivationStarted = true,
isActivationFinished = !cardInfo.isActivationStarted,
)
}.toMutableList()
storage.save(newCardsInfo)
}
private fun restore(): MutableList<DataSourceUsedCardInfoOld> {
val json = storage.preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf()
return try {
storage.jsonConverter.fromJson(
json,
storage.jsonConverter.typedList(DataSourceUsedCardInfoOld::class.java),
)!!
} catch (ex: Exception) {
mutableListOf()
} finally {
storage.preferences.edit(true) { remove(USED_CARDS_INFO) }
}
}
}
}

View file

@ -74,23 +74,40 @@ class ResponseCryptoCurrenciesFactory {
): CryptoCurrency.Coin? {
val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null
// workaround: Dischain was renamed but backend still returns the old name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
val name = if (blockchain == Blockchain.Dischain) blockchain.fullName else responseToken.name
val symbol = if (blockchain == Blockchain.Dischain) blockchain.currency else responseToken.symbol
return CryptoCurrency.Coin(
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = name,
symbol = symbol,
name = blockchain.getNameForCoin(responseToken),
symbol = blockchain.getSymbolForCoin(responseToken),
decimals = responseToken.decimals,
iconUrl = getCoinIconUrl(blockchain),
isCustom = isCustomCoin(network),
)
}
private fun Blockchain.getNameForCoin(responseToken: UserTokensResponse.Token): String {
return when (this) {
// workaround: Dischain was renamed but backend still returns the old name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
Blockchain.Dischain,
Blockchain.Arbitrum,
-> this.fullName
else -> responseToken.name
}
}
private fun Blockchain.getSymbolForCoin(responseToken: UserTokensResponse.Token): String {
return when (this) {
// workaround: Dischain was renamed but backend still returns the old name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
Blockchain.Dischain,
-> this.currency
else -> responseToken.symbol
}
}
private fun createToken(
blockchain: Blockchain,
sdkToken: Token,

View file

@ -4,17 +4,20 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.txhistory.repository.TxHistoryRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.utils.SdkPageConverter
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -25,6 +28,7 @@ class DefaultTxHistoryRepository(
private val userWalletsStore: UserWalletsStore,
private val txHistoryItemsStore: TxHistoryItemsStore,
) : TxHistoryRepository {
private val sdkPageConverter by lazy { SdkPageConverter() }
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int {
val userWallet = getUserWallet(userWalletId)
@ -66,14 +70,49 @@ class DefaultTxHistoryRepository(
override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String {
val blockchain = Blockchain.fromId(networkId.value)
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
""
} else {
blockchain.getExploreTxUrl(txHash)
return when (val txExploreState = blockchain.getExploreTxUrl(txHash)) {
is TxExploreState.Url -> txExploreState.url
is TxExploreState.Unsupported -> ""
}
}
override suspend fun getFixedSizeTxHistoryItems(
userWalletId: UserWalletId,
currency: CryptoCurrency,
pageSize: Int,
refresh: Boolean,
): List<TxHistoryItem> {
cacheRegistry.invokeOnExpire(
key = getTxHistoryPageKey(currency, userWalletId, Page.Initial),
skipCache = refresh,
block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) },
)
val txs = txHistoryItemsStore.getSyncOrNull(
key = TxHistoryItemsStore.Key(userWalletId, currency),
page = Page.Initial,
)?.items
return txs ?: emptyList()
}
private fun getTxHistoryPageKey(currency: CryptoCurrency, userWalletId: UserWalletId, page: Page): String {
return "tx_history_page_${currency}_${userWalletId}_$page"
}
private suspend fun fetchFixedSizeTxHistoryItems(
userWalletId: UserWalletId,
currency: CryptoCurrency,
pageSize: Int,
) {
val wrappedItems = walletManagersFacade.getTxHistoryItems(
userWalletId = userWalletId,
currency = currency,
page = sdkPageConverter.convertBack(Page.Initial),
pageSize = pageSize,
)
txHistoryItemsStore.store(TxHistoryItemsStore.Key(userWalletId, currency), wrappedItems)
}
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find user wallet with provided ID: $userWalletId"

View file

@ -1,6 +1,7 @@
package com.tangem.data.visa.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.lib.visa.model.VisaTxHistoryResponse
@ -43,7 +44,12 @@ internal class VisaTxDetailsFactory {
txHash = request.txHash,
txStatus = request.txStatus,
fiatCurrency = findCurrencyByNumericCode(request.transactionCurrencyCode),
exploreUrl = request.txHash?.let(walletBlockchain::getExploreTxUrl),
exploreUrl = request.txHash?.let {
when (val txUrl = walletBlockchain.getExploreTxUrl(it)) {
is TxExploreState.Url -> txUrl.url
is TxExploreState.Unsupported -> ""
}
},
)
}
}

View file

@ -7,4 +7,29 @@ interface CardRepository {
fun wasCardScanned(cardId: String): Flow<Boolean>
suspend fun setCardWasScanned(cardId: String)
suspend fun startCardActivation(cardId: String)
suspend fun finishCardActivation(cardId: String)
suspend fun finishCardsActivation(cardIds: List<String>)
@Throws
suspend fun isActivationStarted(cardId: String): Boolean
@Throws
suspend fun isActivationFinished(cardId: String): Boolean
@Throws
suspend fun isActivationInProgress(cardId: String): Boolean
@Throws
suspend fun isTangemTOSAccepted(): Boolean
@Throws
suspend fun isStart2CoinTOSAccepted(cardId: String): Boolean
suspend fun acceptTangemTOS()
suspend fun acceptStart2CoinTOS(cardId: String)
}

Some files were not shown because too many files have changed in this diff Show more