Updated on 2026-08-14
This commit is contained in:
commit
3fc5bf8fb3
388 changed files with 5078 additions and 2871 deletions
|
|
@ -276,7 +276,10 @@ class RecentBlockTest : BaseTestCase() {
|
|||
}
|
||||
step("Swipe up") {
|
||||
waitForIdle()
|
||||
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f)
|
||||
onSendAddressScreen {
|
||||
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f)
|
||||
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f)
|
||||
}
|
||||
}
|
||||
step("Check recent address item №7") {
|
||||
checkRecentAddressItem(address = recipientAddressBase + "f", description = recentTransactionAmount2)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class SolanaWarningsTest : BaseTestCase() {
|
|||
private val tokenName = "Solana"
|
||||
private val amountToLeaveLessThanRent = "0.0016941"
|
||||
private val amountToLeaveGreaterThanRent = "0.0000941"
|
||||
private val amountToLeaveRentOnly = "0.00168934"
|
||||
private val amountToLeaveRentOnly = "0.001689338"
|
||||
private val rentAmount = "SOL 0.00089088"
|
||||
|
||||
private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.nfc"
|
||||
android:required="true" />
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -73,6 +71,12 @@ internal class DefaultTrackingContextProxy(private val abTestsManager: ABTestsMa
|
|||
Analytics.removeContext()
|
||||
}
|
||||
|
||||
override fun proceedWithContext(userWallet: UserWallet, action: () -> Unit) {
|
||||
setContext(userWallet)
|
||||
action()
|
||||
eraseContext()
|
||||
}
|
||||
|
||||
private fun calculateUserIdHash(userWalletId: UserWalletId?): String? {
|
||||
return userWalletId?.value
|
||||
?.calculateSha256()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class CardContextInterceptor(
|
|||
|
||||
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
|
||||
return when (event) {
|
||||
is IntroductionProcess.ButtonScanCardLegacy -> false
|
||||
is IntroductionProcess.ButtonScanCard -> false
|
||||
else -> true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ class HotWalletContextInterceptor(
|
|||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ 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
|
||||
|
|
@ -65,20 +66,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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.domain.staking.repositories.StakingErrorResolver
|
|||
import com.tangem.domain.staking.repositories.StakeKitRepository
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import dagger.Module
|
||||
|
|
@ -113,11 +113,11 @@ internal object StakingDomainModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchStakingYieldBalanceUseCase(
|
||||
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): FetchStakingYieldBalanceUseCase {
|
||||
return FetchStakingYieldBalanceUseCase(
|
||||
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
|
||||
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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.models.scan.serialization.*
|
||||
|
|
@ -124,6 +125,8 @@ internal object UserWalletsListManagerModule {
|
|||
appPreferencesStore: AppPreferencesStore,
|
||||
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
tangemHotSdk: TangemHotSdk,
|
||||
trackingContextProxy: TrackingContextProxy,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
): UserWalletsListRepository {
|
||||
val moshi = buildMoshi()
|
||||
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
|
||||
|
|
@ -171,6 +174,8 @@ internal object UserWalletsListManagerModule {
|
|||
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
|
||||
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
|
||||
tangemHotSdk = tangemHotSdk,
|
||||
trackingContextProxy = trackingContextProxy,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ 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
|
||||
|
|
@ -50,6 +53,8 @@ internal class DefaultUserWalletsListRepository(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : UserWalletsListRepository {
|
||||
|
||||
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
|
||||
|
|
@ -235,6 +240,7 @@ internal class DefaultUserWalletsListRepository(
|
|||
when (unlockMethod) {
|
||||
UserWalletsListRepository.UnlockMethod.Biometric -> {
|
||||
unlockAllWallets().bind()
|
||||
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Biometric)
|
||||
select(userWalletId)
|
||||
}
|
||||
UserWalletsListRepository.UnlockMethod.AccessCode -> {
|
||||
|
|
@ -263,7 +269,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 +300,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 +344,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)) }
|
||||
}
|
||||
|
|
@ -508,4 +523,15 @@ internal class DefaultUserWalletsListRepository(
|
|||
|
||||
return lastOrNull()
|
||||
}
|
||||
|
||||
private fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) {
|
||||
trackingContextProxy.proceedWithContext(userWallet) {
|
||||
analyticsEventHandler.send(
|
||||
event = Basic.SignedIn(
|
||||
signInType = type,
|
||||
walletsCount = userWallets.value?.size ?: 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ 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.models.Basic
|
||||
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
|
||||
|
|
@ -23,6 +26,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
|
||||
|
|
@ -59,6 +63,9 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : RoutingComponent,
|
||||
AppComponentContext by context,
|
||||
SnackbarHandler {
|
||||
|
|
@ -130,6 +137,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
else -> {
|
||||
trackSignInEvent()
|
||||
AppRoute.Wallet
|
||||
}
|
||||
}.also {
|
||||
|
|
@ -214,4 +222,19 @@ 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.proceedWithContext(selectedWallet) {
|
||||
analyticsEventHandler.send(
|
||||
event = Basic.SignedIn(
|
||||
signInType = Basic.SignedIn.SignInType.NoSecurity,
|
||||
walletsCount = userWallets.size,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -565,7 +565,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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:AppRoute.kt$AppRoute.CreateWalletBackup$val setAccessCode: Boolean = false</ID>
|
||||
<ID>NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(NAME_KEY, it) }</ID>
|
||||
<ID>NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(TRANSACTION_ID_KEY, it) }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -368,7 +368,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val analyticsSource: String,
|
||||
val analyticsAction: String,
|
||||
val isUpgradeFlow: Boolean = false,
|
||||
val setAccessCode: Boolean = false,
|
||||
val shouldSetAccessCode: Boolean = false,
|
||||
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -55,8 +55,12 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
addQueryParam(DERIVATION_PATH_KEY, derivationPath)
|
||||
}
|
||||
|
||||
transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) }
|
||||
name?.let { addQueryParam(NAME_KEY, it) }
|
||||
if (transactionId != null) {
|
||||
addQueryParam(TRANSACTION_ID_KEY, transactionId)
|
||||
}
|
||||
if (name != null) {
|
||||
addQueryParam(NAME_KEY, name)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.common.test.data.staking
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolExitQueueDTO
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolStakeDTO
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory for creating mock P2P ETH Pool account responses for testing
|
||||
*/
|
||||
object MockP2PEthPoolAccountResponseFactory {
|
||||
|
||||
private val defaultStakingId = StakingID(
|
||||
integrationId = "p2p-ethereum-pooled",
|
||||
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
|
||||
)
|
||||
|
||||
const val defaultVaultAddress = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
|
||||
|
||||
fun createWithBalance(
|
||||
stakingId: StakingID = defaultStakingId,
|
||||
vaultAddress: String = defaultVaultAddress,
|
||||
stakedAmount: BigDecimal = BigDecimal("1.5"),
|
||||
earnedAmount: BigDecimal = BigDecimal("0.05"),
|
||||
): P2PEthPoolAccountResponse {
|
||||
return P2PEthPoolAccountResponse(
|
||||
delegatorAddress = stakingId.address,
|
||||
vaultAddress = vaultAddress,
|
||||
stake = P2PEthPoolStakeDTO(
|
||||
assets = stakedAmount,
|
||||
totalEarnedAssets = earnedAmount,
|
||||
),
|
||||
availableToUnstake = stakedAmount,
|
||||
availableToWithdraw = BigDecimal.ZERO,
|
||||
exitQueue = P2PEthPoolExitQueueDTO(
|
||||
total = 0.0,
|
||||
requests = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createWithEmptyBalance(
|
||||
stakingId: StakingID = defaultStakingId,
|
||||
vaultAddress: String = defaultVaultAddress,
|
||||
): P2PEthPoolAccountResponse {
|
||||
return P2PEthPoolAccountResponse(
|
||||
delegatorAddress = stakingId.address,
|
||||
vaultAddress = vaultAddress,
|
||||
stake = P2PEthPoolStakeDTO(
|
||||
assets = BigDecimal.ZERO,
|
||||
totalEarnedAssets = BigDecimal.ZERO,
|
||||
),
|
||||
availableToUnstake = BigDecimal.ZERO,
|
||||
availableToWithdraw = BigDecimal.ZERO,
|
||||
exitQueue = P2PEthPoolExitQueueDTO(
|
||||
total = 0.0,
|
||||
requests = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createMockVault(vaultAddress: String = defaultVaultAddress): P2PEthPoolVault {
|
||||
return P2PEthPoolVault(
|
||||
vaultAddress = vaultAddress,
|
||||
displayName = "Test Vault",
|
||||
apy = BigDecimal("3.5"),
|
||||
baseApy = BigDecimal("3.0"),
|
||||
capacity = BigDecimal("10000"),
|
||||
totalAssets = BigDecimal("5000"),
|
||||
feePercent = BigDecimal("10"),
|
||||
isPrivate = false,
|
||||
isGenesis = false,
|
||||
isSmoothingPool = false,
|
||||
isErc20 = false,
|
||||
tokenName = "Test Token",
|
||||
tokenSymbol = "TT",
|
||||
createdAt = 0L,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.currency.yieldSupplyKey
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.isStakingSupported
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
|
||||
|
|
@ -159,7 +159,7 @@ class TokenItemStateConverter(
|
|||
return totalAmount.format { crypto(currency) }
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data)
|
||||
private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data)
|
||||
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero()
|
||||
|
||||
private fun createTitleState(
|
||||
|
|
@ -266,12 +266,13 @@ class TokenItemStateConverter(
|
|||
val validators = stakingApyMap[stakingKey]
|
||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
|
||||
val yieldBalance = currencyStatus.value.yieldBalance
|
||||
val hasStakedBalance = yieldBalance is YieldBalance.Data
|
||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||
|
||||
val rateInfo: Pair<BigDecimal, Yield.RewardType?>? = if (hasStakedBalance) {
|
||||
val rateInfo: Pair<BigDecimal, Yield.RewardType?>? = if (stakeKitBalance != null) {
|
||||
// StakeKit-specific: try to find rate from validator address
|
||||
val validatorsByAddress = validators.associateBy { it.address }
|
||||
yieldBalance.balance.items
|
||||
stakeKitBalance.balance.items
|
||||
.mapNotNull { it.validatorAddress }
|
||||
.firstNotNullOfOrNull { address ->
|
||||
val validator = validatorsByAddress[address]
|
||||
|
|
@ -286,6 +287,8 @@ class TokenItemStateConverter(
|
|||
}
|
||||
.maxByOrNull { it.first }
|
||||
} else {
|
||||
// P2P or no balance: use preferred validators
|
||||
// TODO p2p
|
||||
validators
|
||||
.filter { it.preferred }
|
||||
.mapNotNull { validator ->
|
||||
|
|
@ -298,7 +301,7 @@ class TokenItemStateConverter(
|
|||
|
||||
return StakingLocalInfo(
|
||||
rate = rateInfo?.first,
|
||||
isActive = hasStakedBalance,
|
||||
isActive = stakingBalance != null,
|
||||
rewardType = rateInfo?.second,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,12 +83,14 @@ sealed class AnalyticsParam {
|
|||
data object Onboarding : ScreensSources("Onboarding")
|
||||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
data object HotWallet : ScreensSources("Hot Wallet")
|
||||
data object TangemPay : ScreensSources("Tangem Pay")
|
||||
data object WalletSettings : ScreensSources("Wallet Settings")
|
||||
data object Upgrade : ScreensSources("Upgrade")
|
||||
data object HardwareWallet : ScreensSources("Hardware Wallet")
|
||||
data object ImportWallet : ScreensSources("Import Wallet")
|
||||
data object CreateNewWallet : ScreensSources("Create New Wallet")
|
||||
data object AddNewWallet : ScreensSources("Add New Wallet")
|
||||
data object CreateWallet : ScreensSources("Create Wallet")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ sealed class Basic(
|
|||
) : Basic(
|
||||
event = "Card Was Scanned",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
AnalyticsParam.Key.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
|
||||
class SignedIn(
|
||||
class SignedInLegacy(
|
||||
currency: AnalyticsParam.WalletType,
|
||||
batch: String,
|
||||
signInType: SignInType,
|
||||
|
|
@ -24,8 +24,8 @@ sealed class Basic(
|
|||
) : Basic(
|
||||
event = "Signed in",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.CURRENCY, currency.value)
|
||||
put(AnalyticsParam.BATCH, batch)
|
||||
put(AnalyticsParam.Key.CURRENCY, currency.value)
|
||||
put(AnalyticsParam.Key.BATCH, batch)
|
||||
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
|
||||
put("Sign in type", signInType.name)
|
||||
put("Wallets Count", walletsCount)
|
||||
|
|
@ -39,10 +39,37 @@ sealed class Basic(
|
|||
}
|
||||
}
|
||||
|
||||
class SignedIn(
|
||||
signInType: SignInType,
|
||||
walletsCount: Int,
|
||||
) : Basic(
|
||||
event = "Signed in",
|
||||
params = buildMap {
|
||||
put("Sign in type", signInType.value)
|
||||
put("Wallets Count", walletsCount.toString())
|
||||
},
|
||||
) {
|
||||
enum class SignInType(val value: String) {
|
||||
Card("Card"),
|
||||
Biometric("Biometric"),
|
||||
NoSecurity("No Security"),
|
||||
AccessCode("Access Code"),
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonBuy(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
) : Basic(
|
||||
event = "Button - Buy",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.Key.SOURCE, source.value)
|
||||
},
|
||||
)
|
||||
|
||||
class ToppedUp(userWalletId: String, currency: AnalyticsParam.WalletType) :
|
||||
Basic(
|
||||
event = "Topped up",
|
||||
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
|
||||
params = mapOf(AnalyticsParam.Key.CURRENCY to currency.value),
|
||||
),
|
||||
OneTimeAnalyticsEvent {
|
||||
|
||||
|
|
@ -53,16 +80,16 @@ sealed class Basic(
|
|||
Basic(
|
||||
event = "Transaction sent",
|
||||
params = buildMap {
|
||||
this[AnalyticsParam.SOURCE] = sentFrom.value
|
||||
this[AnalyticsParam.Key.SOURCE] = sentFrom.value
|
||||
if (sentFrom is AnalyticsParam.TxData) {
|
||||
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
|
||||
this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token
|
||||
this[AnalyticsParam.Key.BLOCKCHAIN] = sentFrom.blockchain
|
||||
this[AnalyticsParam.Key.TOKEN_PARAM] = sentFrom.token
|
||||
sentFrom.feeType?.value?.let {
|
||||
this[AnalyticsParam.FEE_TYPE] = it
|
||||
this[AnalyticsParam.Key.FEE_TYPE] = it
|
||||
}
|
||||
}
|
||||
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
|
||||
this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType
|
||||
this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType
|
||||
}
|
||||
this["Memo"] = memoType.name
|
||||
},
|
||||
|
|
@ -79,7 +106,7 @@ sealed class Basic(
|
|||
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
|
||||
event = "Request Support",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
AnalyticsParam.Key.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -89,7 +116,7 @@ sealed class Basic(
|
|||
) : Basic(
|
||||
event = "Biometry Failed",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
AnalyticsParam.Key.SOURCE to source.value,
|
||||
"Reason" to reason.value,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.core.analytics.models.event
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class SignIn(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent("Sign In", event, params) {
|
||||
|
||||
data class ScreenOpened(
|
||||
val walletsCount: Int,
|
||||
) : SignIn(
|
||||
event = "Sign In Screen Opened",
|
||||
params = mapOf(
|
||||
"Wallets Count" to walletsCount.toString(),
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonUnlockAllWithBiometric : SignIn(event = "Button - Unlock All With Biometric")
|
||||
|
||||
class ButtonWallet(
|
||||
signInType: SignInType,
|
||||
) : SignIn(
|
||||
event = "Button - Wallet",
|
||||
params = buildMap {
|
||||
put("Sign in type", signInType.value)
|
||||
},
|
||||
) {
|
||||
enum class SignInType(val value: String) {
|
||||
Card("Card"),
|
||||
Biometric("Biometric"),
|
||||
NoSecurity("No Security"),
|
||||
AccessCode("Access Code"),
|
||||
}
|
||||
}
|
||||
|
||||
data class ButtonAddWallet(
|
||||
val sources: AnalyticsParam.ScreensSources,
|
||||
) : SignIn(
|
||||
event = "Button - Add Wallet",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to sources.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -23,4 +23,6 @@ interface TrackingContextProxy {
|
|||
fun addHotWalletContext()
|
||||
|
||||
fun removeContext()
|
||||
|
||||
fun proceedWithContext(userWallet: UserWallet, action: () -> Unit)
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>DoubleMutabilityForCollection:DevExcludedBlockchainsManager.kt$DevExcludedBlockchainsManager$private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()</ID>
|
||||
<ID>DoubleMutabilityForCollection:DevFeatureTogglesManager.kt$DevFeatureTogglesManager$private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()</ID>
|
||||
<ID>Indentation:ExcludedBlockchainToggles.kt$ExcludedBlockchainToggles$ </ID>
|
||||
<ID>Indentation:FeatureToggles.kt$FeatureToggles$ </ID>
|
||||
</CurrentIssues>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
},
|
||||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.32.0"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ internal class DevExcludedBlockchainsManager(
|
|||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private val fileBlockchainToggles: Map<String, Boolean> = getFileBlockchainToggles()
|
||||
|
||||
@Suppress("DoubleMutabilityForCollection")
|
||||
private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ internal class DevFeatureTogglesManager(
|
|||
) : MutableFeatureTogglesManager {
|
||||
|
||||
private val fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
|
||||
|
||||
@Suppress("DoubleMutabilityForCollection")
|
||||
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
init {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ enum class ApiEnvironment {
|
|||
@Json(name = "STAGE")
|
||||
STAGE,
|
||||
|
||||
@Json(name = "STAGE_2")
|
||||
STAGE_2,
|
||||
|
||||
@Json(name = "MOCK")
|
||||
MOCK,
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ internal class Express(
|
|||
createDev2Environment(),
|
||||
createDev3Environment(),
|
||||
createStageEnvironment(),
|
||||
createStage2Environment(),
|
||||
createMockedEnvironment(),
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
|
@ -73,6 +74,12 @@ internal class Express(
|
|||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createStage2Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE_2,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
|
|
@ -22,12 +23,7 @@ internal class P2PEthPool(
|
|||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ internal class YieldSupply(
|
|||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
-> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev
|
||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey
|
||||
} ?: error("No tangem tech api config provided")
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@ interface P2PEthPoolApi {
|
|||
* @param network Ethereum pool network: "mainnet" or "hoodi" (testnet)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/vaults")
|
||||
suspend fun getVaults(
|
||||
@Path("network") network: String = "mainnet",
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
suspend fun getVaults(@Path("network") network: String): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
|
||||
/**
|
||||
* Prepare deposit transaction
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ interface TangemTechApi {
|
|||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PUT("/v1/wallets/{walletId}/tokens")
|
||||
suspend fun saveTokens(
|
||||
@Path(value = "walletId") userId: String,
|
||||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
/** Returns referral status by [walletId] */
|
||||
@GET("v1/referral/{walletId}")
|
||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
|
||||
|
|
@ -129,6 +135,12 @@ interface TangemTechApi {
|
|||
@Body body: List<WalletIdBody>,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PUT("/v1/user-wallets/applications/{application_id}/wallets")
|
||||
suspend fun associateApplicationIdWithWalletsV2(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: AssociateApplicationIdWithWalletsBody,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("v1/user-wallets/wallets/{wallet_id}")
|
||||
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AssociateAppWithWalletsErrorResponse(
|
||||
@Json(name = "missingWalletIds") val missingWalletIds: List<String>,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AssociateApplicationIdWithWalletsBody(
|
||||
@Json(name = "walletIds") val walletIds: List<String>,
|
||||
)
|
||||
|
|
@ -19,6 +19,7 @@ data class GetWalletAccountsResponse(
|
|||
@Json(name = "group") val group: GroupType?,
|
||||
@Json(name = "sort") val sort: SortType?,
|
||||
@Json(name = "totalAccounts") val totalAccounts: Int,
|
||||
@Json(name = "totalArchivedAccounts") val totalArchivedAccounts: Int,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,4 +15,7 @@ interface AppCurrencyResponseStore {
|
|||
|
||||
/** Get [CurrenciesResponse.Currency] synchronously or null */
|
||||
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
|
||||
|
||||
/** Store [CurrenciesResponse.Currency] */
|
||||
suspend fun store(currency: CurrenciesResponse.Currency)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
|
|
@ -25,4 +26,11 @@ internal class DefaultAppCurrencyResponseStore(
|
|||
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun store(currency: CurrenciesResponse.Currency) {
|
||||
appPreferencesStore.storeObject(
|
||||
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
|
|||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
|
|
@ -77,6 +78,24 @@ internal object StakingStoreModule {
|
|||
return DefaultStakingActionsStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PBalancesPersistenceStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): DataStore<Map<String, Set<P2PEthPoolAccountResponse>>> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolVaultsStore(
|
||||
|
|
|
|||
|
|
@ -4,28 +4,32 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.BalanceItem
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.staking.YieldBalanceItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
class YieldBalanceConverter(
|
||||
/**
|
||||
* Converts StakeKit DTO to [StakingBalance].
|
||||
* Returns [StakingBalance.Data.StakeKit] for non-empty balances, [StakingBalance.Empty] otherwise.
|
||||
*/
|
||||
class StakingBalanceConverter(
|
||||
private val source: StatusSource,
|
||||
) : Converter<YieldBalanceWrapperDTO, YieldBalance?> {
|
||||
) : Converter<YieldBalanceWrapperDTO, StakingBalance?> {
|
||||
|
||||
constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL)
|
||||
|
||||
override fun convert(value: YieldBalanceWrapperDTO): YieldBalance? {
|
||||
override fun convert(value: YieldBalanceWrapperDTO): StakingBalance? {
|
||||
val stakingId = StakingID(
|
||||
integrationId = value.integrationId ?: return null,
|
||||
address = value.addresses.address,
|
||||
)
|
||||
|
||||
return if (value.balances.isEmpty()) {
|
||||
YieldBalance.Empty(stakingId = stakingId, source = source)
|
||||
StakingBalance.Empty(stakingId = stakingId, source = source)
|
||||
} else {
|
||||
YieldBalance.Data(
|
||||
StakingBalance.Data.StakeKit(
|
||||
stakingId = stakingId,
|
||||
balance = YieldBalanceItem(
|
||||
items = value.balances
|
||||
|
|
@ -12,6 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
|
|||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
|
|
@ -299,11 +300,17 @@ internal class ProdApiConfigsManagerTest {
|
|||
}
|
||||
|
||||
private fun createP2PModel(): TestModel {
|
||||
val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) {
|
||||
ApiEnvironment.DEV to "https://api-test.p2p.org/"
|
||||
} else {
|
||||
ApiEnvironment.PROD to "https://api.p2p.org/"
|
||||
}
|
||||
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.P2PEthPool,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
environment = environment,
|
||||
baseUrl = baseUrl,
|
||||
headers = mapOf(
|
||||
"Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" },
|
||||
"accept" to ProviderSuspend { "application/json" },
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
<string name="access_code_alert_skip_description">アクセスコードがないとウォレットは安全ではありません。</string>
|
||||
<string name="access_code_alert_skip_ok">とにかくスキップ</string>
|
||||
<string name="access_code_alert_skip_title">アクセスコードが設定されていません</string>
|
||||
<string name="access_code_alert_validation_cancel">コードを変更</string>
|
||||
<string name="access_code_alert_validation_description">アクセスコードは、ウォレットのロック解除・資産へのアクセス保護に使用されます</string>
|
||||
<string name="access_code_alert_validation_ok">このまま使用</string>
|
||||
<string name="access_code_alert_validation_title">このアクセスコードは簡単に推測される可能性があります</string>
|
||||
<string name="access_code_check_title">アクセスコードを入力</string>
|
||||
<string name="access_code_check_warining_delete">アクセスコードが間違っています。あと%s回間違えると、モバイルウォレットが削除されます。</string>
|
||||
<string name="access_code_check_warining_lock">アクセスコードが間違っています。あと%s回失敗すると、アプリはロックされます。</string>
|
||||
|
|
@ -773,6 +777,10 @@
|
|||
<string name="markets_token_details_volume">取引量</string>
|
||||
<string name="markets_tooltip_message">これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します</string>
|
||||
<string name="markets_tooltip_title">トークンを追加</string>
|
||||
<string name="markets_yield_supply_banner_description">資産を即時アクセス可能な状態に保ったまま、パワーアップさせよう。%s</string>
|
||||
<string name="markets_yield_supply_banner_title">利息モードを有効にする</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_body">モバイルウォレットを作成するには、%1$sにアップデートする必要があります</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_title">モバイルウォレットを使用するには、%1$s以降が必要です</string>
|
||||
<string name="news_all_news">すべてのニュース</string>
|
||||
<string name="news_stay_in_the_loop">最新情報を入手</string>
|
||||
<string name="nfc_error_unavailable">お使いのデバイスではNFCが使用できません</string>
|
||||
|
|
@ -1897,6 +1905,7 @@
|
|||
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangemは、生成された利息に対して15%サービス手数料も徴収します。</string>
|
||||
<string name="yield_module_high_fee_error">ネットワーク手数料が下がるか、残高が最低必要額に達すると、資金は自動的にAaveに供給されます。</string>
|
||||
<string name="yield_module_historical_returns">過去のリターン</string>
|
||||
<string name="yield_module_main_screen_promo_banner_message">保有資産に年利%1$s%%を適用</string>
|
||||
<string name="yield_module_main_view_approve_notification_description">利息モードでのトークンの承認が取り消されました。トークンを開いて再度許可してください。</string>
|
||||
<string name="yield_module_main_view_approve_notification_title">トークンの承認が必要です</string>
|
||||
<string name="yield_module_network_fee_unreachable_notification_description">ネットワーク接続を確認してください</string>
|
||||
|
|
@ -1911,7 +1920,9 @@
|
|||
<string name="yield_module_promo_screen_self_custodial_title">分散型・自己管理型</string>
|
||||
<string name="yield_module_promo_screen_terms_disclaimer">このサービスを利用することにより、プロバイダー\n%1$sおよび%2$sに同意するものとします。</string>
|
||||
<string name="yield_module_promo_screen_title">Aaveに接続</string>
|
||||
<string name="yield_module_promo_screen_title_v2">残高に %1$s%% の年利(APY)を適用</string>
|
||||
<string name="yield_module_promo_screen_variable_rate_info">Aave %1$s%% • 変動金利</string>
|
||||
<string name="yield_module_promo_screen_variable_rate_info_v2">変動金利</string>
|
||||
<string name="yield_module_provider">Aave</string>
|
||||
<string name="yield_module_rate_info_sheet_chart_average">平均%s</string>
|
||||
<string name="yield_module_rate_info_sheet_chart_title">昨年のリターン</string>
|
||||
|
|
@ -1938,12 +1949,16 @@
|
|||
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">利息モード</string>
|
||||
<string name="yield_module_token_details_earn_notification_processing">利息モードの有効化</string>
|
||||
<string name="yield_module_token_details_earn_notification_title">利息モード</string>
|
||||
<string name="yield_module_transaction_enter">利息モードが有効になりました</string>
|
||||
<string name="yield_module_transaction_deploy_contract">利息モードコントラクトのデプロイ</string>
|
||||
<string name="yield_module_transaction_enter">利息モードを有効にする</string>
|
||||
<string name="yield_module_transaction_enter_subtitle">%1$sがAaveに供給されました</string>
|
||||
<string name="yield_module_transaction_exit">利息モードが無効になりました</string>
|
||||
<string name="yield_module_transaction_exit">利息モードを無効にする</string>
|
||||
<string name="yield_module_transaction_exit_subtitle">%1$sがAaveから引き出されました</string>
|
||||
<string name="yield_module_transaction_initialize">利息モードをセットアップ</string>
|
||||
<string name="yield_module_transaction_reactivate">利息モードの再有効化</string>
|
||||
<string name="yield_module_transaction_topup">Aaveへの供給</string>
|
||||
<string name="yield_module_transaction_topup_subtitle">%1$sがAaveに供給されました</string>
|
||||
<string name="yield_module_transaction_withdraw">Aaveから引き出す</string>
|
||||
<string name="yield_module_transfer_mode_automatic">自動</string>
|
||||
<string name="yield_module_unable_to_cover_fee_description">取引のネットワーク手数料をカバーするために、 %1$s %2$sを追加してください。</string>
|
||||
<string name="yield_module_unable_to_cover_fee_title">%s手数料を支払えません</string>
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@
|
|||
<string name="common_from">Из</string>
|
||||
<string name="common_from_wallet_name">Из %s</string>
|
||||
<string name="common_generate_addresses">Синхронизировать адреса</string>
|
||||
<string name="common_get_started">Начать зарабатывать</string>
|
||||
<string name="common_get_started">Начать</string>
|
||||
<string name="common_get_token">Получить токен</string>
|
||||
<string name="common_go_to_provider">К провайдеру</string>
|
||||
<string name="common_go_to_token">Перейти в токен</string>
|
||||
|
|
@ -1075,7 +1075,7 @@
|
|||
<string name="save_user_wallet_agreement_notice">Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта или кольцо</string>
|
||||
<string name="scan_card_settings_button">Сканировать</string>
|
||||
<string name="scan_card_settings_message">Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку.</string>
|
||||
<string name="scan_card_settings_title">Приготовьте свой Tangem!</string>
|
||||
<string name="scan_card_settings_title">Приготовьте устройство Tangem!</string>
|
||||
<string name="security_alert_title">Уведомление безопасности</string>
|
||||
<string name="seed_warning_no">Нет</string>
|
||||
<string name="seed_warning_yes">Да</string>
|
||||
|
|
@ -1854,9 +1854,9 @@
|
|||
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Режим доходности</string>
|
||||
<string name="yield_module_token_details_earn_notification_processing">Включение режима доходности</string>
|
||||
<string name="yield_module_token_details_earn_notification_title">Режим доходности</string>
|
||||
<string name="yield_module_transaction_enter">Режим доходности включен</string>
|
||||
<string name="yield_module_transaction_enter">Включение режима доходности</string>
|
||||
<string name="yield_module_transaction_enter_subtitle">%1$s отправлено в Aave</string>
|
||||
<string name="yield_module_transaction_exit">Режим доходности выключен</string>
|
||||
<string name="yield_module_transaction_exit">Отключение режима доходности</string>
|
||||
<string name="yield_module_transaction_exit_subtitle">%1$s выведено из Aave</string>
|
||||
<string name="yield_module_transaction_topup">Перевод средств в Aave</string>
|
||||
<string name="yield_module_transaction_topup_subtitle">%1$s отправлено в Aave</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
<string name="access_code_alert_skip_description">Without an access code, your wallet is not secure.</string>
|
||||
<string name="access_code_alert_skip_ok">Skip anyway</string>
|
||||
<string name="access_code_alert_skip_title">Access code not set</string>
|
||||
<string name="access_code_alert_validation_cancel">Change code</string>
|
||||
<string name="access_code_alert_validation_description">Your access code will be used to unlock your wallet and to protect access to the assets</string>
|
||||
<string name="access_code_alert_validation_ok">Use anyway</string>
|
||||
<string name="access_code_alert_validation_title">This access code can be easily guessed</string>
|
||||
<string name="access_code_check_title">Enter access code</string>
|
||||
<string name="access_code_check_warining_delete">Wrong access code. Your mobile wallet will be deleted after %s more incorrect attempts.</string>
|
||||
<string name="access_code_check_warining_lock">Wrong access code. The app will be locked after %s more failed attempts</string>
|
||||
|
|
@ -788,6 +792,10 @@
|
|||
<string name="markets_token_details_volume">Volume</string>
|
||||
<string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string>
|
||||
<string name="markets_tooltip_title">Add tokens</string>
|
||||
<string name="markets_yield_supply_banner_description">Power up your assets while supplying them with instant access. %s</string>
|
||||
<string name="markets_yield_supply_banner_title">Activate Yield Mode</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_body">You must update to %1$s in order to create mobile wallet</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_title">Mobile Wallet requires %1$s or later</string>
|
||||
<string name="news_all_news">All news</string>
|
||||
<string name="news_stay_in_the_loop">Stay in the loop</string>
|
||||
<string name="nfc_error_unavailable">NFC is not available on your device</string>
|
||||
|
|
@ -1973,6 +1981,7 @@
|
|||
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem also takes a 15% service fee on yield generated.</string>
|
||||
<string name="yield_module_high_fee_error">Your funds will be automatically supplied to Aave once network fees are lower or your balance meets the minimum required amount.</string>
|
||||
<string name="yield_module_historical_returns">Historical returns</string>
|
||||
<string name="yield_module_main_screen_promo_banner_message">"Enable %1$s%% APY on your balance"</string>
|
||||
<string name="yield_module_main_view_approve_notification_description">Approval for your token in Yield Mode has been revoked. Open the token to grant permission again.</string>
|
||||
<string name="yield_module_main_view_approve_notification_title">Token approval needed</string>
|
||||
<string name="yield_module_network_fee_unreachable_notification_description">Check your network connection</string>
|
||||
|
|
@ -2016,12 +2025,16 @@
|
|||
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Yield Mode</string>
|
||||
<string name="yield_module_token_details_earn_notification_processing">Enabling Yield Mode</string>
|
||||
<string name="yield_module_token_details_earn_notification_title">Yield Mode</string>
|
||||
<string name="yield_module_transaction_enter">Yield Mode enabled</string>
|
||||
<string name="yield_module_transaction_deploy_contract">Yield Mode contract deploy</string>
|
||||
<string name="yield_module_transaction_enter">Yield Mode enable</string>
|
||||
<string name="yield_module_transaction_enter_subtitle">%1$s supplied to Aave</string>
|
||||
<string name="yield_module_transaction_exit">Yield Mode disabled</string>
|
||||
<string name="yield_module_transaction_exit">Yield Mode disable</string>
|
||||
<string name="yield_module_transaction_exit_subtitle">%1$s withdrawn from Aave</string>
|
||||
<string name="yield_module_transaction_initialize">Yield Mode initialize</string>
|
||||
<string name="yield_module_transaction_reactivate">Yield Mode reactivate</string>
|
||||
<string name="yield_module_transaction_topup">Supply to Aave</string>
|
||||
<string name="yield_module_transaction_topup_subtitle">%1$s supplied to Aave</string>
|
||||
<string name="yield_module_transaction_withdraw">Withdraw from Aave</string>
|
||||
<string name="yield_module_transfer_mode_automatic">Automatic</string>
|
||||
<string name="yield_module_unable_to_cover_fee_description">Add some %1$s %2$s to cover the network fee for transactions.</string>
|
||||
<string name="yield_module_unable_to_cover_fee_title">Unable to cover %s fee</string>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,30 @@ object Dialogs {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot wallet creation not supported dialog
|
||||
*
|
||||
* @param leastSupportedVersion least supported OS version name ex. "Android 10"
|
||||
* @param onDismiss lambda be invoked when dialog is dismissed
|
||||
*/
|
||||
fun hotWalletCreationNotSupportedDialog(leastSupportedVersion: String, onDismiss: () -> Unit = {}): DialogMessage {
|
||||
return DialogMessage(
|
||||
title = resourceReference(
|
||||
id = R.string.mobile_wallet_requires_min_os_warning_title,
|
||||
formatArgs = wrappedList(leastSupportedVersion),
|
||||
),
|
||||
message = resourceReference(
|
||||
id = R.string.mobile_wallet_requires_min_os_warning_body,
|
||||
formatArgs = wrappedList(leastSupportedVersion),
|
||||
),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_got_it),
|
||||
onClick = {},
|
||||
),
|
||||
onDismissRequest = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal error dialog
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@
|
|||
<ID>MultilineLambdaItParameter:DefaultAccountsCRUDRepository.kt$DefaultAccountsCRUDRepository${ if (it is HttpException && it.code == HttpException.Code.NOT_MODIFIED) { null } else { throw it } }</ID>
|
||||
<ID>MultilineLambdaItParameter:GetWalletAccountsResponseExt.kt${ enrichedTokensByAccountId[it].orEmpty().map { token -> // Tokens from unexisting accounts should be copied to the main account token.copy(accountId = accountDTO.id) } }</ID>
|
||||
<ID>NoNameShadowing:GetWalletAccountsResponseExt.kt$tokens</ID>
|
||||
<ID>NullableToStringCall:AccountListCryptoCurrenciesProducer.kt$AccountListCryptoCurrenciesProducer$${this::class.simpleName}</ID>
|
||||
<ID>NullableToStringCall:DefaultMultiWalletCryptoCurrenciesProducer.kt$DefaultMultiWalletCryptoCurrenciesProducer$${this::class.simpleName}</ID>
|
||||
<ID>UnnecessaryLet:DefaultAccountsCRUDRepository.kt$DefaultAccountsCRUDRepository$let(AccountName::invoke)</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ internal class AccountListConverter @AssistedInject constructor(
|
|||
userWalletId = userWallet.walletId,
|
||||
accounts = value.accounts.map(cryptoPortfolioConverter::convert),
|
||||
totalAccounts = value.wallet.totalAccounts,
|
||||
totalArchivedAccounts = value.wallet.totalArchivedAccounts,
|
||||
sortType = sortType,
|
||||
groupType = groupType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal class GetWalletAccountsResponseConverter @AssistedInject constructor(
|
|||
group = TokensGroupTypeConverter.convertBack(value.groupType),
|
||||
sort = TokensSortTypeConverter.convertBack(value.sortType),
|
||||
totalAccounts = value.totalAccounts,
|
||||
totalArchivedAccounts = value.totalArchivedAccounts,
|
||||
),
|
||||
accounts = value.accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
|
|
|||
|
|
@ -94,28 +94,30 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
override suspend fun push(
|
||||
userWalletId: UserWalletId,
|
||||
body: SaveWalletAccountsResponse,
|
||||
): GetWalletAccountsResponse? {
|
||||
return pushInternal(userWalletId = userWalletId, body = body)
|
||||
}
|
||||
|
||||
private suspend fun pushInternal(
|
||||
userWalletId: UserWalletId,
|
||||
body: SaveWalletAccountsResponse,
|
||||
eTag: String? = null,
|
||||
): GetWalletAccountsResponse? {
|
||||
return safeApiCall(
|
||||
call = {
|
||||
var eTag = getETag(userWalletId)
|
||||
|
||||
if (eTag == null) {
|
||||
fetch(userWalletId)
|
||||
|
||||
eTag = getETag(userWalletId) ?: error("ETag is null after fetch")
|
||||
}
|
||||
val resolvedETag = eTag ?: getETagForPush(userWalletId)
|
||||
|
||||
val apiResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
eTag = resolvedETag,
|
||||
body = body,
|
||||
)
|
||||
}
|
||||
|
||||
saveETag(userWalletId, apiResponse)
|
||||
|
||||
apiResponse.bind()
|
||||
apiResponse.bind().enrichByAccountId()
|
||||
},
|
||||
onError = { error ->
|
||||
if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) {
|
||||
|
|
@ -127,6 +129,19 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun getETagForPush(userWalletId: UserWalletId): String {
|
||||
var savedETag = getETag(userWalletId)
|
||||
|
||||
if (savedETag == null) {
|
||||
fetch(userWalletId)
|
||||
|
||||
savedETag = getETag(userWalletId)
|
||||
?: error("Failed to retrieve ETag after fetching wallet accounts for wallet $userWalletId")
|
||||
}
|
||||
|
||||
return savedETag
|
||||
}
|
||||
|
||||
private suspend fun fetchWalletAccounts(
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
|
|
@ -142,10 +157,11 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
|
||||
saveETag(userWalletId, apiResponse)
|
||||
|
||||
val responseBody = apiResponse.bind()
|
||||
store(userWalletId = userWalletId, response = responseBody)
|
||||
val response = apiResponse.bind().enrichByAccountId()
|
||||
|
||||
FetchResult(responseBody)
|
||||
store(userWalletId = userWalletId, response = response)
|
||||
|
||||
FetchResult(response)
|
||||
},
|
||||
onError = { throwable ->
|
||||
// pushWalletAccounts and storeWalletAccounts help to avoid cyclic dependency
|
||||
|
|
@ -153,7 +169,13 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
error = throwable,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
pushWalletAccounts = ::push,
|
||||
pushWalletAccounts = { accounts, eTag ->
|
||||
pushInternal(
|
||||
userWalletId = userWalletId,
|
||||
body = SaveWalletAccountsResponse(accounts),
|
||||
eTag = eTag,
|
||||
)
|
||||
},
|
||||
storeWalletAccounts = ::store,
|
||||
)
|
||||
},
|
||||
|
|
@ -215,6 +237,18 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun GetWalletAccountsResponse.enrichByAccountId(): GetWalletAccountsResponse {
|
||||
return copy(
|
||||
accounts = accounts.map { accountDTO ->
|
||||
accountDTO.copy(
|
||||
tokens = accountDTO.tokens?.map { token ->
|
||||
token.copy(accountId = accountDTO.id)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAccountsResponseStore(userWalletId: UserWalletId): AccountsResponseStore {
|
||||
return accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.data.account.fetcher
|
|||
|
||||
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -29,10 +28,10 @@ import javax.inject.Singleton
|
|||
* Handles errors that occur during the fetching of wallet accounts
|
||||
*
|
||||
* @property tangemTechApi API for network requests
|
||||
* @property userWalletsStore provides access to user wallets storage
|
||||
* @property userTokensSaver saves user tokens to the storage
|
||||
* @property userTokensResponseStore provides access to user token responses.
|
||||
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
|
||||
* @property eTagsStore store for ETags to manage caching
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
* @see DefaultWalletAccountsFetcher
|
||||
|
|
@ -47,7 +46,6 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
private val userTokensSaver: UserTokensSaver,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
|
||||
private val eTagsStore: ETagsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
|
|
@ -66,7 +64,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
error: ApiResponseError,
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse?,
|
||||
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
|
||||
): FetchResult {
|
||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||
|
|
@ -87,9 +85,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
val eTag = createWallet(userWalletId)
|
||||
|
||||
if (eTag != null) {
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag)
|
||||
|
||||
pushWalletAccounts(userWalletId, accountDTOs)
|
||||
pushWalletAccounts(accountDTOs, eTag)
|
||||
userTokensSaver.pushWithRetryer(userWalletId, userTokensResponse)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,11 +36,12 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
|
|||
|
||||
override val fallback: Option<Set<CryptoCurrency>> = emptySet<CryptoCurrency>().some()
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
override fun produce(): Flow<Set<CryptoCurrency>> {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
|
||||
if (!userWallet.isMultiCurrency) {
|
||||
error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet")
|
||||
}
|
||||
|
||||
return accountsResponseStoreFactory.create(userWalletId = userWallet.walletId).data
|
||||
|
|
@ -49,10 +50,13 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
|
|||
if (response == null) return@map emptySet()
|
||||
|
||||
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
|
||||
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
|
||||
?: return@map emptySet()
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = accountDTO.tokens.orEmpty(),
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import arrow.core.some
|
|||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
|
|
@ -39,7 +40,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constr
|
|||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
|
||||
if (!userWallet.isMultiCurrency) {
|
||||
error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet")
|
||||
}
|
||||
|
||||
return userTokensResponseStore.get(userWalletId = params.userWalletId)
|
||||
|
|
@ -50,6 +51,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constr
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = response,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
).toSet()
|
||||
}
|
||||
.onEmpty { emit(emptySet()) }
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
|
||||
val unassignedTokens = mainAccount.findUnassignedTokens(derivationIndex)
|
||||
|
||||
if (unassignedTokens == null) {
|
||||
if (unassignedTokens.isNullOrEmpty()) {
|
||||
Timber.i("No unassigned tokens found for migration")
|
||||
return@either
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor(
|
|||
group = response.group,
|
||||
sort = response.sort,
|
||||
totalAccounts = accountDTOs.size,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = response.tokens),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ internal fun createGetWalletAccountsResponse(
|
|||
group = groupType,
|
||||
sort = sortType,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = buildList {
|
||||
createWalletAccountDTO(
|
||||
|
|
@ -79,6 +80,7 @@ internal fun createAccountList(
|
|||
userWalletId = userWalletId,
|
||||
accounts = listOf(createCryptoPortfolio(userWalletId)),
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
sortType = sortType,
|
||||
groupType = groupType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ class AccountListConverterTest {
|
|||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.data.account.fetcher
|
|||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
|
|
@ -38,7 +37,6 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
|
||||
private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true)
|
||||
|
||||
private val handler = FetchWalletAccountsErrorHandler(
|
||||
tangemTechApi = tangemTechApi,
|
||||
|
|
@ -46,17 +44,18 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
userTokensSaver = userTokensSaver,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||
eTagsStore = eTagsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse =
|
||||
private val pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse =
|
||||
mockk(relaxed = true)
|
||||
private val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(
|
||||
tangemTechApi,
|
||||
userWalletsStore,
|
||||
userTokensSaver,
|
||||
userTokensResponseStore,
|
||||
defaultWalletAccountsResponseFactory,
|
||||
|
|
@ -129,7 +128,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
),
|
||||
)
|
||||
} returns apiResponse
|
||||
coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse
|
||||
coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
|
|
@ -150,8 +149,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
walletType = WalletType.COLD,
|
||||
),
|
||||
)
|
||||
eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue)
|
||||
pushWalletAccounts(userWalletId, listOf(accountDTO))
|
||||
pushWalletAccounts(listOf(accountDTO), eTagValue)
|
||||
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +180,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(accountDTO),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
|||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
|
|
@ -72,7 +73,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
}
|
||||
|
||||
verify(inverse = true) {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any())
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any(), accountIndex = any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
} returns cryptoCurrencies.toList()
|
||||
|
||||
|
|
@ -122,6 +124,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = updatedUserTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
} returns updatedCryptoCurrencies.toList()
|
||||
|
||||
|
|
@ -144,6 +147,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +166,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = updatedUserTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -186,6 +191,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
} returns cryptoCurrencies.toList()
|
||||
|
||||
|
|
@ -208,6 +214,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -252,6 +259,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
} returns cryptoCurrencies.toList()
|
||||
|
||||
|
|
@ -283,6 +291,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -307,7 +316,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
}
|
||||
|
||||
verify(inverse = true) {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any())
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any(), accountIndex = any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,7 +344,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
|
||||
verify(inverse = true) {
|
||||
userTokensResponseStore.get(any())
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any())
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any(), accountIndex = any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ class DefaultMainAccountTokensMigrationTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(mainAccount, selectedAccount),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -135,6 +136,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = defaultResponse.group,
|
||||
sort = defaultResponse.sort,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(accountsDTO.copy(tokens = listOf(token))),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -190,6 +192,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = defaultResponse.group,
|
||||
sort = defaultResponse.sort,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -228,6 +231,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
group = userTokensResponse.group,
|
||||
sort = userTokensResponse.sort,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(accountsDTO.copy(tokens = userTokensResponse.tokens)),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -55,6 +56,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -84,6 +86,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account1, account2, account3),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -110,6 +113,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -141,6 +145,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(token2),
|
||||
|
|
@ -178,6 +183,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account1, account2),
|
||||
unassignedTokens = listOf(token1, token2),
|
||||
|
|
@ -217,6 +223,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
|
|
@ -248,6 +255,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
totalArchivedAccounts = 0,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(token1, token2),
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ import com.tangem.data.common.api.safeApiCall
|
|||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -24,7 +20,7 @@ import org.joda.time.Duration
|
|||
|
||||
internal class DefaultAppCurrencyRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
private val availableAppCurrenciesStore: AvailableAppCurrenciesStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -35,15 +31,15 @@ internal class DefaultAppCurrencyRepository(
|
|||
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
|
||||
return channelFlow {
|
||||
launch {
|
||||
appPreferencesStore
|
||||
.getObject<CurrenciesResponse.Currency>(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
|
||||
appCurrencyResponseStore
|
||||
.get()
|
||||
.filterNotNull()
|
||||
.map(appCurrencyConverter::convert)
|
||||
.collect(::send)
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
if (appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY) == null) {
|
||||
if (appCurrencyResponseStore.getSyncOrNull() == null) {
|
||||
fetchDefaultAppCurrency()
|
||||
}
|
||||
}
|
||||
|
|
@ -70,17 +66,14 @@ internal class DefaultAppCurrencyRepository(
|
|||
"Unable to find app currency with provided code: $currencyCode"
|
||||
}
|
||||
|
||||
appPreferencesStore.storeObject(
|
||||
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
value = currency,
|
||||
)
|
||||
appCurrencyResponseStore.store(currency)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchDefaultAppCurrency(isRefresh: Boolean) {
|
||||
withContext(dispatchers.io) {
|
||||
fetchAvailableCurrenciesIfExpired(isRefresh)
|
||||
val appCurrency = appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
|
||||
val appCurrency = appCurrencyResponseStore.getSyncOrNull()?.code
|
||||
changeAppCurrency(appCurrency ?: DEFAULT_CURRENCY_CODE)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package com.tangem.data.appcurrency.di
|
|||
import com.tangem.data.appcurrency.DefaultAppCurrencyRepository
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -21,17 +21,17 @@ internal object AppCurrencyDataModule {
|
|||
@Singleton
|
||||
fun provideAppCurrencyRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
availableAppCurrenciesStore: AvailableAppCurrenciesStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AppCurrencyRepository {
|
||||
return DefaultAppCurrencyRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
availableAppCurrenciesStore = availableAppCurrenciesStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
dispatchers = dispatchers,
|
||||
appCurrencyResponseStore = appCurrencyResponseStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets)
|
||||
|
||||
/* Libs - SDK */
|
||||
|
|
|
|||
|
|
@ -144,6 +144,9 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
?: return emptyMap()
|
||||
|
||||
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
|
||||
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
|
||||
?: return@flatMapTo emptySet()
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = accountDTO.tokens.orEmpty().filter { token ->
|
||||
networks.any {
|
||||
|
|
@ -151,7 +154,7 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
}
|
||||
},
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -163,6 +166,7 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
|
||||
},
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
.groupBy(CryptoCurrency::network)
|
||||
|
|
@ -181,10 +185,13 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
?: return emptyMap()
|
||||
|
||||
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
|
||||
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
|
||||
?: return@flatMapTo emptySet()
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -194,6 +201,7 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = response.tokens.filter { token -> token.networkId in networkIds },
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
.groupBy { it.network.id.rawId }
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
fun createCurrencies(
|
||||
response: UserTokensResponse,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
accountIndex: DerivationIndex,
|
||||
): List<CryptoCurrency> {
|
||||
return createCurrencies(tokens = response.tokens, userWallet = userWallet, accountIndex = accountIndex)
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
fun createCurrencies(
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
accountIndex: DerivationIndex,
|
||||
): List<CryptoCurrency> {
|
||||
return tokens
|
||||
.asSequence()
|
||||
|
|
@ -42,7 +42,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
fun createCurrency(
|
||||
responseToken: UserTokensResponse.Token,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
accountIndex: DerivationIndex,
|
||||
): CryptoCurrency? {
|
||||
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
|
|
@ -103,7 +103,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
blockchain: Blockchain,
|
||||
responseToken: UserTokensResponse.Token,
|
||||
network: Network,
|
||||
): CryptoCurrency.Coin? {
|
||||
): CryptoCurrency.Coin {
|
||||
return CryptoCurrency.Coin(
|
||||
id = getCoinId(network, blockchain.toCoinId()),
|
||||
network = network,
|
||||
|
|
@ -127,7 +127,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createToken(blockchain: Blockchain, sdkToken: Token, network: Network): CryptoCurrency.Token? {
|
||||
private fun createToken(blockchain: Blockchain, sdkToken: Token, network: Network): CryptoCurrency.Token {
|
||||
val id = getTokenId(network, sdkToken)
|
||||
|
||||
return CryptoCurrency.Token(
|
||||
|
|
|
|||
|
|
@ -1,55 +1,45 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class UserTokensResponseAddressesEnricher @Inject constructor(
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse {
|
||||
val isNotificationsEnabled = walletsRepository.isNotificationsEnabled(userWalletId)
|
||||
|
||||
return withContext(dispatchers.default) {
|
||||
val networksStatuses = if (isNotificationsEnabled) {
|
||||
withTimeoutOrNull(
|
||||
FETCH_TIMEOUT_SECONDS.seconds,
|
||||
{ multiNetworkStatusSupplier.invoke(MultiNetworkStatusProducer.Params(userWalletId)).first() },
|
||||
).orEmpty()
|
||||
val addressByToken = if (isNotificationsEnabled) {
|
||||
response.tokens.associateWith { token ->
|
||||
val blockchain = Blockchain.fromNetworkId(token.networkId) ?: return@associateWith null
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
|
||||
walletManager?.wallet?.addresses?.map(Address::value)
|
||||
}
|
||||
} else {
|
||||
emptySet()
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val enrichedTokens = response.tokens.map { token ->
|
||||
if (isNotificationsEnabled) {
|
||||
val matchingNetwork = networksStatuses.find { status ->
|
||||
status.network.backendId == token.networkId &&
|
||||
status.network.derivationPath.value == token.derivationPath
|
||||
} ?: return@map token
|
||||
|
||||
val networkAddress = when (matchingNetwork.value) {
|
||||
is NetworkStatus.Verified -> (matchingNetwork.value as NetworkStatus.Verified).address
|
||||
is NetworkStatus.NoAccount -> (matchingNetwork.value as NetworkStatus.NoAccount).address
|
||||
else -> null
|
||||
}
|
||||
|
||||
val addresses = networkAddress
|
||||
?.availableAddresses
|
||||
?.map { it.value }
|
||||
?.toList()
|
||||
.orEmpty()
|
||||
val addresses = addressByToken[token] ?: return@map token
|
||||
|
||||
token.copy(addresses = addresses)
|
||||
} else {
|
||||
|
|
@ -60,8 +50,4 @@ class UserTokensResponseAddressesEnricher @Inject constructor(
|
|||
response.copy(tokens = enrichedTokens, notifyStatus = isNotificationsEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FETCH_TIMEOUT_SECONDS = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,17 @@ package com.tangem.data.common.currency
|
|||
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.isNetworkError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.retryer.Retryer
|
||||
|
|
@ -50,24 +55,26 @@ class UserTokensSaver(
|
|||
response: UserTokensResponse,
|
||||
useEnricher: Boolean = true,
|
||||
onFailSend: () -> Unit = {},
|
||||
) {
|
||||
withContext(dispatchers.default) {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId)
|
||||
) = withContext(dispatchers.io) {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId)
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.")
|
||||
onFailSend()
|
||||
return@withContext
|
||||
}
|
||||
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
|
||||
|
||||
pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
|
||||
} else {
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
|
||||
walletName = userWallet?.name,
|
||||
walletName = userWallet.name,
|
||||
walletType = WalletType.from(userWallet),
|
||||
)
|
||||
|
||||
safeApiCall(
|
||||
call = {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = enrichedResponse)
|
||||
.bind()
|
||||
}
|
||||
},
|
||||
onError = { onFailSend() },
|
||||
)
|
||||
pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +95,39 @@ class UserTokensSaver(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) {
|
||||
safeApiCall(
|
||||
call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() },
|
||||
onError = { onFailSend() },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
val apiResponse = tangemTechApi.saveTokens(
|
||||
userId = userWallet.walletId.stringValue,
|
||||
userTokens = response,
|
||||
)
|
||||
|
||||
val isWalletNotFound = apiResponse is ApiResponse.Error &&
|
||||
apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND)
|
||||
|
||||
if (isWalletNotFound) {
|
||||
tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind()
|
||||
|
||||
tangemTechApi.saveTokens(
|
||||
userId = userWallet.walletId.stringValue,
|
||||
userTokens = response,
|
||||
).bind()
|
||||
} else {
|
||||
apiResponse.bind()
|
||||
}
|
||||
},
|
||||
onError = { onFailSend() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.applyCompatibility(): UserTokensResponse {
|
||||
return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.tangem.datasource.local.token.UserTokensResponseStore
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.retryer.RetryerPool
|
||||
|
|
@ -54,12 +54,12 @@ internal object DataCommonModule {
|
|||
@Singleton
|
||||
fun provideUserTokensEncricher(
|
||||
walletsRepository: WalletsRepository,
|
||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): UserTokensResponseAddressesEnricher {
|
||||
return UserTokensResponseAddressesEnricher(
|
||||
walletsRepository = walletsRepository,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ class NetworkFactory @Inject constructor(
|
|||
blockchain = blockchain,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
),
|
||||
shouldCheckChia = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -128,9 +129,10 @@ class NetworkFactory @Inject constructor(
|
|||
derivationPath: Network.DerivationPath,
|
||||
canHandleTokens: Boolean,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
shouldCheckChia: Boolean = true,
|
||||
): Network? {
|
||||
if (!blockchain.isBlockchainSupported()) return null
|
||||
if (blockchain == Blockchain.Chia && accountIndex != DerivationIndex.Main) return null
|
||||
if (shouldCheckChia && blockchain == Blockchain.Chia && accountIndex != DerivationIndex.Main) return null
|
||||
|
||||
return runCatching {
|
||||
Network(
|
||||
|
|
|
|||
|
|
@ -1,92 +1,68 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class UserTokensResponseAddressesEnricherTest {
|
||||
|
||||
private lateinit var walletsRepository: WalletsRepository
|
||||
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
|
||||
private lateinit var multiNetworkStatusSupplier: MultiNetworkStatusSupplier
|
||||
private lateinit var enricher: UserTokensResponseAddressesEnricher
|
||||
private val walletsRepository: WalletsRepository = mockk()
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
private val enricher: UserTokensResponseAddressesEnricher = UserTokensResponseAddressesEnricher(
|
||||
walletsRepository = walletsRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
walletsRepository = mockk()
|
||||
multiNetworkStatusSupplier = mockk()
|
||||
private val userWalletId = UserWalletId("1234567890abcdef")
|
||||
|
||||
enricher = UserTokensResponseAddressesEnricher(
|
||||
walletsRepository = walletsRepository,
|
||||
dispatchers = dispatchers,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN notifications are disabled globally WHEN invoke THEN return original response`() = runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(response)
|
||||
clearMocks(walletsRepository, walletManagersFacade)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN notifications are disabled for wallet WHEN invoke THEN return response with empty addresses`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
val walletManager = mockk<WalletManager> {
|
||||
val wallet = mockk<Wallet> {
|
||||
every { addresses } returns setOf(
|
||||
Address(value = "0x12345", type = AddressType.Default),
|
||||
)
|
||||
}
|
||||
|
||||
every { this@mockk.wallet } returns wallet
|
||||
}
|
||||
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns false
|
||||
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
} returns flowOf(
|
||||
setOf(
|
||||
NetworkStatus(
|
||||
network = mockk {
|
||||
every { backendId } returns "ethereum"
|
||||
every { derivationPath.value } returns "m/44'/60'/0'/0/0"
|
||||
},
|
||||
value = NetworkStatus.Verified(
|
||||
address = mockk {
|
||||
every { availableAddresses } returns emptySet()
|
||||
},
|
||||
amounts = emptyMap(),
|
||||
pendingTransactions = emptyMap(),
|
||||
yieldSupplyStatuses = emptyMap(),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
} returns walletManager
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
|
@ -100,75 +76,52 @@ class UserTokensResponseAddressesEnricherTest {
|
|||
fun `GIVEN notifications are enabled and addresses available WHEN invoke THEN return enriched response`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
val addresses = listOf("0x123", "0x456")
|
||||
val addresses = setOf(
|
||||
Address(value = "0x123", type = AddressType.Default),
|
||||
Address(value = "0x456", type = AddressType.Legacy),
|
||||
)
|
||||
|
||||
val walletManager = mockk<WalletManager> {
|
||||
val wallet = mockk<Wallet> {
|
||||
every { this@mockk.addresses } returns addresses
|
||||
}
|
||||
|
||||
every { this@mockk.wallet } returns wallet
|
||||
}
|
||||
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
} returns flowOf(
|
||||
setOf(
|
||||
NetworkStatus(
|
||||
network = mockk {
|
||||
every { backendId } returns "ethereum"
|
||||
every { derivationPath.value } returns "m/44'/60'/0'/0/0"
|
||||
},
|
||||
value = NetworkStatus.Verified(
|
||||
address = mockk {
|
||||
every { availableAddresses } returns addresses.map { address ->
|
||||
mockk<NetworkAddress.Address> {
|
||||
every { value } returns address
|
||||
}
|
||||
}.toSet()
|
||||
},
|
||||
amounts = emptyMap(),
|
||||
pendingTransactions = emptyMap(),
|
||||
yieldSupplyStatuses = emptyMap(),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
} returns walletManager
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
||||
// THEN
|
||||
assertThat(result.tokens).hasSize(1)
|
||||
assertThat(result.tokens[0].addresses).containsExactlyElementsIn(addresses)
|
||||
assertThat(result.tokens[0].addresses).containsExactlyElementsIn(addresses.map { it.value })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN notifications are enabled but no matching network WHEN invoke THEN return original token`() = runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
} returns flowOf(
|
||||
setOf(
|
||||
NetworkStatus(
|
||||
network = mockk {
|
||||
every { backendId } returns "bitcoin"
|
||||
every { derivationPath.value } returns "m/44'/0'/0'/0/0"
|
||||
},
|
||||
value = NetworkStatus.Verified(
|
||||
address = mockk {
|
||||
every { availableAddresses } returns emptySet()
|
||||
},
|
||||
amounts = emptyMap(),
|
||||
pendingTransactions = emptyMap(),
|
||||
yieldSupplyStatuses = emptyMap(),
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationPath = token.derivationPath,
|
||||
)
|
||||
} returns null
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class UserTokensSaverTest {
|
|||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
tangemTechApi.saveUserTokens(any(), any())
|
||||
tangemTechApi.saveTokens(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,8 @@ class UserTokensSaverTest {
|
|||
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
coEvery { enricher(userWalletId, response) } returns enrichedResponse
|
||||
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
||||
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
||||
coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
||||
|
||||
// WHEN
|
||||
userTokensSaver.push(
|
||||
|
|
@ -120,7 +121,7 @@ class UserTokensSaverTest {
|
|||
// THEN
|
||||
coVerifyOrder {
|
||||
enricher(userWalletId, response)
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
|
||||
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
|
||||
}
|
||||
|
||||
assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" }
|
||||
|
|
@ -155,7 +156,7 @@ class UserTokensSaverTest {
|
|||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
coEvery { enricher(userWalletId, response) } returns enrichedResponse
|
||||
coEvery {
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
|
||||
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
|
||||
} returns ApiResponse.Success(Unit)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -164,7 +165,7 @@ class UserTokensSaverTest {
|
|||
// THEN
|
||||
coVerifyOrder {
|
||||
enricher(userWalletId, response)
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
|
||||
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:DefaultFeedbackRepository.kt$DefaultFeedbackRepository$private val useNewUserWalletsRepository: Boolean</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultFeedbackRepository.kt$DefaultFeedbackRepository${ it.toMutableMap().apply { put(userWallet.walletId, error) } }</ID>
|
||||
<ID>UseOrEmpty:BlockchainInfoConverter.kt$BlockchainInfoConverter$value.wallet.publicKey.derivationPath?.rawPath ?: ""</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -26,7 +26,7 @@ import java.io.File
|
|||
*
|
||||
* @property appLogsStore app logs store
|
||||
* @property userWalletsListManager user wallets list manager
|
||||
* @property useNewUserWalletsRepository flag to use new user wallets repository
|
||||
* @property shouldUseNewUserWalletsRepository flag to use new user wallets repository
|
||||
* @property userWalletsListRepository user wallets repository
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property emailSender email sender
|
||||
|
|
@ -37,7 +37,7 @@ import java.io.File
|
|||
@Suppress("LongParameterList")
|
||||
internal class DefaultFeedbackRepository(
|
||||
private val appLogsStore: AppLogsStore,
|
||||
private val useNewUserWalletsRepository: Boolean,
|
||||
private val shouldUseNewUserWalletsRepository: Boolean,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
|
|
@ -97,9 +97,9 @@ internal class DefaultFeedbackRepository(
|
|||
override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) {
|
||||
val userWallet = getSelectedWalletUseCase.sync().getOrNull() ?: error("UserWallet is not selected")
|
||||
|
||||
blockchainsErrors.update {
|
||||
it.toMutableMap().apply {
|
||||
put(userWallet.walletId, error)
|
||||
blockchainsErrors.update { map ->
|
||||
map.toMutableMap().apply {
|
||||
this[userWallet.walletId] = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ internal class DefaultFeedbackRepository(
|
|||
}
|
||||
|
||||
private suspend fun getUserWalletById(userWalletId: UserWalletId): UserWallet? {
|
||||
return if (useNewUserWalletsRepository) {
|
||||
return if (shouldUseNewUserWalletsRepository) {
|
||||
userWalletsListRepository.userWalletsSync().find { it.walletId == userWalletId }
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync.find { it.walletId == userWalletId }
|
||||
|
|
@ -134,7 +134,7 @@ internal class DefaultFeedbackRepository(
|
|||
}
|
||||
|
||||
private fun totalUserWallets(): Int {
|
||||
return if (useNewUserWalletsRepository) {
|
||||
return if (shouldUseNewUserWalletsRepository) {
|
||||
userWalletsListRepository.userWallets.value?.size ?: 0
|
||||
} else {
|
||||
userWalletsListManager.walletsCount
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainA
|
|||
internal object BlockchainInfoConverter : Converter<WalletManager, BlockchainInfo> {
|
||||
|
||||
override fun convert(value: WalletManager): BlockchainInfo {
|
||||
val derivationPath = value.wallet.publicKey.derivationPath
|
||||
|
||||
return BlockchainInfo(
|
||||
blockchain = value.wallet.blockchain.fullName,
|
||||
derivationPath = value.wallet.publicKey.derivationPath?.rawPath ?: "",
|
||||
derivationPath = derivationPath?.rawPath.orEmpty(),
|
||||
outputsCount = value.outputsCount?.toString(),
|
||||
host = value.currentHost,
|
||||
addresses = value.wallet.mapAddresses(Address::value),
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ internal object FeedbackModule {
|
|||
emailSender = emailSender,
|
||||
appVersionProvider = appVersionProvider,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
useNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
||||
shouldUseNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.data.hotwallet
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
|
|
@ -12,6 +14,13 @@ internal class DefaultHotWalletRepository(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : HotWalletRepository {
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
|
||||
override fun isWalletCreationSupported(): Boolean {
|
||||
return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
override fun getLeastSupportedAndroidVersionName(): String = "Android 10"
|
||||
|
||||
override fun accessCodeSkipped(userWalletId: UserWalletId): Flow<Boolean> = appPreferencesStore
|
||||
.getObjectMap<Boolean>(PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY)
|
||||
.map { it[userWalletId.stringValue] == true }
|
||||
|
|
|
|||
|
|
@ -3,15 +3,7 @@
|
|||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MultilineLambdaItParameter:DefaultCustomTokensRepository.kt$DefaultCustomTokensRepository${ // TODO: refactor https://tangem.atlassian.net/browse/AND-10006\ if (it.isTestnet() || it in excludedBlockchains || it in hotWalletExcludedBlockchains) { return@mapNotNull null } networkFactory.create( blockchain = it, extraDerivationPath = null, userWallet = userWallet, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultManageTokensRepository.kt$DefaultManageTokensRepository${ it.contractAddress != null && it.networkId == network.backendId && it.derivationPath == network.derivationPath.value }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManageTokensUpdateFetcher.kt$ManageTokensUpdateFetcher${ if (it.key == toUpdate[index].key) { Batch(it.key, updatedItems) } else { null } }</ID>
|
||||
<ID>NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$create(coinsResponse, tokensResponse, userWallet, accountIndex)</ID>
|
||||
<ID>NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet, accountIndex)</ID>
|
||||
<ID>NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$findAddedInNetworks(coinResponse.id, tokensResponse, userWallet, accountIndex)</ID>
|
||||
<ID>NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$findAddedInNetworks(testnetToken.id, tokensResponse, userWallet, accountIndex)</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:DefaultManageTokensRepository.kt$DefaultManageTokensRepository$runCatching</ID>
|
||||
<ID>UnsafeCallOnNullableType:DefaultCustomTokensRepository.kt$DefaultCustomTokensRepository$coinNetwork.decimalCount!!</ID>
|
||||
<ID>UseOrEmpty:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$testnetToken.networks?.mapNotNull { network -> createSource( networkId = network.id, contractAddress = network.address, decimals = network.decimalCount, userWallet = userWallet, accountIndex = accountIndex, ) } ?: emptyList()</ID>
|
||||
<ID>UseOrEmpty:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$tokensResponse ?.let { createCustomTokens(it, userWallet, accountIndex) } ?: emptyList()</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
|
|||
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher.Request
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultManageTokensRepository(
|
||||
|
|
@ -176,7 +177,7 @@ internal class DefaultManageTokensRepository(
|
|||
val shouldFetch = loadUserTokensFromRemote && userWallet != null
|
||||
|
||||
val fetchedResponse = if (shouldFetch) {
|
||||
runCatching { walletAccountsFetcher.fetch(userWalletId = userWallet.walletId) }.getOrNull()
|
||||
runSuspendCatching { walletAccountsFetcher.fetch(userWalletId = userWallet.walletId) }.getOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -214,19 +215,22 @@ internal class DefaultManageTokensRepository(
|
|||
userWallet != null &&
|
||||
query == null
|
||||
|
||||
val accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull()
|
||||
?: return emptyList()
|
||||
|
||||
val items = if (isCreateWithCustom) {
|
||||
managedCryptoCurrencyFactory.createWithCustomTokens(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
} else {
|
||||
managedCryptoCurrencyFactory.create(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -262,14 +266,14 @@ internal class DefaultManageTokensRepository(
|
|||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = null,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
} else {
|
||||
managedCryptoCurrencyFactory.create(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = null,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -307,6 +311,13 @@ internal class DefaultManageTokensRepository(
|
|||
)
|
||||
}
|
||||
|
||||
val accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull()
|
||||
?: return BatchFetchResult.Success(
|
||||
data = emptyList(),
|
||||
empty = true,
|
||||
last = true,
|
||||
)
|
||||
|
||||
val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens(
|
||||
testnetTokensConfig = if (!searchText.isNullOrBlank()) {
|
||||
testnetTokensConfig.copy(
|
||||
|
|
@ -320,7 +331,7 @@ internal class DefaultManageTokensRepository(
|
|||
},
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
|
||||
return BatchFetchResult.Success(
|
||||
|
|
@ -350,7 +361,7 @@ internal class DefaultManageTokensRepository(
|
|||
},
|
||||
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
|
||||
userWallet = userWallet,
|
||||
accountIndex = null,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
|
||||
return BatchFetchResult.Success(
|
||||
|
|
@ -392,10 +403,10 @@ internal class DefaultManageTokensRepository(
|
|||
)
|
||||
val newTokensList = storedTokens.tokens + addedTokens - removedTokens.toSet()
|
||||
|
||||
return newTokensList.any {
|
||||
it.contractAddress != null &&
|
||||
it.networkId == network.backendId &&
|
||||
it.derivationPath == network.derivationPath.value
|
||||
return newTokensList.any { token ->
|
||||
token.contractAddress != null &&
|
||||
token.networkId == network.backendId &&
|
||||
token.derivationPath == network.derivationPath.value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,10 +35,16 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
coinsResponse: CoinsResponse,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
userWallet: UserWallet?,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
return coinsResponse.coins.mapNotNull { coin ->
|
||||
createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet, accountIndex)
|
||||
createToken(
|
||||
coinResponse = coin,
|
||||
tokensResponse = tokensResponse,
|
||||
imageHost = coinsResponse.imageHost,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,10 +52,15 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
coinsResponse: CoinsResponse,
|
||||
tokensResponse: UserTokensResponse,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
val customTokens = createCustomTokens(tokensResponse, userWallet, accountIndex)
|
||||
val tokens = create(coinsResponse, tokensResponse, userWallet, accountIndex)
|
||||
val tokens = create(
|
||||
coinsResponse = coinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
|
||||
return customTokens + tokens
|
||||
}
|
||||
|
|
@ -58,11 +69,11 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
testnetTokensConfig: TestnetTokensConfig,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
val customTokens = tokensResponse
|
||||
?.let { createCustomTokens(it, userWallet, accountIndex) }
|
||||
?: emptyList()
|
||||
.orEmpty()
|
||||
val testnetTokens = testnetTokensConfig.tokens.map { testnetToken ->
|
||||
ManagedCryptoCurrency.Token(
|
||||
id = ManagedCryptoCurrency.ID(testnetToken.id),
|
||||
|
|
@ -77,8 +88,13 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
} ?: emptyList(),
|
||||
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, userWallet, accountIndex),
|
||||
}.orEmpty(),
|
||||
addedIn = findAddedInNetworks(
|
||||
currencyId = testnetToken.id,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +104,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
private fun createCustomTokens(
|
||||
tokensResponse: UserTokensResponse,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): List<ManagedCryptoCurrency> = tokensResponse.tokens
|
||||
.mapNotNull { token ->
|
||||
maybeCreateCustomToken(token, userWallet, accountIndex)
|
||||
|
|
@ -97,7 +113,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
private fun maybeCreateCustomToken(
|
||||
token: UserTokensResponse.Token,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): ManagedCryptoCurrency? {
|
||||
val blockchain = Blockchain.fromNetworkId(token.networkId)
|
||||
?.takeUnless { it in excludedBlockchains }
|
||||
|
|
@ -161,7 +177,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
tokensResponse: UserTokensResponse?,
|
||||
imageHost: String?,
|
||||
userWallet: UserWallet?,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): ManagedCryptoCurrency? {
|
||||
if (coinResponse.networks.isEmpty() || !coinResponse.active) return null
|
||||
|
||||
|
|
@ -184,7 +200,12 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
symbol = coinResponse.symbol,
|
||||
iconUrl = getIconUrl(coinResponse.id, imageHost),
|
||||
availableNetworks = availableNetworks,
|
||||
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, userWallet, accountIndex),
|
||||
addedIn = findAddedInNetworks(
|
||||
currencyId = coinResponse.id,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +215,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
decimals: Int?,
|
||||
userWallet: UserWallet?,
|
||||
extraDerivationPath: String? = null,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): SourceNetwork? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
?.takeUnless { it in excludedBlockchains }
|
||||
|
|
@ -235,7 +256,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
currencyId: String,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
userWallet: UserWallet?,
|
||||
accountIndex: DerivationIndex?,
|
||||
accountIndex: DerivationIndex,
|
||||
): Set<Network> {
|
||||
if (tokensResponse == null) return emptySet()
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ android {
|
|||
namespace = "com.tangem.data.nft"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Data */
|
||||
|
|
@ -53,4 +57,8 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.common.test)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue