Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-19 10:20:09 +03:00
commit f0e4aec9a4
858 changed files with 13724 additions and 5705 deletions

View file

@ -367,7 +367,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
if (isFromPush) {
analyticsEventsHandler.send(Push.PushNotificationOpened)
analyticsEventsHandler.send(Push.PushNotificationOpened())
}
handleDeepLink(intent = intent, isFromOnNewIntent = true)

View file

@ -328,7 +328,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.blockchainSdkNetwork) {
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(),
ChuckerInterceptor(this),

View file

@ -25,8 +25,6 @@ internal class DefaultTrackingContextProxy(private val abTestsManager: ABTestsMa
override fun setContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
Analytics.setContext(userWalletId, scanResponse)
abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWalletId),
batch = scanResponse.card.batchId,

View file

@ -12,12 +12,6 @@ sealed class AnalyticsParam {
class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
}
sealed class CardBalanceState(val value: String) {
data object Empty : CardBalanceState("Empty")
data object Full : CardBalanceState("Full")
companion object
}
sealed class RateApp(val value: String) {
data object Liked : RateApp("Liked")
data object Closed : RateApp("Close")

View file

@ -11,85 +11,5 @@ sealed class Onboarding(
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category, event, params) {
class Started : Onboarding("Onboarding", "Onboarding Started")
class Finished : Onboarding("Onboarding", "Onboarding Finished")
sealed class CreateWallet(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Create Wallet", event, params) {
class ScreenOpened : CreateWallet("Create Wallet Screen Opened")
class ButtonCreateWallet : CreateWallet("Button - Create Wallet")
class WalletCreatedSuccessfully(
creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey,
seedPhraseLength: Int? = null,
) : CreateWallet(
event = "Wallet Created Successfully",
params = buildMap {
put(AnalyticsParam.CREATION_TYPE, creationType.value)
if (seedPhraseLength != null) put(AnalyticsParam.SEED_PHRASE_LENGTH, seedPhraseLength.toString())
},
)
}
sealed class Backup(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Backup", event, params) {
class ScreenOpened : Backup("Backup Screen Opened")
class Started : Backup("Backup Started")
class Skipped : Backup("Backup Skipped")
class SettingAccessCodeStarted : Backup("Setting Access Code Started")
class AccessCodeEntered : Backup("Access Code Entered")
class AccessCodeReEntered : Backup("Access Code Re-entered")
class Finished(cardsCount: Int) : Backup(
event = "Backup Finished",
params = mapOf("Cards count" to "$cardsCount"),
)
object ResetCancelEvent : Backup(
event = "Reset Card Notification",
params = mapOf("Option" to "Cancel"),
)
object ResetPerformEvent : Backup(
event = "Reset Card Notification",
params = mapOf("Option" to "Reset"),
)
}
sealed class Topup(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Top Up", event, params) {
class ScreenOpened : Topup("Activation Screen Opened")
class ButtonBuyCrypto(currency: AnalyticsParam.CurrencyType) : Topup(
event = "Button - Buy Crypto",
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
)
class ButtonShowWalletAddress : Topup("Button - Show the Wallet Address")
}
sealed class Twins(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Twins", event, params) {
class ScreenOpened : Twins("Twinning Screen Opened")
class SetupStarted : Twins("Twin Setup Started")
class SetupFinished : Twins("Twin Setup Finished")
}
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
category = "Onboarding / Biometric",
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
}

View file

@ -8,5 +8,5 @@ internal sealed class Push(event: String) : AnalyticsEvent(
params = emptyMap(),
) {
data object PushNotificationOpened : Push(event = "Push Notification Opened")
class PushNotificationOpened : Push(event = "Push Notification Opened")
}

View file

@ -67,7 +67,7 @@ sealed class Settings(
params = mapOf("State" to state.value),
)
object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class MainCurrencyChanged(currencyType: String) : AppSettings(
event = "Main Currency Changed",
@ -79,7 +79,7 @@ sealed class Settings(
params = mapOf("State" to theme.value),
)
object EnableBiometrics : AppSettings(event = "Notice - Enable Biometric")
class EnableBiometrics : AppSettings(event = "Notice - Enable Biometric")
class HideBalanceChanged(state: AnalyticsParam.OnOffState) : AppSettings(
event = "Hide Balance Changed",

View file

@ -10,8 +10,6 @@ sealed class SignIn(
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Sign In", event, params) {
class ScreenOpened : SignIn(event = "Sign In Screen Opened")
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
class ButtonCardSignIn : SignIn(event = "Button - Card Sign In")
}

View file

@ -29,7 +29,7 @@ class AmplitudeAnalyticsHandler(
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler {
return AmplitudeAnalyticsHandler(
client = if (data.logConfig.amplitude) {
client = if (data.logConfig.isAmplitudeLogEnabled) {
AmplitudeLogClient(data.jsonConverter)
} else {
AmplitudeClient(data.application, data.config.amplitudeApiKey)

View file

@ -49,7 +49,7 @@ class FirebaseAnalyticsHandler(
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> FirebaseClient()
data.isDebug && data.logConfig.firebase -> FirebaseLogClient(data.jsonConverter)
data.isDebug && data.logConfig.isFirebaseLogEnabled -> FirebaseLogClient(data.jsonConverter)
else -> null
}?.let { FirebaseAnalyticsHandler(it) }
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
@ -30,7 +31,12 @@ class CardContextInterceptor(
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return when (event) {
is IntroductionProcess.ButtonScanCard -> false
is IntroductionProcess.ButtonScanCard,
is IntroductionProcess.ButtonScanCardLegacy,
is SignIn.ScreenOpened,
is SignIn.ButtonAddWallet,
-> false
is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll
else -> true
}
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess
class HotWalletContextInterceptor(
val parent: ParamsInterceptor? = null,
@ -10,10 +12,23 @@ class HotWalletContextInterceptor(
override fun id(): String = HotWalletContextInterceptor.id()
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return when (event) {
is SignIn.ScreenOpened,
is SignIn.ButtonAddWallet,
is SignIn.ButtonUnlockAllWithBiometric,
is IntroductionProcess.ButtonScanCard,
-> false
is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll
else -> true
}
}
override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.PRODUCT_TYPE] = AnalyticsParam.ProductType.MobileWallet.value
params.remove(AnalyticsParam.BATCH)
params.remove(AnalyticsParam.FIRMWARE)
params.remove(AnalyticsParam.CURRENCY)
}
companion object {

View file

@ -0,0 +1,13 @@
package com.tangem.tap.core.security
import com.dexprotector.rtc.RtcStatus
import com.tangem.security.DeviceSecurityInfoProvider
internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
override val isRooted: Boolean
get() = RtcStatus.getRtcStatus().root
override val isBootloaderUnlocked: Boolean
get() = RtcStatus.getRtcStatus().unlockedBootloader
override val isXposed: Boolean
get() = RtcStatus.getRtcStatus().xposed
}

View file

@ -115,6 +115,7 @@ internal class DefaultTangemPayStorage @Inject constructor(
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
}
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
@ -144,6 +145,20 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
}
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
}
}
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
) == true
}
}
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"

View file

@ -0,0 +1,20 @@
package com.tangem.tap.di.core.security
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.tap.core.security.DefaultDeviceSecurityInfoProvider
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 SecurityModule {
@Provides
@Singleton
fun provideDeviceSecurityInfoProvider(): DeviceSecurityInfoProvider {
return DefaultDeviceSecurityInfoProvider()
}
}

View file

@ -67,11 +67,13 @@ internal object AccountDomainModule {
accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
singleAccountListFetcher: SingleAccountListFetcher,
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
singleAccountListFetcher = singleAccountListFetcher,
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase
import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import dagger.Module
@ -24,4 +26,18 @@ internal object HotWalletDomainModule {
fun provideSetAccessCodeSkippedUseCase(hotWalletRepository: HotWalletRepository): SetAccessCodeSkippedUseCase {
return SetAccessCodeSkippedUseCase(hotWalletRepository)
}
@Provides
@Singleton
fun provideIsAccessCodeSimpleUseCase(): IsAccessCodeSimpleUseCase {
return IsAccessCodeSimpleUseCase()
}
@Provides
@Singleton
fun provideIsWalletCreationSupportedUseCase(
hotWalletRepository: HotWalletRepository,
): IsHotWalletCreationSupported {
return IsHotWalletCreationSupported(hotWalletRepository)
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
@ -74,7 +74,7 @@ internal object ManageTokensDomainModule {
derivationsRepository: DerivationsRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveManagedTokensUseCase {
@ -85,7 +85,7 @@ internal object ManageTokensDomainModule {
derivationsRepository = derivationsRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)

View file

@ -8,10 +8,10 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
@ -36,6 +36,14 @@ object MarketsDomainModule {
return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTopFiveMarketTokenUseCase(
marketsTokenRepository: MarketsTokenRepository,
): GetTopFiveMarketTokenUseCase {
return GetTopFiveMarketTokenUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase {
@ -65,20 +73,22 @@ object MarketsDomainModule {
fun provideSaveMarketTokensUseCase(
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)
@ -118,13 +128,11 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetStakingNotificationMaxApyUseCase(
settingsRepository: SettingsRepository,
fun provideShouldShowYieldModeMarketPromoUseCase(
promoRepository: PromoRepository,
marketsTokenRepository: MarketsTokenRepository,
): GetStakingNotificationMaxApyUseCase {
return GetStakingNotificationMaxApyUseCase(
settingsRepository = settingsRepository,
): ShouldShowYieldModeMarketPromoUseCase {
return ShouldShowYieldModeMarketPromoUseCase(
promoRepository = promoRepository,
marketsTokenRepository = marketsTokenRepository,
)

View file

@ -1,11 +1,11 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.nft.utils.NFTCleaner
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
@ -27,12 +27,12 @@ internal object NFTDomainModule {
fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
singleAccountListSupplier: SingleAccountListSupplier,
accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
singleAccountListSupplier = singleAccountListSupplier,
accountsFeatureToggles = accountsFeatureToggles,
)
@ -67,12 +67,12 @@ internal object NFTDomainModule {
@Singleton
fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
singleAccountListSupplier: SingleAccountListSupplier,
currenciesRepository: CurrenciesRepository,
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
singleAccountListSupplier = singleAccountListSupplier,
)
@Provides
@ -126,13 +126,13 @@ internal object NFTDomainModule {
@Singleton
fun provideDisableWalletNFTUseCase(
walletsRepository: WalletsRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
nftCleaner: NFTCleaner,
): DisableWalletNFTUseCase {
return DisableWalletNFTUseCase(
walletsRepository = walletsRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
nftCleaner = nftCleaner,
)
}
@ -145,13 +145,13 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun provideClearNFTCacheUseCase(
nftRepository: NFTRepository,
nftCleaner: NFTCleaner,
currenciesRepository: CurrenciesRepository,
accountsFeatureToggles: AccountsFeatureToggles,
singleAccountListSupplier: SingleAccountListSupplier,
): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase(
nftRepository = nftRepository,
nftCleaner = nftCleaner,
currenciesRepository = currenciesRepository,
accountsFeatureToggles = accountsFeatureToggles,
singleAccountListSupplier = singleAccountListSupplier,

View file

@ -1,10 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.news.repository.NewsRepository
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
import com.tangem.domain.news.usecase.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -38,4 +35,10 @@ internal object NewsDomainModule {
fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase {
return GetNewsListBatchFlowUseCase(repository)
}
@Provides
@Singleton
fun provideFetchTrendingNewsUseCase(repository: NewsRepository): FetchTrendingNewsUseCase {
return FetchTrendingNewsUseCase(repository)
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module
@ -107,11 +107,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideFetchStakingYieldBalanceUseCase(
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchStakingYieldBalanceUseCase {
return FetchStakingYieldBalanceUseCase(
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}

View file

@ -12,15 +12,18 @@ import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
@ -40,17 +43,19 @@ internal object TokensDomainModule {
@Singleton
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
@ -148,7 +153,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase {
@ -156,7 +161,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
@ -339,8 +344,8 @@ internal object TokensDomainModule {
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
singleStakingBalanceSupplier: SingleStakingBalanceSupplier,
multiStakingBalanceSupplier: MultiStakingBalanceSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
@ -350,8 +355,8 @@ internal object TokensDomainModule {
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
singleStakingBalanceSupplier = singleStakingBalanceSupplier,
multiStakingBalanceSupplier = multiStakingBalanceSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
@ -371,7 +376,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
@ -381,7 +386,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)

View file

@ -1,10 +1,10 @@
package com.tangem.tap.di.domain
import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
@ -16,6 +16,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
internal object YieldSupplyDomainModule {
@ -225,4 +226,12 @@ internal object YieldSupplyDomainModule {
fun provideYieldSupplyGetDustMinAmountUseCase(): YieldSupplyGetDustMinAmountUseCase {
return YieldSupplyGetDustMinAmountUseCase()
}
@Provides
@Singleton
fun provideYieldSupplyGetAvailabilityUseCase(
yieldSupplyRepository: YieldSupplyRepository,
): YieldSupplyGetAvailabilityUseCase {
return YieldSupplyGetAvailabilityUseCase(yieldSupplyRepository)
}
}

View file

@ -164,7 +164,7 @@ internal class LegacyScanProcessor @Inject constructor(
) {
if (error is TangemSdkError.CardVerificationFailed) {
analyticsEventHandler.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(
event = OnboardingAnalyticsEvent.Error.OfflineAttestationFailed(
analyticsSource,
),
)

View file

@ -11,7 +11,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
@ -31,7 +31,7 @@ import kotlinx.coroutines.withContext
class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
@Assisted private val coroutineScope: CoroutineScope,
private val dispatchersProvider: CoroutineDispatcherProvider,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val tangemPayRemoteDataSource: TangemPayRemoteDataSource,
) : CardSessionRunnable<TangemPayInitialCredentials> {
override fun run(session: CardSession, callback: CompletionCallback<TangemPayInitialCredentials>) {
@ -52,7 +52,7 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey)
val challenge = withContext(dispatchersProvider.io) {
visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(
tangemPayRemoteDataSource.getCustomerWalletAuthChallenge(
customerWalletAddress = address,
customerWalletId = userWalletId.stringValue,
)
@ -71,7 +71,7 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
}
val authTokens = withContext(dispatchersProvider.io) {
visaAuthRemoteDataSource.getTokenWithCustomerWallet(
tangemPayRemoteDataSource.getTokenWithCustomerWallet(
sessionId = challenge.session.sessionId,
signature = signedData.signature,
nonce = signedData.dataToSign.hashToSign,

View file

@ -7,8 +7,10 @@ import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
@ -125,6 +127,9 @@ internal object UserWalletsListManagerModule {
appPreferencesStore: AppPreferencesStore,
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
tangemHotSdk: TangemHotSdk,
trackingContextProxy: TrackingContextProxy,
analyticsEventHandler: AnalyticsEventHandler,
hotWalletRepository: HotWalletRepository,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
@ -172,6 +177,9 @@ internal object UserWalletsListManagerModule {
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
tangemHotSdk = tangemHotSdk,
trackingContextProxy = trackingContextProxy,
analyticsEventHandler = analyticsEventHandler,
hotWalletRepository = hotWalletRepository,
)
}

View file

@ -7,12 +7,16 @@ import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.common.wallets.error.*
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
@ -50,6 +54,9 @@ internal class DefaultUserWalletsListRepository(
private val appPreferencesStore: AppPreferencesStore,
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
private val tangemHotSdk: TangemHotSdk,
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletRepository: HotWalletRepository,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
@ -204,7 +211,7 @@ internal class DefaultUserWalletsListRepository(
userWalletEncryptionKeysRepository.delete(userWalletIds)
removeHotWalletsFromSDK(userWalletIds)
removeHotWalletsFromSDKAndRepos(userWalletIds)
userWallets.update { currentWallets ->
val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() }
@ -235,6 +242,7 @@ internal class DefaultUserWalletsListRepository(
when (unlockMethod) {
UserWalletsListRepository.UnlockMethod.Biometric -> {
unlockAllWallets().bind()
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Biometric)
select(userWalletId)
}
UserWalletsListRepository.UnlockMethod.AccessCode -> {
@ -263,7 +271,10 @@ internal class DefaultUserWalletsListRepository(
removePasswordAttempts(userWallet)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } }
.doOnSuccess { sensitiveInfo ->
updateWallets { it?.updateWith(sensitiveInfo) }
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.AccessCode)
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
}
is UserWalletsListRepository.UnlockMethod.Scan -> {
@ -291,7 +302,10 @@ internal class DefaultUserWalletsListRepository(
)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } }
.doOnSuccess { sensitiveInfo ->
updateWallets { it?.updateWith(sensitiveInfo) }
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card)
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
}
}
@ -332,6 +346,9 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo ->
updateWallets { wallets -> wallets?.updateWith(sensitiveInfo) }
selectedUserWallet.value?.let {
trackSignInEvent(it, Basic.SignedIn.SignInType.Biometric)
}
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
}
@ -373,7 +390,7 @@ internal class DefaultUserWalletsListRepository(
if (newUserWallet.walletId == oldUserWallet.walletId &&
oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold
) {
removeHotWalletsFromSDK(walletIds = listOf(oldUserWallet.walletId))
removeHotWalletsFromSDKAndRepos(walletIds = listOf(oldUserWallet.walletId))
// When upgrading from Hot to Cold, if biometric lock is available, set it
if (hasBiometry()) {
setLock(newUserWallet.walletId, LockMethod.Biometric, changeUnsecured = true)
@ -384,13 +401,14 @@ internal class DefaultUserWalletsListRepository(
}
}
private suspend fun removeHotWalletsFromSDK(walletIds: List<UserWalletId>) {
private suspend fun removeHotWalletsFromSDKAndRepos(walletIds: List<UserWalletId>) {
val hotWalletsToDelete = userWalletsSync()
.filterIsInstance<UserWallet.Hot>()
.filter { walletIds.contains(it.walletId) }
hotWalletsToDelete.forEach {
tangemHotSdk.delete(it.hotWalletId)
hotWalletsToDelete.forEach { wallet ->
hotWalletRepository.setAccessCodeSkipped(wallet.walletId, false) // In case the wallet is added again
tangemHotSdk.delete(wallet.hotWalletId)
}
}
@ -508,4 +526,14 @@ internal class DefaultUserWalletsListRepository(
return lastOrNull()
}
private fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) {
trackingContextProxy.addContext(userWallet)
analyticsEventHandler.send(
event = Basic.SignedIn(
signInType = type,
walletsCount = userWallets.value?.size ?: 0,
),
)
}
}

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol
@ -96,7 +97,13 @@ internal class UserWalletEncryptionKeysRepository(
StorageKey.UserWalletEncryptionKey(userWalletId).name
}
authenticatedStorage.get(keys).mapNotNull {
val result = authenticatedStorage.get(keys)
if (keys.isNotEmpty() && result.isEmpty()) {
throw TangemSdkError.KeystoreInvalidated(Exception("Keys is empty"))
}
result.mapNotNull {
it.value.decodeToKey()
}
}

View file

@ -196,7 +196,7 @@ class DetailsMiddleware {
}
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication())
store.inject(DaggerGraphState::settingsManager).openBiometricSettings()
}

View file

@ -25,7 +25,7 @@ internal class AppSettingsItemsAnalyticsSender @Inject constructor(
private fun getEvent(item: AppSettingsScreenState.Item): AnalyticsEvent? {
return when (item.id) {
AppSettingsItemsFactory.ID_ENROLL_BIOMETRICS_CARD -> Settings.AppSettings.EnableBiometrics
AppSettingsItemsFactory.ID_ENROLL_BIOMETRICS_CARD -> Settings.AppSettings.EnableBiometrics()
else -> null
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -24,6 +25,9 @@ import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
private const val CARD_PLACEHOLDER_SECONDARY_ROTATION = -15f
private const val CARD_PLACEHOLDER_PRIMARY_ROTATION = -1f
@Composable
internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) {
val isCardReadingNeeded = state.cardDetails == null
@ -42,74 +46,82 @@ internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifi
)
}
@Suppress("MagicNumber")
@Composable
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing40)
.testTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK),
) {
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing80,
end = TangemTheme.dimens.spacing80,
top = TangemTheme.dimens.spacing70,
)
.rotate(-15f),
painter = painterResource(id = R.drawable.card_placeholder_secondary),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing60,
end = TangemTheme.dimens.spacing60,
)
.rotate(-1f),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
CardPlaceholderImages()
Spacer(modifier = Modifier.weight(1f))
Column(
ScanCardContent(onScanCardClick = onScanCardClick)
}
}
@Composable
private fun CardPlaceholderImages() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing40)
.testTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK),
) {
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
) {
Text(
text = stringResourceSafe(id = R.string.scan_card_settings_title),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size20))
Text(
text = stringResourceSafe(id = R.string.scan_card_settings_message),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body1,
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(weight = 1f, fill = false),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
DetailsMainButton(
title = stringResourceSafe(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
)
}
start = TangemTheme.dimens.spacing80,
end = TangemTheme.dimens.spacing80,
top = TangemTheme.dimens.spacing70,
)
.rotate(CARD_PLACEHOLDER_SECONDARY_ROTATION),
painter = painterResource(id = R.drawable.card_placeholder_secondary),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing60,
end = TangemTheme.dimens.spacing60,
)
.rotate(CARD_PLACEHOLDER_PRIMARY_ROTATION),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
}
@Composable
private fun ScanCardContent(onScanCardClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
) {
Text(
text = stringResourceSafe(id = R.string.scan_card_settings_title),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size20))
Text(
text = stringResourceSafe(id = R.string.scan_card_settings_message),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body1,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
DetailsMainButton(
title = stringResourceSafe(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
)
}
}

View file

@ -1,9 +1,7 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.wallet.R
@ -32,7 +30,7 @@ internal sealed class CardInfo(
class SignedHashes(hashes: String) : CardInfo(
titleRes = TextReference.Res(R.string.details_row_title_signed_hashes),
subtitle = TextReference.Res(R.string.details_row_subtitle_signed_hashes_format, hashes),
subtitle = TextReference.Res(R.string.details_row_subtitle_signed_hashes_format, wrappedList(hashes)),
)
class SecurityMode(securityOption: SecurityOption, clickable: Boolean) : CardInfo(
@ -47,7 +45,7 @@ internal sealed class CardInfo(
isClickable = true,
)
class AccessCodeRecovery(val isEnabled: Boolean) : CardInfo(
class AccessCodeRecovery(isEnabled: Boolean) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title),
subtitle = if (isEnabled) {
TextReference.Res(R.string.common_enabled)
@ -62,22 +60,4 @@ internal sealed class CardInfo(
subtitle = description,
isClickable = true,
)
}
// TODO("Remove and use the same from coreUI")
internal sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
}
class Str(val value: String) : TextReference
}
@Composable
@ReadOnlyComposable
internal fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResourceSafe(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.card.CardTypesResolver
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.core.ui.extensions.TextReference
import com.tangem.wallet.R
internal fun getResetToFactoryDescription(

View file

@ -17,8 +17,8 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.ResetCardScreenTestTags
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -198,7 +198,7 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
}
@Composable
private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
private fun CommonResetDialog(dialog: ResetCardDialog) {
BasicDialog(
title = stringResourceSafe(dialog.titleResId),
message = stringResourceSafe(dialog.messageResId),

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.annotation.StringRes
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.core.ui.extensions.TextReference
import com.tangem.wallet.R
internal data class ResetCardScreenState(

View file

@ -18,12 +18,12 @@ object UnfinishedBackupFoundDialog {
setTitle(R.string.common_warning)
setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup)
Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse))
}
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup)
Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse)))
}

View file

@ -0,0 +1,68 @@
package com.tangem.tap.features.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.DialogFullScreen
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@Suppress("UnusedPrivateProperty")
class RootDetectedWarningComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
private val securityInfoProvider: DeviceSecurityInfoProvider,
private val settingsRepository: SettingsRepository,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) }
suspend fun tryToShowWarningAndWaitContinuation() {
if (isShown.value) return
if (settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()) {
isShown.value = true
}
isShown.first { it == false } // Wait until the warning is dismissed
}
@Composable
override fun Content(modifier: Modifier) {
val isShownState by isShown.collectAsStateWithLifecycle()
if (isShownState) {
DialogFullScreen(onDismissRequest = {}) {
RootDetectedWarningContent(
modifier = modifier,
onContinueClick = remember(this) { ::onContinueClick },
)
}
}
}
private fun onContinueClick() {
componentScope.launch {
settingsRepository.setRootDetectedWarningShown(true)
isShown.value = false
}
}
@AssistedFactory
interface Factory : ComponentFactory<Unit, RootDetectedWarningComponent> {
override fun create(context: AppComponentContext, params: Unit): RootDetectedWarningComponent
}
}

View file

@ -0,0 +1,93 @@
package com.tangem.tap.features.root
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@Composable
internal fun RootDetectedWarningContent(modifier: Modifier = Modifier, onContinueClick: () -> Unit = {}) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.statusBarsPadding()
.padding(horizontal = 16.dp),
) {
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center,
) {
InfoBlock(
modifier = Modifier.padding(top = 48.dp, bottom = 24.dp),
)
}
PrimaryButton(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.common_understand_continue),
onClick = onContinueClick,
)
}
}
@Composable
private fun InfoBlock(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
HighlightedIcon(
icon = R.drawable.ic_alert_circle_24,
iconTint = TangemTheme.colors.icon.warning,
)
SpacerH(20.dp)
Text(
text = stringResourceSafe(R.string.root_detected_warning_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH(12.dp)
Text(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResourceSafe(R.string.root_detected_warning_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
}
@Preview
@Composable
private fun Preview() {
TangemThemePreview {
RootDetectedWarningContent()
}
}

View file

@ -10,7 +10,7 @@ import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.redux.WelcomeState
@ -45,7 +45,6 @@ internal class WelcomeModel @Inject constructor(
init {
subscribeToStoreChanges()
initGlobalState()
analyticsEventsHandler.send(SignIn.ScreenOpened())
val welcomeAction = when (params.launchMode) {
is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard

View file

@ -58,7 +58,7 @@ internal class WelcomeMiddleware {
.doOnSuccess { selectedUserWallet ->
sendSignedInAnalyticsEvent(
userWallet = selectedUserWallet,
signInType = Basic.SignedIn.SignInType.Biometric,
signInType = Basic.SignedInLegacy.SignInType.Biometric,
)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
@ -80,7 +80,7 @@ internal class WelcomeMiddleware {
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error))
}
.doOnSuccess {
sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedIn.SignInType.Card)
sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedInLegacy.SignInType.Card)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
@ -89,9 +89,7 @@ internal class WelcomeMiddleware {
}
}
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) {
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Analytics
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedInLegacy.SignInType) {
if (userWallet !is UserWallet.Cold) {
return
}
@ -108,7 +106,7 @@ internal class WelcomeMiddleware {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
Analytics.send(
event = Basic.SignedIn(
event = Basic.SignedInLegacy(
currency = currency,
batch = scanResponse.card.batchId,
signInType = signInType,

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.welcome.ui
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.welcome.ui.model.WarningModel
internal data class WelcomeScreenState(

View file

@ -18,8 +18,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent
import com.tangem.tap.features.welcome.ui.WelcomeScreenState

View file

@ -51,7 +51,9 @@ internal class DefaultAuthProvider(
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.STAGE_2,
ApiEnvironment.STAGE,
-> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
} ?: error("No tangem tech api config provided")
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider(
@ -11,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
?: error("No P2P api keys provided")
return keys.mainnet
return if (P2PStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
}
}

View file

@ -47,6 +47,7 @@ internal fun RootContent(
modifier: Modifier = Modifier,
wcContent: @Composable (modifier: Modifier) -> Unit,
hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit,
rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit,
) {
val context = LocalContext.current
@ -82,6 +83,8 @@ internal fun RootContent(
hotAccessCodeContent(Modifier.fillMaxSize())
rootDetectedWarningContent(Modifier.fillMaxSize())
TangemSnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)

View file

@ -11,8 +11,11 @@ import com.arkivanov.essenty.lifecycle.subscribe
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
@ -26,6 +29,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.hot.sdk.TangemHotSdk
@ -34,6 +38,7 @@ import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.hot.TangemHotSDKProxy
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.root.RootDetectedWarningComponent
import com.tangem.tap.routing.RootContent
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
@ -60,9 +65,13 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val tangemHotSDKProxy: TangemHotSDKProxy,
private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory,
private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy,
private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory,
private val userWalletsListRepository: UserWalletsListRepository,
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : RoutingComponent,
AppComponentContext by context,
@ -78,6 +87,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
.create(child("hotAccessCodeRequestComponent"), Unit)
}
private val rootDetectedWarningComponent: RootDetectedWarningComponent by lazy {
rootDetectedWarningComponentFactory
.create(child("rootDetectedWarningComponent"), Unit)
}
private val navigation = navigationProvider.getOrCreateTyped<AppRoute>()
private val stack: Value<ChildStack<AppRoute, Child>> = childStack(
@ -127,6 +141,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private fun initializeInitialNavigation() {
if (initialStack.isNullOrEmpty()) {
componentScope.launch {
rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation()
val initialRoute = resolveInitialRoute()
router.replaceAll(initialRoute)
}
@ -151,6 +166,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
)
}
else -> {
trackSignInEvent()
AppRoute.Wallet
}
}.also {
@ -169,6 +185,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
modifier = modifier,
wcContent = { wcRoutingComponent.Content(it) },
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
)
}
@ -235,4 +252,18 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
}
}
private suspend fun trackSignInEvent() {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val userWallets = userWalletsListRepository.userWalletsSync()
val selectedWallet = userWalletsListRepository.selectedUserWalletSync() ?: return
trackingContextProxy.addContext(selectedWallet)
analyticsEventHandler.send(
event = Basic.SignedIn(
signInType = Basic.SignedIn.SignInType.NoSecurity,
walletsCount = userWallets.size,
),
)
}
}
}

View file

@ -14,7 +14,6 @@ object RoutingTransitionAnimationFactory {
@Suppress("MagicNumber")
fun create(appRoute: AppRoute): StackAnimator {
return when (appRoute) {
is AppRoute.Onboarding,
is AppRoute.Welcome,
is AppRoute.Home,
-> fade(tween(400)).plus(scale(tween(400)))

View file

@ -40,6 +40,8 @@ import com.tangem.features.tangempay.components.TangemPayDetailsContainerCompone
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerOnMain
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerInSettings
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -529,7 +531,9 @@ internal class ChildFactory @Inject constructor(
is AppRoute.CreateMobileWallet -> {
createComponentChild(
context = context,
params = Unit,
params = CreateMobileWalletComponent.Params(
source = route.source,
),
componentFactory = createMobileWalletComponentFactory,
)
}
@ -565,7 +569,7 @@ internal class ChildFactory @Inject constructor(
params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId,
isUpgradeFlow = route.isUpgradeFlow,
shouldSetAccessCode = route.setAccessCode,
shouldSetAccessCode = route.shouldSetAccessCode,
analyticsSource = route.analyticsSource,
analyticsAction = route.analyticsAction,
),
@ -665,6 +669,9 @@ internal class ChildFactory @Inject constructor(
)
is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink(
deeplink = mode.deeplink,
)
is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings
is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain(
userWalletId = mode.userWalletId,
)
},